Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions json/json.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,20 @@ priv enum WriteFrame {
///
/// Only applies to object properties, not array elements.
pub struct Replacer {
priv f : (String, Json) -> Json?
} derive(@debug.Debug)
priv kind : ReplacerKind
}

///|
priv enum ReplacerKind {
Custom((String, Json) -> Json?)
Keep(ArrayView[StringView])
Exclude(ArrayView[StringView])
}

///|
pub impl @debug.Debug for Replacer with fn to_repr(_) {
@debug.Repr::record(Map([("f", @debug.Repr::literal("<function: ...>"))]))
}

///|
/// Create a new Replacer with a custom function.
Expand Down Expand Up @@ -153,7 +165,7 @@ pub struct Replacer {
/// ```
#alias(new, deprecated="Use `Replacer()` instead")
pub fn Replacer::Replacer(f : (String, Json) -> Json?) -> Replacer {
{ f, }
{ kind: Custom(f) }
}

///|
Expand All @@ -175,7 +187,7 @@ pub fn Replacer::Replacer(f : (String, Json) -> Json?) -> Replacer {
/// }
/// ```
pub fn Replacer::keep(array : ArrayView[StringView]) -> Replacer {
{ f: (idx, value) => if array.contains(idx) { Some(value) } else { None } }
{ kind: Keep(array) }
}

///|
Expand All @@ -197,7 +209,34 @@ pub fn Replacer::keep(array : ArrayView[StringView]) -> Replacer {
/// }
/// ```
pub fn Replacer::exclude(array : ArrayView[StringView]) -> Replacer {
{ f: (idx, value) => if array.contains(idx) { None } else { Some(value) } }
{ kind: Exclude(array) }
}

///|
fn Replacer::prepare(self : Replacer) -> (String, Json) -> Json? {
match self.kind {
Custom(f) => f
Keep(array) =>
if array.length() <= 8 {
(idx, value) => if array.contains(idx) { Some(value) } else { None }
} else {
let keys : Map[String, Unit] = Map([])
for key in array {
keys[key.to_owned()] = ()
}
(idx, value) => if keys.contains(idx) { Some(value) } else { None }
}
Exclude(array) =>
if array.length() <= 8 {
(idx, value) => if array.contains(idx) { None } else { Some(value) }
} else {
let keys : Map[String, Unit] = Map([])
for key in array {
keys[key.to_owned()] = ()
}
(idx, value) => if keys.contains(idx) { None } else { Some(value) }
}
}
}

///|
Expand Down Expand Up @@ -280,6 +319,7 @@ pub fn Json::stringify(
replacer? : Replacer,
) -> String {
let buf = StringBuilder(size_hint=0)
let prepared_replacer = replacer.map(replacer => replacer.prepare())

// Explicit stack to replace recursive calls
let stack : Array[WriteFrame] = []
Expand Down Expand Up @@ -347,8 +387,8 @@ pub fn Json::stringify(
match iterator.next() {
Some((k, v)) => {
let mut v2 = v
if replacer is Some(replacer) {
if (replacer.f)(k, v) is Some(v) {
if prepared_replacer is Some(replacer) {
if replacer(k, v) is Some(v) {
v2 = v
} else {
continue None
Expand Down Expand Up @@ -527,21 +567,29 @@ fn escape(str : String, escape_slash~ : Bool) -> String {
/// - Non-object values (arrays, strings, numbers, etc.) are returned unchanged
/// - The original JSON value is not modified; a new value is returned
pub fn Json::transform(self : Self, replacer : Replacer) -> Json {
self.transform_with(replacer.prepare())
}

///|
fn Json::transform_with(
self : Self,
replacer : (String, Json) -> Json?,
) -> Json {
match self {
Object(members) =>
members
.iter()
.filter_map(pair => {
let (k, v) = pair
if (replacer.f)(k, v) is Some(v2) {
Some((k, v2.transform(replacer)))
if replacer(k, v) is Some(v2) {
Some((k, v2.transform_with(replacer)))
} else {
None
}
})
|> Map::from_iter()
|> Object
Array(members) => Array(members.map(m => m.transform(replacer)))
Array(members) => Array(members.map(m => m.transform_with(replacer)))
value => value
}
}
Expand Down
16 changes: 16 additions & 0 deletions json/json_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@ test "stringify with replacer" {
)
}

///|
test "Replacer::keep observes key array changes between uses" {
let keys : Array[StringView] = Array::makei(9, i => {
if i == 0 {
"a"
} else {
"unused-" + i.to_string()
}
})
let replacer = @json.Replacer::keep(keys)
let json : Json = { "a": 1, "b": 2 }
inspect(json.stringify(replacer~), content="{\"a\":1}")
keys[0] = "b"
inspect(json.stringify(replacer~), content="{\"b\":2}")
}

///|
test "stringify with replace recursively" {
let json : Json = {
Expand Down
3 changes: 2 additions & 1 deletion json/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ pub fn Position::equal(Self, Self) -> Bool

pub struct Replacer {
// private fields
} derive(@debug.Debug)
}
#alias(new, deprecated)
pub fn Replacer::Replacer((String, Json) -> Json?) -> Self
pub fn Replacer::exclude(ArrayView[StringView]) -> Self
pub fn Replacer::keep(ArrayView[StringView]) -> Self
pub impl @debug.Debug for Replacer

#deprecated
pub fn Json::as_array(Self) -> Array[Self]?
Expand Down
44 changes: 44 additions & 0 deletions json/replacer_bench_test.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
fn make_replacer_bench_data() -> (Json, Array[StringView]) {
let object = Map([])
let keys : Array[StringView] = Array::makei(2500, i => {
"key-" + (i * 2).to_string()
})
for i in 0..<5000 {
object["key-" + i.to_string()] = Json::number(i.to_double())
}
(Json::object(object), keys)
}

///|
test "bench Json::stringify Replacer::keep keys=2500 fields=5000" (
it : @bench.T,
) {
let (json, keys) = make_replacer_bench_data()
it.bench(fn() {
let replacer = @json.Replacer::keep(keys)
it.keep(json.stringify(replacer~).length())
})
}

///|
test "bench Json::stringify Replacer::keep keys=3 fields=5000" (it : @bench.T) {
let (json, _) = make_replacer_bench_data()
let keys : Array[StringView] = ["key-0", "key-2500", "key-4999"]
let replacer = @json.Replacer::keep(keys)
it.bench(fn() { it.keep(json.stringify(replacer~).length()) })
}
Loading