diff --git a/json/json.mbt b/json/json.mbt index c42271ca8..c84a4457d 100644 --- a/json/json.mbt +++ b/json/json.mbt @@ -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(""))])) +} ///| /// Create a new Replacer with a custom function. @@ -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) } } ///| @@ -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) } } ///| @@ -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) } + } + } } ///| @@ -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] = [] @@ -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 @@ -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 } } diff --git a/json/json_test.mbt b/json/json_test.mbt index 547719510..0a9964c9d 100644 --- a/json/json_test.mbt +++ b/json/json_test.mbt @@ -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 = { diff --git a/json/pkg.generated.mbti b/json/pkg.generated.mbti index 147a443c1..6dce682a3 100644 --- a/json/pkg.generated.mbti +++ b/json/pkg.generated.mbti @@ -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]? diff --git a/json/replacer_bench_test.mbt b/json/replacer_bench_test.mbt new file mode 100644 index 000000000..5eac6cf8c --- /dev/null +++ b/json/replacer_bench_test.mbt @@ -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()) }) +}