├── .gitignore ├── AUTHORS ├── lib ├── src │ ├── jsontool.dart │ └── json │ │ ├── sink │ │ ├── null_sink.dart │ │ ├── sink_validator.dart │ │ ├── object_writer.dart │ │ ├── string_writer.dart │ │ ├── sink.dart │ │ └── byte_writer.dart │ │ ├── reader │ │ ├── util.dart │ │ ├── reader_validator.dart │ │ ├── object_reader.dart │ │ ├── reader.dart │ │ ├── byte_reader.dart │ │ └── string_reader.dart │ │ ├── json_structure_validator.dart │ │ ├── builder │ │ └── builder.dart │ │ └── processor │ │ └── processor.dart └── jsontool.dart ├── pubspec.yaml ├── test ├── src │ └── json_data.dart ├── objectreader_test.dart ├── example_test.dart ├── benchmark │ └── benchmark.dart ├── jsonbuilder_test.dart ├── jsonsink_test.dart └── jsonreader_test.dart ├── analysis_options.yaml ├── CONTRIBUTING.md ├── README.md ├── CHANGELOG.md ├── example ├── bigint_json.dart └── planets_json.dart └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Lasse R.H. Nielsen, https://github.com/lrhn 2 | -------------------------------------------------------------------------------- /lib/src/jsontool.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export "json/builder/builder.dart"; 16 | export "json/processor/processor.dart"; 17 | export "json/reader/reader.dart"; 18 | export "json/sink/sink.dart"; 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | name: jsontool 15 | version: 2.1.0 16 | description: 17 | Low-level tools for working with JSON without creating intermediate objects. 18 | repository: https://github.com/lrhn/json-tool 19 | 20 | environment: 21 | sdk: '>=3.3.0 <4.0.0' 22 | 23 | dev_dependencies: 24 | test: ^1.24.0 25 | lints: ^3.0.0 26 | -------------------------------------------------------------------------------- /test/src/json_data.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const simpleJson = ''' 16 | { 17 | "key1": ["value11", "value12"], 18 | "key2": ["value21"], 19 | "key3": [] 20 | }'''; 21 | 22 | const complexJson = r''' 23 | [ 24 | 0, 25 | 1.1e+2, 26 | 1.1e-2, 27 | "", 28 | "abc", 29 | "ab\n\r\b\f\"\\\/z", 30 | true, 31 | false, 32 | null, 33 | [], 34 | [1], 35 | [1,2], 36 | {}, 37 | {"x":1}, 38 | {"x":1,"y":2}, 39 | [{"a":[{}]}] 40 | ]'''; 41 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | include: package:lints/recommended.yaml 15 | 16 | linter: 17 | rules: 18 | - avoid_annotating_with_dynamic 19 | - avoid_escaping_inner_quotes 20 | - avoid_final_parameters 21 | - avoid_multiple_declarations_per_line 22 | - avoid_relative_lib_imports 23 | - avoid_type_to_string 24 | - directives_ordering 25 | - leading_newlines_in_multiline_strings 26 | - omit_local_variable_types 27 | - prefer_relative_imports 28 | - use_if_null_to_convert_nulls_to_bools 29 | 30 | analyzer: 31 | errors: 32 | unnecessary_this: ignore -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project, 4 | but any larger change should be discussed using the issue tracker 5 | before submitting a pull request. 6 | There are a few other guidelines you need to follow. 7 | 8 | ## Contributor License Agreement 9 | 10 | Contributions to this project must be accompanied by a Contributor License 11 | Agreement. You (or your employer) retain the copyright to your contribution; 12 | this simply gives us permission to use and redistribute your contributions as 13 | part of the project. Head over to to see 14 | your current agreements on file or to sign a new one. 15 | 16 | You generally only need to submit a CLA once, so if you've already submitted one 17 | (even if it was for a different project), you probably don't need to do it 18 | again. 19 | 20 | ## Code reviews 21 | 22 | All submissions by non project members require review. 23 | We use GitHub pull requests for this purpose. Consult 24 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 25 | information on using pull requests. 26 | 27 | ## Community Guidelines 28 | 29 | This project follows 30 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). -------------------------------------------------------------------------------- /lib/src/json/sink/null_sink.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "sink.dart"; 16 | 17 | /// A [JsonSink] which does nothing. 18 | final class NullJsonSink implements JsonSink { 19 | const NullJsonSink(); 20 | 21 | @override 22 | void addBool(bool value) {} 23 | 24 | @override 25 | void addKey(String key) {} 26 | 27 | @override 28 | void addNull() {} 29 | 30 | @override 31 | void addNumber(num value) {} 32 | 33 | @override 34 | void addString(String value) {} 35 | 36 | @override 37 | void endArray() {} 38 | 39 | @override 40 | void endObject() {} 41 | 42 | @override 43 | void startArray() {} 44 | 45 | @override 46 | void startObject() {} 47 | } 48 | -------------------------------------------------------------------------------- /test/objectreader_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "package:jsontool/jsontool.dart"; 16 | import "package:test/test.dart"; 17 | 18 | void main() { 19 | test("simple rebuild object", () { 20 | Object? jsonObject = {"a": 1, "b": 2}; 21 | 22 | final jsonReader = JsonReader.fromObject(jsonObject); 23 | 24 | Object? newJson; 25 | final sink = jsonObjectWriter((result) { 26 | newJson = result; 27 | }); 28 | jsonReader.expectAnyValue(sink); 29 | 30 | expect(newJson, jsonObject); 31 | }); 32 | 33 | test("simple rebuild array", () { 34 | Object? jsonObject = [1, 2, 3]; 35 | 36 | final jsonReader = JsonReader.fromObject(jsonObject); 37 | 38 | Object? newJson; 39 | final sink = jsonObjectWriter((result) { 40 | newJson = result; 41 | }); 42 | jsonReader.expectAnyValue(sink); 43 | 44 | expect(newJson, jsonObject); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A collection of JSON related operations. 2 | 3 | This package contains an *experimental* API for dealing with JSON 4 | encoded data. 5 | 6 | This is not an officially supported Google product. 7 | 8 | ------- 9 | 10 | [JSON](https://json.org/) is a text format which can represent a general 11 | "JSON structure". There are many ways to generate JSON text and convert 12 | JSON text to native data structures in most languages. 13 | 14 | A JSON structure is either a *primitive value*: a number, a string, 15 | a boolean or a null value, or it is a composite value. 16 | The composite value is either a JSON array, a seqeunce of JSON structures, 17 | or a JSON object, a sequence of pairs of string keys and JSON structure values. 18 | 19 | Dart typically represents the JSON structure using `List` and `Map` objects 20 | for the composite values and `num`, `String`, `bool` and `Null` values for 21 | the primitive values. 22 | 23 | This package provides various ways to operate on a JSON structure without 24 | necessarily creating intermediate Dart lists or maps. 25 | 26 | The `JsonReader` provides a *pull based* approach to investigating and 27 | deconstructing a JSON structure, whether it's represented as a string, 28 | bytes which are the UTF-8 encoding of such a string, or by Dart object 29 | structures. 30 | 31 | The `JsonSink` provides a *push based* approach to building 32 | a JSON structure. This can be used to create JSON source or structures 33 | from other kinds of values. 34 | 35 | The `JsonBuilder` functions provide a composable way to convert a 36 | JSON structure to another kind of value. 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.0 2 | 3 | - Adds `expectStringIndex`, `tryStringIndex`, and `tryKeyIndex` 4 | to `JsonReader. Allows matching a string in a list, like 5 | the non-`Index` operations, but returns in the position in 6 | the list instead of the string at that position. 7 | - Adds `JsonReader.fail` which returns a `FormatException` 8 | at the current position in the reader, with a user provided 9 | error message. 10 | 11 | ## 2.0.0 12 | 13 | - Require SDK 3.3.0+. 14 | - Adds class modifiers. 15 | - Update lints. 16 | 17 | - Change some `void` return types to `Null` in `JsonReader` 18 | This allows uses like `reader.tryString() ?? reader.expectNull()` 19 | to match a string or `null`. 20 | - Adds `processObjectEntry` method to `JsonProcessor`, called 21 | for each object entry. 22 | - Use table to improve parsing speed for `skipAnyValue`. 23 | - Faster whitespace skipping in readers. 24 | 25 | ## 1.2.0 26 | 27 | - Allow SDK 3.0.0+. 28 | - Tweak `ByteWriter` implementation. 29 | 30 | ## 1.1.3 31 | 32 | - Typos fixed. 33 | - Bug in `byte_reader.dart` fixed. 34 | 35 | ## 1.1.2 36 | 37 | - Optimizes to avoid or cheapen `as` casts where possible. 38 | Uses `as dynamic` with a context type where a cast cannot be avoided, 39 | for better dart2js performance. 40 | 41 | ## 1.1.1 42 | 43 | - Populate the pubspec's `repository` field. 44 | - Use `package:lints` for linting. 45 | - Adds `processObjectEntry` to `JsonProcessor`. 46 | 47 | ## 1.1.0 48 | 49 | - Null safe. 50 | - Adds `JsonWriter` for JSON sinks which generate JSON-like structures. 51 | Allows injecting "source" (of a matching format) directly into the structure. 52 | - Adds `JsonProcessor`. Like a `JsonSink` but gets the `JsonReader` so it can 53 | process the values itself instead of getting the processed value. 54 | - Adds `JsonReader.hasNextKey` and some methods on `StringSlice`. 55 | 56 | ## 1.0.1 57 | 58 | - Add CHANGELOG.md. 59 | 60 | ## 1.0.0 61 | 62 | - Initial Release 63 | -------------------------------------------------------------------------------- /lib/jsontool.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A collection of JSON related operations. 16 | /// 17 | /// [JSON](https://json.org/) is a text format which can represent a general 18 | /// "JSON structure". There are many ways to generate JSON text and convert 19 | /// JSON text to native data structures in most languages. 20 | /// 21 | /// A JSON structure is either a *primitive value*: a number, a string, 22 | /// a boolean or a null value, or it is a composite value. 23 | /// The composite value is either a JSON array, a seqeunce of JSON structures, 24 | /// or a JSON object, a sequence of pairs of string keys and JSON structure values. 25 | /// 26 | /// Dart typically represents the JSON structure using [List] and [Map] objects 27 | /// for the composite values and [num], [String], [bool] and [Null] values for 28 | /// the primitive values. 29 | /// 30 | /// This package provides various ways to operate on a JSON structure without 31 | /// necessarily creating intermediate Dart lists or maps. 32 | /// 33 | /// The [JsonReader] provides a *pull based* approach to investigating and 34 | /// deconstructing a JSON structure, whether it's represented as a string, 35 | /// bytes which are the UTF-8 encoding of such a string, or by Dart object 36 | /// structures. 37 | /// 38 | /// The [JsonSink] provides a *push based* approach to building 39 | /// a JSON structure. This can be used to create JSON source or structures 40 | /// from other kinds of values. 41 | /// 42 | /// The [JsonBuilder] functions provide a composable way to convert a 43 | /// JSON structure to another kind of value. 44 | library jsontool; 45 | 46 | export "src/jsontool.dart"; 47 | -------------------------------------------------------------------------------- /test/example_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "dart:async"; 16 | 17 | import "package:test/test.dart"; 18 | 19 | import "../example/bigint_json.dart" as bigint; 20 | import "../example/planets_json.dart" as planets; 21 | 22 | const planetsOutput = """ 23 | Mercury is a Rock Planet and has a volume 0.05 times that of the Earth 24 | Venus is a Rock Planet and has a volume 0.86 times that of the Earth 25 | Earth is a Rock Planet and has a volume 1.00 times that of the Earth 26 | Mars is a Rock Planet and has a volume 0.15 times that of the Earth 27 | Saturn is a Gas Giant and has a volume 843.91 times that of the Earth 28 | Jupiter is a Gas Giant and has a volume 1404.93 times that of the Earth 29 | Uranus is a Ice Giant and has a volume 64.00 times that of the Earth 30 | Neptune is a Ice Giant and has a volume 58.41 times that of the Earth 31 | Pluto is a Rock Planet and has a volume 0.01 times that of the Earth 32 | """; 33 | 34 | const bigintOutput = """ 35 | source: {"x":123456789123456789123456789123456789} 36 | big value: 123456789123456789123456789123456789 37 | new source: {"x":123456789123456789123456789123456789} 38 | """; 39 | 40 | void main() { 41 | group("Ensure examples are running:", () { 42 | test("planets", () { 43 | expect(capturePrint(planets.main), planetsOutput); 44 | }); 45 | test("bigint", () { 46 | expect(capturePrint(bigint.main), bigintOutput); 47 | }); 48 | }); 49 | } 50 | 51 | // Captures *synchronous* prints, avoids them going to the console. 52 | String capturePrint(void Function() action) { 53 | var capture = StringBuffer(); 54 | runZoned(action, zoneSpecification: ZoneSpecification(print: (s, p, z, text) { 55 | capture.writeln(text); 56 | })); 57 | return capture.toString(); 58 | } 59 | -------------------------------------------------------------------------------- /test/benchmark/benchmark.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "dart:convert"; 16 | 17 | import "package:jsontool/jsontool.dart"; 18 | import "package:jsontool/src/json/sink/null_sink.dart"; 19 | 20 | import "../src/json_data.dart"; 21 | 22 | void main() { 23 | benchByteReader(); 24 | benchStringReader(); 25 | } 26 | 27 | void benchByteReader() { 28 | _benchByteReader(100); 29 | for (var i = 0; i < 3; i++) { 30 | _benchByteReader(1500); 31 | } 32 | } 33 | 34 | void _benchByteReader(int limit) { 35 | var inputUtf8 = utf8.encode(complexJson); 36 | var sink = NullJsonSink(); 37 | var reader = JsonReader.fromUtf8(inputUtf8); 38 | 39 | var c = 0; 40 | var e = 0; 41 | var n = 100; 42 | var sw = Stopwatch()..start(); 43 | do { 44 | for (var i = 0; i < n; i++) { 45 | reader.copy().expectAnyValue(sink); 46 | } 47 | e = sw.elapsedMilliseconds; 48 | c += n; 49 | n *= 2; 50 | } while (e < limit); 51 | 52 | if (limit >= 200) { 53 | print("UTF-8 JsonReader: ${(c / e).toStringAsFixed(3)} parses/ms"); 54 | } 55 | } 56 | 57 | void benchStringReader() { 58 | _benchStringReader(100); 59 | for (var i = 0; i < 3; i++) { 60 | _benchStringReader(1500); 61 | } 62 | } 63 | 64 | void _benchStringReader(int limit) { 65 | var input = complexJson; 66 | var sink = NullJsonSink(); 67 | var reader = JsonReader.fromString(input); 68 | 69 | var c = 0; 70 | var e = 0; 71 | var n = 100; 72 | var sw = Stopwatch()..start(); 73 | do { 74 | for (var i = 0; i < n; i++) { 75 | reader.copy().expectAnyValue(sink); 76 | } 77 | e = sw.elapsedMilliseconds; 78 | c += n; 79 | n *= 2; 80 | } while (e < limit); 81 | 82 | if (limit >= 200) { 83 | print("String JsonReader: ${(c / e).toStringAsFixed(3)} parses/ms"); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/json/sink/sink_validator.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "../json_structure_validator.dart"; 16 | 17 | import "sink.dart"; 18 | 19 | /// Validating [JsonSink] which checks that methods are only used correctly. 20 | /// 21 | /// Maintains an internal state machine which knows whether the sink is 22 | /// currently expecting a top-level value, an array value or an 23 | /// object key or value. 24 | final class ValidatingJsonSink implements JsonSink { 25 | /// The original sink. All method calls are forwarded to this after validation. 26 | final JsonSink _sink; 27 | 28 | final JsonStructureValidator _validator; 29 | 30 | ValidatingJsonSink(this._sink, bool allowReuse) 31 | : _validator = JsonStructureValidator(allowReuse: allowReuse); 32 | 33 | @override 34 | void addBool(bool value) { 35 | _validator.value(); 36 | _sink.addBool(value); 37 | } 38 | 39 | @override 40 | void addKey(String key) { 41 | _validator.key(); 42 | _sink.addKey(key); 43 | } 44 | 45 | @override 46 | void addNull() { 47 | _validator.value(); 48 | _sink.addNull(); 49 | } 50 | 51 | @override 52 | void addNumber(num value) { 53 | _validator.value(); 54 | _sink.addNumber(value); 55 | } 56 | 57 | @override 58 | void addString(String value) { 59 | _validator.value(); 60 | _sink.addString(value); 61 | } 62 | 63 | @override 64 | void endArray() { 65 | _validator.endArray(); 66 | _sink.endArray(); 67 | } 68 | 69 | @override 70 | void endObject() { 71 | _validator.endObject(); 72 | _sink.endObject(); 73 | } 74 | 75 | @override 76 | void startArray() { 77 | _validator.startArray(); 78 | _sink.startArray(); 79 | } 80 | 81 | @override 82 | void startObject() { 83 | _validator.startObject(); 84 | _sink.startObject(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example/bigint_json.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// An example of how to read JSON numbers into custom number types. 16 | /// 17 | /// Here integers are parsed into [BigInt] and floating points are 18 | /// kept as doubles. 19 | /// Choosing other representations is a matter of adapting the 20 | /// `processNum`/`processUnknown` overrides in the [JsonProcessor] 21 | /// sub-classes. 22 | library; 23 | 24 | import "package:jsontool/jsontool.dart"; 25 | 26 | /// Parses a JSON string into JSON-like object structure. 27 | /// 28 | /// Like normal JSON parsing except the all integers are represented 29 | /// by [BigInt] values. 30 | Object? parseBigIntJson(String source) { 31 | Object? value; 32 | _BigIntJsonReader((result) { 33 | value = result; 34 | }).processValue(JsonReader.fromString(source)); 35 | return value; 36 | } 37 | 38 | /// Converts a JSON-like object structure to a string. 39 | /// 40 | /// Any [BigInt] values are written into the result as number literals. 41 | String serializeBigIntJson(Object? value, {String? indent}) { 42 | var buffer = StringBuffer(); 43 | var serializer = _BigIntJsonWriter(buffer, indent); 44 | serializer.processValue(JsonReader.fromObject(value)); 45 | return buffer.toString(); 46 | } 47 | 48 | final class _BigIntJsonWriter 49 | extends JsonSinkProcessor, JsonWriter> { 50 | _BigIntJsonWriter(StringSink sink, String? indent) 51 | : super(jsonStringWriter(sink, indent: indent)); 52 | 53 | @override 54 | void processUnknown(JsonReader reader, String? key) { 55 | // The source of a JsonReader is the unknown object. 56 | var value = reader.expectAnyValueSource(); 57 | if (value is BigInt) { 58 | if (key != null) sink.addKey(key); 59 | sink.addSourceValue(value.toString()); 60 | } else { 61 | throw FormatException("Unexpected value", value); 62 | } 63 | } 64 | } 65 | 66 | final class _BigIntJsonReader 67 | extends JsonSinkProcessor, JsonWriter> { 68 | _BigIntJsonReader(void Function(Object?) resultCallback) 69 | : super(jsonObjectWriter(resultCallback)); 70 | 71 | @override 72 | void processNum(JsonReader reader, String? key) { 73 | if (key != null) sink.addKey(key); 74 | var source = reader.expectAnyValueSource().toString(); 75 | assert(source.isNotEmpty); 76 | var result = BigInt.tryParse(source); 77 | if (result != null) { 78 | sink.addSourceValue(result); 79 | return; 80 | } 81 | sink.addNumber(double.parse(source)); 82 | } 83 | } 84 | 85 | void main() { 86 | // Start with a JSON containing a number too big for Dart integers. 87 | var source = '{"x":123456789123456789123456789123456789}'; 88 | print("source: $source"); 89 | var data = parseBigIntJson(source) as Map; 90 | print("big value: ${data["x"]}"); // The big integer. 91 | var newSource = serializeBigIntJson(data); 92 | print("new source: $newSource"); // Roundtripped. 93 | assert(source == newSource); 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/json/reader/util.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Utility functions. 16 | 17 | /// Used to validate the argument to [JsonReader.tryKey]. 18 | bool areSorted(List candidates) { 19 | for (var i = 1; i < candidates.length; i++) { 20 | if (candidates[i - 1].compareTo(candidates[i]) > 0) return false; 21 | } 22 | return true; 23 | } 24 | 25 | bool isWhitespace(int char) => 26 | char == $tab || char == $nl || char == $cr || char == $space; 27 | 28 | /// ASCII character table with mapping from JSON-significant characters 29 | /// to consecutive integers. 30 | /// 31 | /// * `[`: 0 32 | /// * `{`: 1 33 | /// * `"`: 2 34 | /// * `t`: 3 35 | /// * `f`: 4 36 | /// * `n`: 5 37 | /// * `0`-`9`, `-`: 6 38 | /// * `,`: 7 39 | /// * `:`, 8 40 | const jsonCharacters = 41 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 42 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 43 | "\xFF\xFF\x02\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x07\x06\xFF\xFF" 44 | "\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x08\xFF\xFF\xFF\xFF\xFF" 45 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 46 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF" 47 | "\xFF\xFF\xFF\xFF\xFF\xFF\x04\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x05\xFF" 48 | "\xFF\xFF\xFF\xFF\x03\xFF\xFF\xFF\xFF\xFF\xFF\x01\xFF\xFF\xFF\xFF"; 49 | 50 | // JSON-significant character constants used by JSON readers. 51 | 52 | /// Character `\t`. 53 | const int $tab = 0x09; 54 | 55 | /// Character `\n`. 56 | const int $nl = 0x0a; 57 | 58 | /// Character `\r`. 59 | const int $cr = 0x0d; 60 | 61 | /// Character space. 62 | const int $space = 0x20; 63 | 64 | /// Character `0`. 65 | const int $0 = 0x30; 66 | 67 | /// Character `a`. 68 | const int $a = 0x61; 69 | 70 | /// Character `b`. 71 | const int $b = 0x62; 72 | 73 | /// Character `\`. 74 | const int $backslash = 0x5C; 75 | 76 | /// Character `:`. 77 | const int $colon = 0x3A; 78 | 79 | /// Character `,`. 80 | const int $comma = 0x2C; 81 | 82 | /// Character `.`. 83 | const int $dot = 0x2E; 84 | 85 | /// Character `3`. 86 | const int $e = 0x65; 87 | 88 | /// Character `f`. 89 | const int $f = 0x66; 90 | 91 | /// Character `{`. 92 | const int $lbrace = 0x7B; 93 | 94 | /// Character `[`. 95 | const int $lbracket = 0x5B; 96 | 97 | /// Character `-`. 98 | const int $minus = 0x2D; 99 | 100 | /// Character `n`. 101 | const int $n = 0x6e; 102 | 103 | /// Character `+`. 104 | const int $plus = 0x2B; 105 | 106 | /// Character `"`. 107 | const int $quot = 0x22; 108 | 109 | /// Character `r`. 110 | const int $r = 0x72; 111 | 112 | /// Character `}`. 113 | const int $rbrace = 0x7D; 114 | 115 | /// Character `]`. 116 | const int $rbracket = 0x5D; 117 | 118 | /// Character `/`. 119 | const int $slash = 0x2F; 120 | 121 | /// Character `t`. 122 | const int $t = 0x74; 123 | 124 | /// Character `u`. 125 | const int $u = 0x75; 126 | 127 | /// Character `z`. 128 | const int $z = 0x7a; 129 | -------------------------------------------------------------------------------- /lib/src/json/sink/object_writer.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "sink.dart"; 16 | 17 | /// A [JsonSink] which builds Dart object structures. 18 | /// 19 | /// I only the plain [JsonSink] methods are used, in correct order, 20 | /// then the structure will represent valid JSON and can be traversed 21 | /// by the reader of [JsonReader.fromObject]. 22 | final class JsonObjectWriter implements JsonWriter { 23 | /// The callback which is called for each complete JSON object. 24 | final void Function(Object?) _result; 25 | 26 | /// Stack of objects or arrays being built, and pending [_key] values. 27 | final List _stack = []; 28 | 29 | /// Last key added using [addKey]. 30 | String? _key; 31 | 32 | // Typed caches of the top of the [_stack]. 33 | // Set when pushing a new map or list on the stack. 34 | // Filled in lazily when first needed after popping the stack, 35 | Map? _topObjectCache; 36 | List? _topArrayCache; 37 | 38 | JsonObjectWriter(this._result); 39 | 40 | void _value(Object? value) { 41 | if (_stack.isEmpty) { 42 | _result(value); 43 | _key = null; 44 | } else { 45 | var key = _key; 46 | if (key != null) { 47 | Map topObject = _topObjectCache ??= (_stack.last as dynamic); 48 | topObject[key] = value; 49 | _key = null; 50 | } else { 51 | List topArray = _topArrayCache ??= (_stack.last as dynamic); 52 | topArray.add(value); 53 | } 54 | } 55 | } 56 | 57 | @override 58 | void addBool(bool value) { 59 | _value(value); 60 | } 61 | 62 | @override 63 | void endArray() { 64 | var top = _stack.removeLast(); 65 | List array = _topArrayCache ?? (top as dynamic); 66 | _key = _stack.removeLast() as dynamic; 67 | _topArrayCache = null; 68 | _value(array); 69 | } 70 | 71 | @override 72 | void endObject() { 73 | var top = _stack.removeLast(); 74 | Map object = _topObjectCache ?? (top as dynamic); 75 | _key = _stack.removeLast() as dynamic; 76 | _topObjectCache = null; 77 | _value(object); 78 | } 79 | 80 | @override 81 | void addKey(String key) { 82 | _key = key; 83 | } 84 | 85 | @override 86 | void addNull() { 87 | _value(null); 88 | } 89 | 90 | @override 91 | void addNumber(num value) { 92 | _value(value); 93 | } 94 | 95 | @override 96 | void startArray() { 97 | var array = []; 98 | _stack.add(_key); 99 | _stack.add(array); 100 | _topArrayCache = array; 101 | _topObjectCache = null; 102 | _key = null; 103 | } 104 | 105 | @override 106 | void startObject() { 107 | var object = {}; 108 | _stack.add(_key); 109 | _stack.add(object); 110 | _topArrayCache = null; 111 | _topObjectCache = object; 112 | _key = null; 113 | } 114 | 115 | @override 116 | void addString(String value) { 117 | _value(value); 118 | } 119 | 120 | @override 121 | void addSourceValue(Object? source) { 122 | _value(source); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/jsonbuilder_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "package:jsontool/jsontool.dart"; 16 | import "package:test/test.dart"; 17 | 18 | void main() { 19 | JsonReader read(String source) => JsonReader.fromString(source); 20 | 21 | test("primitive values", () { 22 | expect(jsonInt(read("1")), 1); 23 | expect(jsonDouble(read("1.5")), 1.5); 24 | expect(jsonString(read('"str"')), "str"); 25 | expect(jsonBool(read("false")), false); 26 | }); 27 | 28 | test("optional values", () { 29 | expect(jsonOptional(jsonInt)(read("1")), 1); 30 | expect(jsonOptional(jsonInt)(read("null")), null); 31 | expect(jsonOptional(jsonDouble)(read("1.5")), 1.5); 32 | expect(jsonOptional(jsonDouble)(read("null")), null); 33 | expect(jsonOptional(jsonString)(read('"str"')), "str"); 34 | expect(jsonOptional(jsonString)(read("null")), null); 35 | expect(jsonOptional(jsonBool)(read("false")), false); 36 | expect(jsonOptional(jsonBool)(read("null")), null); 37 | }); 38 | 39 | test("array values", () { 40 | var jsonStringArray = jsonArray(jsonString); 41 | expect(jsonStringArray(read('[]')), []); 42 | 43 | expect(jsonStringArray(read('["a"]')), ["a"]); 44 | 45 | expect(jsonStringArray(read('["a", "b"]')), ["a", "b"]); 46 | 47 | expect(jsonArray(jsonStringArray)(read('[[], ["a"], ["a", "b"]]')), [ 48 | [], 49 | ["a"], 50 | ["a", "b"] 51 | ]); 52 | }); 53 | 54 | test("array index builder", () { 55 | var intStringArray = 56 | jsonIndexedArray((index) => index.isEven ? jsonInt : jsonString); 57 | expect(intStringArray(read('[]')), []); 58 | expect(intStringArray(read('[1, "2", 3, "4", 5]')), [1, "2", 3, "4", 5]); 59 | }); 60 | 61 | test("array fold builder", () { 62 | var foldArray = jsonFoldArray(jsonInt, () => 0, (int a, int b) => a + b); 63 | expect(foldArray(read('[]')), 0); 64 | expect(foldArray(read('[1]')), 1); 65 | expect(foldArray(read('[1, 2, 3, 4, 5]')), 15); 66 | }); 67 | 68 | test("object values", () { 69 | expect(jsonObject(jsonString)(read('{}')), {}); 70 | 71 | expect(jsonObject(jsonInt)(read('{"a": 2}')), {"a": 2}); 72 | 73 | expect(jsonObject(jsonInt)(read('{"a": 1, "b": 2}')), {"a": 1, "b": 2}); 74 | 75 | expect( 76 | jsonObject(jsonObject(jsonInt))( 77 | read('{"0": {}, "2": {"a": 1, "b": 2}}')), 78 | { 79 | "0": {}, 80 | "2": {"a": 1, "b": 2} 81 | }); 82 | }); 83 | 84 | test("object key builder", () { 85 | var objectKeys = 86 | jsonStruct({"x": jsonInt, "y": jsonString, "z": jsonArray(jsonBool)}); 87 | expect(objectKeys(read('{}')), {}); 88 | expect(objectKeys(read('{"x": 1}')), {"x": 1}); 89 | expect(objectKeys(read('{"other": 3.14, "x": 1}')), {"x": 1}); 90 | expect(objectKeys(read('{"y": "str", "x": 1}')), {"x": 1, "y": "str"}); 91 | expect(objectKeys(read('{"y": "str", "x": 1, "z": [true, false]}')), { 92 | "x": 1, 93 | "y": "str", 94 | "z": [true, false] 95 | }); 96 | var objectKeysDefault = jsonStruct( 97 | {"x": jsonInt, "y": jsonString, "z": jsonArray(jsonBool)}, jsonDouble); 98 | expect(objectKeysDefault(read('{"other": 3.14, "x": 1}')), 99 | {"x": 1, "other": 3.14}); 100 | }); 101 | 102 | test("object fold builder", () { 103 | var objectFold = 104 | jsonFoldObject(jsonInt, () => 0, (int a, String? key, int b) => a + b); 105 | expect(objectFold(read('{}')), 0); 106 | expect(objectFold(read('{"x": 1}')), 1); 107 | expect(objectFold(read('{"x": 1, "y": 2, "z": 3}')), 6); 108 | var objectFoldKey = jsonFoldObject( 109 | jsonInt, 110 | () => [], 111 | (List a, String? key, int b) => a 112 | ..add(key) 113 | ..add(b)); 114 | expect(objectFoldKey(read('{"x": 1, "y": 2, "z": 3}')), 115 | ["x", 1, "y", 2, "z", 3]); 116 | }); 117 | 118 | test("date build", () { 119 | var date = DateTime.now(); 120 | var dateString = date.toString(); 121 | expect(jsonDateString(read('"$dateString"')), date); 122 | }); 123 | 124 | test("uri build", () { 125 | var uri = Uri.parse("https://example.com/pathway"); 126 | var uriString = uri.toString(); 127 | expect(jsonUriString(read('"$uriString"')), uri); 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /test/jsonsink_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "dart:convert"; 16 | import 'dart:io'; 17 | import 'dart:typed_data'; 18 | 19 | import "package:jsontool/jsontool.dart"; 20 | import "package:test/test.dart"; 21 | 22 | import "src/json_data.dart"; 23 | 24 | void main() { 25 | test("simple rebuild", () { 26 | var simple = jsonDecode(simpleJson); 27 | Object? builtSimple; 28 | JsonReader.fromString(simpleJson).expectAnyValue(jsonObjectWriter((result) { 29 | builtSimple = result; 30 | })); 31 | expect(simple, builtSimple); 32 | }); 33 | 34 | test("simple toString", () { 35 | var simple = jsonEncode(jsonDecode(simpleJson)); 36 | var buffer = StringBuffer(); 37 | JsonReader.fromString(simpleJson).expectAnyValue(jsonStringWriter(buffer)); 38 | expect(simple, buffer.toString()); 39 | }); 40 | 41 | group('jsonObjectWriter', () { 42 | Object? result; 43 | setUp(() => result = null); 44 | 45 | testJsonSink( 46 | createSink: () => jsonObjectWriter((out) => result = out), 47 | parsedResult: () => jsonEncode(result), 48 | ); 49 | }); 50 | 51 | group('jsonStringWriter', () { 52 | var buffer = StringBuffer(); 53 | setUp(() => buffer.clear()); 54 | 55 | testJsonSink( 56 | createSink: () => jsonStringWriter(buffer), 57 | parsedResult: () => buffer.toString(), 58 | ); 59 | }); 60 | 61 | group('jsonByteWriter', () { 62 | var builder = BytesBuilder(); 63 | setUp(() => builder.clear()); 64 | 65 | testJsonSink( 66 | createSink: () => 67 | jsonByteWriter(ByteConversionSink.withCallback(builder.add)), 68 | parsedResult: () => utf8.decode(builder.toBytes()), 69 | ); 70 | }); 71 | 72 | group("validating sink,", () { 73 | test("single value", () { 74 | var sink = validateJsonSink(nullJsonSink); 75 | expectValue(sink); 76 | sink.addNumber(1); 77 | expectDone(sink); 78 | }); 79 | test("single value, reusable", () { 80 | var sink = validateJsonSink(nullJsonSink, allowReuse: true); 81 | expectValue(sink); 82 | sink.addNumber(1); 83 | expectValue(sink); 84 | sink.addString("x"); 85 | }); 86 | test("composite", () { 87 | var sink = validateJsonSink(nullJsonSink); 88 | expectValue(sink); 89 | sink.startArray(); 90 | expectValue(sink, insideArray: true); 91 | sink.addString("x"); 92 | expectValue(sink, insideArray: true); 93 | sink.startObject(); 94 | expectKey(sink); 95 | sink.addKey("x"); 96 | expectValue(sink); 97 | sink.startArray(); 98 | expectValue(sink, insideArray: true); 99 | sink.endArray(); 100 | expectKey(sink); 101 | sink.endObject(); 102 | expectValue(sink, insideArray: true); 103 | sink.endArray(); 104 | expectDone(sink); 105 | }); 106 | }); 107 | } 108 | 109 | void testJsonSink({ 110 | required JsonSink Function() createSink, 111 | required String Function() parsedResult, 112 | }) { 113 | test('simple', () { 114 | JsonReader.fromString(simpleJson).expectAnyValue(createSink()); 115 | var simple = jsonEncode(jsonDecode(simpleJson)); 116 | expect(parsedResult(), simple); 117 | }); 118 | 119 | test('complex', () { 120 | var complex = jsonEncode(jsonDecode(complexJson)); 121 | JsonReader.fromString(complexJson).expectAnyValue(createSink()); 122 | expect(parsedResult(), complex); 123 | }); 124 | } 125 | 126 | // Utility functions for checking validating sink. 127 | void expectNoKey(JsonSink sink) { 128 | expect(() => sink.addKey("a"), throwsStateError); 129 | } 130 | 131 | void expectNoValue(JsonSink sink) { 132 | expect(() => sink.addBool(true), throwsStateError); 133 | expect(() => sink.addNull(), throwsStateError); 134 | expect(() => sink.addNumber(1), throwsStateError); 135 | expect(() => sink.addString(""), throwsStateError); 136 | expect(() => sink.startArray(), throwsStateError); 137 | expect(() => sink.startObject(), throwsStateError); 138 | } 139 | 140 | void expectKey(JsonSink sink) { 141 | expectNoValue(sink); 142 | expect(() => sink.endArray(), throwsStateError); 143 | } 144 | 145 | void expectValue(JsonSink sink, {bool insideArray = false}) { 146 | expectNoKey(sink); 147 | expect(() => sink.endObject(), throwsStateError); 148 | if (!insideArray) { 149 | expect(() => sink.endArray(), throwsStateError); 150 | } 151 | } 152 | 153 | void expectDone(JsonSink sink) { 154 | expectNoValue(sink); 155 | expectNoKey(sink); 156 | expect(() => sink.endObject(), throwsStateError); 157 | expect(() => sink.endArray(), throwsStateError); 158 | } 159 | -------------------------------------------------------------------------------- /example/planets_json.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "dart:convert"; 16 | 17 | import "package:jsontool/jsontool.dart"; 18 | 19 | const jsonText = """ 20 | { 21 | "version": 1, 22 | "planets": [ 23 | {"name": "Mercury", "type": "rock", "size": 0.38}, 24 | {"name": "Venus", "type": "rock", "size": 0.95}, 25 | {"name": "Earth", "type": "rock", "size": 1.0 }, 26 | {"name": "Mars", "type": "rock", "size": 0.53}, 27 | {"name": "Saturn", "type": "gas", "size": 9.45}, 28 | {"name": "Jupiter", "type": "gas", "size": 11.2}, 29 | {"name": "Uranus", "type": "ice", "size": 4.0 }, 30 | {"name": "Neptune", "type": "ice", "size": 3.88}, 31 | {"name": "Pluto", "type": "rock", "size": 0.18} 32 | ] 33 | } 34 | """; 35 | 36 | /// Enumeration of types of planets. 37 | enum PlanetType implements JsonWritable { 38 | /// The type of gas giants. 39 | gas("gas", "Gas Giant"), 40 | 41 | /// The type of ice giants. 42 | ice("ice", "Ice Giant"), 43 | 44 | /// The type of rock based planets. 45 | rock("rock", "Rock Planet"); 46 | 47 | /// String key used to represent the type 48 | final String key; 49 | 50 | /// A descriptive representation of the type. 51 | final String description; 52 | 53 | const PlanetType(this.key, this.description); 54 | 55 | /// Look up a planet type by its [PlanetType.key]. 56 | static PlanetType? fromKey(String key) => const { 57 | "gas": gas, 58 | "ice": ice, 59 | "rock": rock, 60 | }[key]; 61 | 62 | static PlanetType readJson(JsonReader reader) { 63 | var keyIndex = reader.tryStringIndex(const ["gas", "ice", "rock"]); 64 | if (keyIndex == null) { 65 | throw reader.fail('Not a planet type: "gas", "ice" or "rock"'); 66 | } 67 | return values[keyIndex]; 68 | } 69 | 70 | @override 71 | void writeJson(JsonSink target) { 72 | target.addString(key); 73 | } 74 | 75 | @override 76 | String toString() => description; 77 | } 78 | 79 | /// A celestial object orbiting a star. 80 | class Planet implements JsonWritable { 81 | /// Traditional name of the planet. 82 | final String name; 83 | 84 | /// Diameter relative to Earth's diameter. 85 | final double sizeIndex; 86 | 87 | /// The kind of planet. 88 | final PlanetType type; 89 | 90 | Planet(this.name, this.sizeIndex, this.type); 91 | 92 | @override 93 | String toString() => name; 94 | 95 | @override 96 | void writeJson(JsonSink target) { 97 | target 98 | ..startObject() 99 | ..addStringEntry("name", name) 100 | ..addNumberEntry("size", sizeIndex) 101 | ..addWritableEntry("name", type) 102 | ..endObject(); 103 | } 104 | 105 | /// Builds a planet from a JSON object. 106 | /// 107 | /// Properties of the object must be: 108 | /// * `name`: A string. 109 | /// * `size`: A double. 110 | /// * `type`: A string. One of `"gas"`, `"ice"` or `"rock"`. 111 | static Planet readJson(JsonReader reader) { 112 | reader.expectObject(); 113 | String? name; 114 | PlanetType? type; 115 | double? sizeIndex; 116 | while (reader.hasNextKey()) { 117 | switch (reader.tryKeyIndex(const ["name", "size", "type"])) { 118 | case 0: // "name" 119 | name = reader.tryString() ?? 120 | (throw reader.fail("Planet name must be a string")); 121 | break; 122 | case 1: // "size" 123 | sizeIndex = reader.tryDouble() ?? 124 | (throw reader.fail("Plane size index must be a number")); 125 | break; 126 | case 2: // "type" 127 | type = PlanetType.readJson(reader); 128 | break; 129 | default: 130 | reader.skipObjectEntry(); 131 | } 132 | } 133 | // No more entries in JSON object. 134 | if (name == null) { 135 | throw reader.fail("Planet missing name"); 136 | } 137 | if (type == null) { 138 | throw reader.fail("Planet $name missing type"); 139 | } 140 | if (sizeIndex == null) { 141 | throw reader.fail("Planet $name missing size"); 142 | } 143 | return Planet(name, sizeIndex, type); 144 | } 145 | } 146 | 147 | /// Build planets from a planet registry. 148 | /// 149 | /// A planet registry is a JSON object with a 150 | /// `"planets"` entry containing an array of planets. 151 | /// 152 | /// It may contain a `"vesion"` entry too. If so, the 153 | /// version number must be the integer `1` until 154 | /// further versions are provided. 155 | List buildPlanets(JsonReader reader) { 156 | reader.expectObject(); 157 | var result = []; 158 | while (true) { 159 | const planets = 0; 160 | const version = 1; 161 | var key = reader.tryKeyIndex(const ["planets", "version"]); 162 | if (key == version) { 163 | var version = reader.expectInt(); 164 | if (version != 1) throw FormatException("Unknown version"); 165 | } else if (key == planets) { 166 | reader.expectArray(); 167 | while (reader.hasNext()) { 168 | result.add(Planet.readJson(reader)); 169 | } 170 | } else { 171 | if (!reader.skipObjectEntry()) { 172 | // No entries left. 173 | break; 174 | } 175 | } 176 | } 177 | return result; 178 | } 179 | 180 | /// Example use. 181 | void main() { 182 | // Reader planets from a JSON string. 183 | var reader = JsonReader.fromString(jsonText); 184 | // reader = ValidatingJsonReader(reader); // Convenient while debugging. 185 | var planets = buildPlanets(reader); 186 | 187 | // Show them. 188 | for (var planet in planets) { 189 | var size = planet.sizeIndex; 190 | var volume = size * size * size; 191 | print("$planet is a ${planet.type} and has a volume " 192 | "${volume.toStringAsFixed(2)} times that of the Earth"); 193 | } 194 | 195 | // Read same planets from a UTF-8 encoding. 196 | var bytes = utf8.encode(jsonText); 197 | var utf8Reader = JsonReader.fromUtf8(bytes); 198 | // utf8Reader = ValidatingJsonReader(utf8Reader); 199 | var planetsAgain = buildPlanets(utf8Reader); 200 | 201 | // The `readJson` methods work on any `JsonReader`, and gives the same result. 202 | assert(planets.length == planetsAgain.length); 203 | for (var i = 0; i < planets.length; i++) { 204 | assert(planets[i].name == planetsAgain[i].name); 205 | assert(planets[i].sizeIndex == planetsAgain[i].sizeIndex); 206 | assert(planets[i].type == planetsAgain[i].type); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/src/json/sink/string_writer.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "sink.dart"; 16 | 17 | /// A [JsonSink] which builds a textual representation of the JSON structure. 18 | /// 19 | /// The resulting string representation is a minimal JSON text with no 20 | /// whitespace between tokens. 21 | final class JsonStringWriter implements JsonWriter { 22 | final StringSink _sink; 23 | final bool _asciiOnly; 24 | String _separator = ""; 25 | 26 | /// Creates a writer which writes the result into [target]. 27 | /// 28 | /// When an entire JSON value has been written, [target] 29 | /// will contain the string representation. 30 | /// 31 | /// If [asciiOnly] is true, string values will escape any non-ASCII 32 | /// character. If not, only control characters are escaped. 33 | JsonStringWriter(StringSink target, {bool asciiOnly = false}) 34 | : _sink = target, 35 | _asciiOnly = asciiOnly; 36 | 37 | @override 38 | void addBool(bool value) { 39 | _sink.write(_separator); 40 | _sink.write(value); 41 | _separator = ","; 42 | } 43 | 44 | @override 45 | void endArray() { 46 | _sink.write("]"); 47 | _separator = ","; 48 | } 49 | 50 | @override 51 | void endObject() { 52 | _sink.write("}"); 53 | _separator = ","; 54 | } 55 | 56 | @override 57 | void addKey(String key) { 58 | _sink.write(_separator); 59 | _writeString(_sink, key, _asciiOnly); 60 | _separator = ":"; 61 | } 62 | 63 | @override 64 | void addNull() { 65 | _sink.write(_separator); 66 | _sink.write("null"); 67 | _separator = ","; 68 | } 69 | 70 | @override 71 | void addNumber(num value) { 72 | _sink.write(_separator); 73 | _sink.write(value); 74 | _separator = ","; 75 | } 76 | 77 | @override 78 | void startArray() { 79 | _sink.write(_separator); 80 | _sink.write("["); 81 | _separator = ""; 82 | } 83 | 84 | @override 85 | void startObject() { 86 | _sink.write(_separator); 87 | _sink.write("{"); 88 | _separator = ""; 89 | } 90 | 91 | @override 92 | void addString(String value) { 93 | _sink.write(_separator); 94 | _writeString(_sink, value, _asciiOnly); 95 | _separator = ","; 96 | } 97 | 98 | @override 99 | void addSourceValue(String source) { 100 | _sink.write(_separator); 101 | _sink.write(source); 102 | _separator = ","; 103 | } 104 | } 105 | 106 | /// A [JsonSink] which builds a pretty textual representation of the JSON. 107 | /// 108 | /// The textual representation is spread on multiple lines and 109 | /// the content of JSON arrays or objects are indented. 110 | class JsonPrettyStringWriter implements JsonWriter { 111 | final StringSink _sink; 112 | final bool _asciiOnly; 113 | final String _indentString; 114 | int _indent = 0; 115 | String? _separator = ""; 116 | 117 | /// Creates a writer which writes the result into [target]. 118 | /// 119 | /// The [indentString] is used for indenting nested structures 120 | /// on new lines. If [indentString] is not pure whitespace, 121 | /// typically a single TAB character or a number of space characters, 122 | /// then the resulting text may not be valid JSON. 123 | /// 124 | /// If [asciiOnly] is true, string values will escape any non-ASCII 125 | /// character. If not, only control characters are escaped. 126 | JsonPrettyStringWriter(StringSink target, String indentString, 127 | {bool asciiOnly = false}) 128 | : _sink = target, 129 | _indentString = indentString, 130 | _asciiOnly = asciiOnly; 131 | 132 | void _writeSeparator() { 133 | if (_separator != null) { 134 | _sink.write(_separator); 135 | _writeIndent(); 136 | } 137 | } 138 | 139 | void _writeIndent() { 140 | _sink.write("\n"); 141 | for (var i = 0; i < _indent; i++) { 142 | _sink.write(_indentString); 143 | } 144 | } 145 | 146 | @override 147 | void addBool(bool value) { 148 | _writeSeparator(); 149 | _sink.write(value); 150 | _separator = ","; 151 | } 152 | 153 | @override 154 | void endArray() { 155 | _indent--; 156 | _writeIndent(); 157 | _sink.write("]"); 158 | _separator = ","; 159 | } 160 | 161 | @override 162 | void endObject() { 163 | _indent--; 164 | _writeIndent(); 165 | _sink.write("}"); 166 | _separator = ","; 167 | } 168 | 169 | @override 170 | void addKey(String key) { 171 | _writeSeparator(); 172 | _writeString(_sink, key, _asciiOnly); 173 | _sink.write(": "); 174 | _separator = null; 175 | } 176 | 177 | @override 178 | void addNull() { 179 | _writeSeparator(); 180 | _sink.write("null"); 181 | _separator = ","; 182 | } 183 | 184 | @override 185 | void addNumber(num value) { 186 | _writeSeparator(); 187 | _sink.write(value); 188 | _separator = ","; 189 | } 190 | 191 | @override 192 | void startArray() { 193 | _writeSeparator(); 194 | _sink.write("["); 195 | _indent++; 196 | _separator = ""; 197 | } 198 | 199 | @override 200 | void startObject() { 201 | _writeSeparator(); 202 | _sink.write("{"); 203 | _indent++; 204 | _separator = ""; 205 | } 206 | 207 | @override 208 | void addString(String value) { 209 | _writeSeparator(); 210 | _writeString(_sink, value, _asciiOnly); 211 | _separator = ","; 212 | } 213 | 214 | @override 215 | void addSourceValue(String source) { 216 | _writeSeparator(); 217 | _sink.write(source); 218 | _separator = ","; 219 | } 220 | } 221 | 222 | void _writeString(StringSink sink, String string, bool asciiOnly) { 223 | sink.write('"'); 224 | var start = 0; 225 | for (var i = 0; i < string.length; i++) { 226 | var char = string.codeUnitAt(i); 227 | if (char < 0x20 || 228 | char == 0x22 || 229 | char == 0x5c || 230 | (asciiOnly && char > 0x7f)) { 231 | if (i > start) sink.write(string.substring(start, i)); 232 | switch (char) { 233 | case 0x08: 234 | sink.write(r"\b"); 235 | break; 236 | case 0x09: 237 | sink.write(r"\t"); 238 | break; 239 | case 0x0a: 240 | sink.write(r"\n"); 241 | break; 242 | case 0x0c: 243 | sink.write(r"\f"); 244 | break; 245 | case 0x0d: 246 | sink.write(r"\r"); 247 | break; 248 | case 0x22: 249 | sink.write(r'\"'); 250 | break; 251 | case 0x5c: 252 | sink.write(r"\\"); 253 | break; 254 | default: 255 | sink.write(char < 256 256 | ? (char < 0x10 ? r"\u000" : r"\u00") 257 | : (char < 0x1000 ? r"\u0" : r"\u")); 258 | sink.write(char.toRadixString(16)); 259 | } 260 | start = i + 1; 261 | } 262 | } 263 | if (start < string.length) sink.write(string.substring(start)); 264 | sink.write('"'); 265 | } 266 | -------------------------------------------------------------------------------- /lib/src/json/json_structure_validator.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "../jsontool.dart"; 16 | 17 | /// A progressive JSON structure sink validator. 18 | /// 19 | /// Maintains a state representing a prefix of a valid JSON structure, 20 | /// with ways to query that state, and it allows only valid continuations 21 | /// to be added to the state. 22 | /// 23 | /// A valid JSON structure represents a single JSON value, either 24 | /// a primitive value or a compositve value. 25 | /// 26 | /// It must satisfy: 27 | /// * A [startArray] must be followed by a, potentially empty, sequence of 28 | /// valid JSON structures, and then an [endArray]. 29 | /// * A [startObject] must be followed by a, potentially empty, sequence of 30 | /// [addKey] and values pairs where the values are valid JSON structures, 31 | /// an then an [endObject]. 32 | /// 33 | /// The state will eventually represent a single JSON value, or 34 | /// a sequence of JSON values if the validator is *reusable*. 35 | /// 36 | /// The current state can be queried using 37 | /// [allowsValue], [allowsKey], [isArray], [isObject] and [hasValue]. 38 | final class JsonStructureValidator { 39 | // Whether a composite structure contains any values yet. 40 | static const int _hasValue = 1; 41 | // If set in state, removes _allowValue from the state when a value is provided. 42 | static const int _preventValueAfter = 2; 43 | // Whether a state allows a value. 44 | static const int _allowValue = 4; 45 | // Being inside a composite value. 46 | static const int _insideComposite = 8; 47 | 48 | /// State expecting a single top-level value. 49 | static const int _stateInitial = _allowValue | _preventValueAfter; 50 | 51 | /// State expecting multiple top-level values. 52 | static const int _stateInitialReusable = _allowValue; 53 | 54 | /// State expecting a value in an array, or ending the array. 55 | static const int _stateArray = _insideComposite | _allowValue; 56 | 57 | /// State expecting an object key or ending the object.. 58 | static const int _stateObjectKey = _insideComposite | _preventValueAfter; 59 | 60 | /// State expecting an object value, after seeing a key. 61 | static const int _stateObjectValue = 62 | _insideComposite | _allowValue | _preventValueAfter; 63 | 64 | /// Stack of states to return to when ending an array or object. 65 | final List _stateStack = []; 66 | 67 | /// Current state. 68 | int _state; 69 | 70 | /// Creates a validating [JsonSink]. 71 | /// 72 | /// Events sent to the sink must describe a valid JSON structure. 73 | JsonStructureValidator({bool allowReuse = false}) 74 | : _state = allowReuse ? _stateInitialReusable : _stateInitial; 75 | 76 | /// Add a JSON value. 77 | /// 78 | /// Throws if a value cannot occur at the current position. 79 | /// 80 | /// Since this sink does not care which value is being added, 81 | /// this method can be used to represent any primitive value, 82 | /// or even a composite value that needs not be checked. 83 | void value() { 84 | _value(); 85 | } 86 | 87 | void _value() { 88 | if (!allowsValue) { 89 | throw StateError("Cannot add value"); 90 | } 91 | _state = _state ^ ((_state & _preventValueAfter) * 2) | _hasValue; 92 | } 93 | 94 | /// Adds an object key. 95 | /// 96 | /// Throws if an object key cannot occur at the current position. 97 | void key() { 98 | if (!allowsKey) { 99 | throw StateError("Cannot add key"); 100 | } 101 | _state = _stateObjectValue; 102 | } 103 | 104 | /// Ends an array. 105 | /// 106 | /// Throws if not inside an array. 107 | void endArray() { 108 | if (!isArray) { 109 | throw StateError("Not inside array"); 110 | } 111 | _state = _stateStack.removeLast(); 112 | } 113 | 114 | /// Ends an object. 115 | /// 116 | /// Throws if not inside an object, or if positioned after an object key 117 | /// which has not received a value. 118 | void endObject() { 119 | if (!allowsKey) { 120 | throw StateError("Not at object end"); 121 | } 122 | _state = _stateStack.removeLast(); 123 | } 124 | 125 | /// Starts an array. 126 | /// 127 | /// Throws if a value cannot occur at the current position. 128 | /// 129 | /// When the array is closed, the value is considered added 130 | /// to the surrounding context. 131 | void startArray() { 132 | _value(); 133 | _stateStack.add(_state); 134 | _state = _stateArray; 135 | } 136 | 137 | /// Starts an object. 138 | /// 139 | /// Throws if a value cannot occur at the current position. 140 | /// 141 | /// When the object is closed, the value is considered added 142 | /// to the surrounding context. 143 | void startObject() { 144 | _value(); 145 | _stateStack.add(_state); 146 | _state = _stateObjectKey; 147 | } 148 | 149 | /// Checks whether a value is currently allowed. 150 | /// 151 | /// Throws if it is not, but does not change the state to assume that 152 | /// a value has actually been added. 153 | bool get allowsValue => _state & _allowValue != 0; 154 | 155 | /// Checks whether an object key is currently allowed. 156 | /// 157 | /// This is only true if inside an object ([isObject]) and 158 | /// not allowing a value. 159 | /// 160 | /// Throws if it is not, but does not change the state to assume that 161 | /// a key has actually been added. 162 | bool get allowsKey => 163 | (_state & (_insideComposite | _allowValue)) == _insideComposite; 164 | 165 | /// Whether the current position is immediately inside an array. 166 | bool get isArray => 167 | _state & (_insideComposite | _preventValueAfter) == _insideComposite; 168 | 169 | /// Whether the current position is immediately inside an object. 170 | bool get isObject => 171 | _state & (_insideComposite | _preventValueAfter) == 172 | (_insideComposite | _preventValueAfter); 173 | 174 | /// Whether the current position is inside any array. 175 | bool get insideArray { 176 | var state = _state; 177 | var i = _stateStack.length - 1; 178 | while (true) { 179 | if (state & (_insideComposite | _preventValueAfter) == _insideComposite) { 180 | return true; 181 | } 182 | if (i <= 0) return false; 183 | state = _stateStack[i]; 184 | i--; 185 | } 186 | } 187 | 188 | /// Whether the current position is inside any object. 189 | bool get insideObject { 190 | var state = _state; 191 | var i = _stateStack.length - 1; 192 | while (true) { 193 | if (state & (_insideComposite | _preventValueAfter) == 194 | (_insideComposite | _preventValueAfter)) { 195 | return true; 196 | } 197 | if (i <= 0) return false; 198 | state = _stateStack[i]; 199 | i--; 200 | } 201 | } 202 | 203 | /// Whether a value has been read. 204 | /// 205 | /// If [isArray] then whether a value has been added to that array. 206 | /// If [isObjet] then whether a key/value pair has been added to the object. 207 | /// If neither, then whether a value has been added. 208 | bool get hasValue => _state & _hasValue != 0; 209 | } 210 | -------------------------------------------------------------------------------- /lib/src/json/builder/builder.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "../reader/reader.dart"; 16 | 17 | /// A function reading JSON data from a [JsonReader] into a Dart value. 18 | /// 19 | /// This is a general type which can be implemented 20 | /// for any type that can be built from JSON data. 21 | typedef JsonBuilder = T Function(JsonReader); 22 | 23 | /// Parses a JSON value from [reader] into a plain JSON-like structure. 24 | /// 25 | /// A JSON-like structure is either a number, a string, a boolean, `null`, or 26 | /// a `List` containing JSON-like structures, or a `Map` 27 | /// where the values are JSON-like structures. 28 | Object? jsonValue(JsonReader reader) { 29 | if (reader.checkObject()) { 30 | return jsonObject(jsonValue)(reader); 31 | } 32 | if (reader.checkArray()) { 33 | return jsonArray(jsonValue)(reader); 34 | } 35 | if (reader.tryNull()) return null; 36 | return reader.tryNum() ?? 37 | reader.tryString() ?? 38 | reader.tryBool() ?? 39 | (throw FormatException("Reader has no value")); 40 | } 41 | 42 | /// Reads an integer from [reader]. 43 | int jsonInt(JsonReader reader) => reader.expectInt(); 44 | 45 | /// Reads a double from [reader]. 46 | double jsonDouble(JsonReader reader) => reader.expectDouble(); 47 | 48 | /// Reads a number from [reader]. 49 | num jsonNum(JsonReader reader) => reader.expectNum(); 50 | 51 | /// Reads a string from [reader]. 52 | String jsonString(JsonReader reader) => reader.expectString(); 53 | 54 | /// Reads a boolean from [reader]. 55 | bool jsonBool(JsonReader reader) => reader.expectBool(); 56 | 57 | /// Reads eiher a `null` or what [builder] reads from a [JsonReader]. 58 | /// 59 | /// If the next value of [builder] is `null`, then the result 60 | /// is `null`, otherwise returns the same value as [builder] 61 | /// on `reader`. 62 | /// 63 | /// Can be used as, for example: 64 | /// ```dart 65 | /// var optionalJsonInt = jsonOptional(jsonInt); 66 | /// var optionalInt = optionalJsonInt(reader); // int or null 67 | /// ``` 68 | JsonBuilder jsonOptional(JsonBuilder builder) => 69 | (JsonReader reader) => reader.tryNull() ? null : builder(reader); 70 | 71 | /// Reads an array of values from a [JsonReader]. 72 | /// 73 | /// Reads an array from the provided `reader`, where each 74 | /// element of the array is built by [elementBuilder]. 75 | JsonBuilder> jsonArray(JsonBuilder elementBuilder) => 76 | (JsonReader reader) { 77 | reader.expectArray(); 78 | var result = []; 79 | while (reader.hasNext()) { 80 | result.add(elementBuilder(reader)); 81 | } 82 | return result; 83 | }; 84 | 85 | /// Reads an array of values from [JsonReader]. 86 | /// 87 | /// The builder used for each element is provided 88 | /// by the [elementBuilder] function based on the 89 | /// index of the element. This allows, for example, 90 | /// building an array that alternates between two 91 | /// types. 92 | JsonBuilder> jsonIndexedArray( 93 | JsonBuilder Function(int index) elementBuilder) => 94 | (JsonReader reader) { 95 | reader.expectArray(); 96 | var result = []; 97 | var index = 0; 98 | while (reader.hasNext()) { 99 | result.add(elementBuilder(index)(reader)); 100 | index++; 101 | } 102 | return result; 103 | }; 104 | 105 | /// Builds a value from a JSON array. 106 | /// 107 | /// Builds a value for each element using [elementBuilder], 108 | /// then folds those into a single value using [initialValue] 109 | /// and [combine], just like [Iterable.fold]. 110 | JsonBuilder jsonFoldArray( 111 | JsonBuilder elementBuilder, 112 | T Function() initialValue, 113 | T Function(T previus, E elementValue) combine) => 114 | (JsonReader reader) { 115 | reader.expectArray(); 116 | var result = initialValue(); 117 | while (reader.hasNext()) { 118 | var element = elementBuilder(reader); 119 | result = combine(result, element); 120 | } 121 | return result; 122 | }; 123 | 124 | /// Builds a map from a JSON object. 125 | /// 126 | /// Reads a JSON object and builds a value for each object value 127 | /// using [valueBuilder]. Then creates a `Map` of 128 | /// the keys and built values. 129 | JsonBuilder> jsonObject(JsonBuilder valueBuilder) => 130 | (JsonReader reader) { 131 | reader.expectObject(); 132 | var result = {}; 133 | String? key; 134 | while ((key = reader.nextKey()) != null) { 135 | result[key] = valueBuilder(reader); 136 | } 137 | return result; 138 | }; 139 | 140 | /// Builds a map from a JSON object 141 | /// 142 | /// Reads a JSON object and builds a value for each object value 143 | /// using the builder returned by `valueBuilders[key]` for the corresponding 144 | /// object key. 145 | /// Then creates a `Map` of the keys and built values. 146 | /// 147 | /// The [defaultBuilder] is used if [valueBuilders] has no entry for 148 | /// a give key. If there is no [defaultBuilder] and not entry in 149 | /// [valueBuilders] for a key, then the entry is ignored. 150 | JsonBuilder> jsonStruct( 151 | Map> valueBuilders, 152 | [JsonBuilder? defaultBuilder]) => 153 | (JsonReader reader) { 154 | reader.expectObject(); 155 | var result = {}; 156 | var key = reader.nextKey(); 157 | while (key != null) { 158 | var builder = valueBuilders[key] ?? defaultBuilder; 159 | if (builder != null) { 160 | result[key] = builder(reader); 161 | } else { 162 | reader.skipAnyValue(); 163 | } 164 | key = reader.nextKey(); 165 | } 166 | return result; 167 | }; 168 | 169 | /// Reads a JSON object and combines its entries into a single value. 170 | /// 171 | /// Each entry value is built using [elementBuilder], 172 | /// then the keys and those values are combined using [combine]. 173 | /// 174 | /// Equivalent to 175 | /// ```dart 176 | /// jsonObject(elementBuilder).entries.fold(initialValue(), (value, entry) => 177 | /// combine(value, entry.key, entry.value)); 178 | /// ``` 179 | JsonBuilder jsonFoldObject( 180 | JsonBuilder elementBuilder, 181 | T Function() initialValue, 182 | T Function(T previus, String? key, E elementValue) combine) => 183 | (JsonReader reader) { 184 | reader.expectObject(); 185 | var result = initialValue(); 186 | String? key; 187 | while ((key = reader.nextKey()) != null) { 188 | var element = elementBuilder(reader); 189 | result = combine(result, key, element); 190 | } 191 | return result; 192 | }; 193 | 194 | /// Builds a [DateTime] from a JSON string read from [reader]. 195 | DateTime jsonDateString(JsonReader reader) => 196 | DateTime.parse(reader.expectString()); 197 | 198 | /// Builds a [Uri] from a JSON string read from [reader]. 199 | Uri jsonUriString(JsonReader reader) => Uri.parse(reader.expectString()); 200 | -------------------------------------------------------------------------------- /lib/src/json/sink/sink.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:convert'; 16 | 17 | import 'byte_writer.dart'; 18 | import "null_sink.dart"; 19 | import "object_writer.dart"; 20 | import "sink_validator.dart"; 21 | import "string_writer.dart"; 22 | 23 | /// A generalized JSON visitor interface. 24 | /// 25 | /// A JSON-like object structure (or just "JSON structure") 26 | /// is a recursive structure of either atomic values: 27 | /// 28 | /// * number 29 | /// * string 30 | /// * boolean 31 | /// * null 32 | /// 33 | /// or composte structures which are either 34 | /// an *array* of one or more JSON structures, 35 | /// or an *object* with pairs of string keys and 36 | /// JSON structure values. 37 | /// 38 | /// A [JsonSink] expects its members to be called in sequences 39 | /// corresponding to the JSON structure of a single value: 40 | /// Either a primitive value, [addNumber], [addString], [addBool] or [addNull], 41 | /// or a [startArray] followed by JSON values and ended by an [endArray], 42 | /// or a [startObject] followed by alternating [addKey] and values, and ended 43 | /// with an [endObject]. 44 | /// 45 | /// In general, a [JsonSink] is not required or expected to 46 | /// work correctly if calls are performed out of order. 47 | /// Only call sequences corresponding to a correct JSON structure 48 | /// are guaranteed to give a meaningful result. 49 | abstract interface class JsonSink { 50 | /// Called for a number value. 51 | void addNumber(num value); 52 | 53 | /// Called for a null value. 54 | void addNull(); 55 | 56 | /// Called for a string value. 57 | void addString(String value); 58 | 59 | /// Called for a boolean value. 60 | void addBool(bool value); 61 | 62 | /// Called at the beginning of an array value. 63 | /// 64 | /// Each value added until a corresponding [endArray] 65 | /// is considered an element of this array, unless it's part of a nested 66 | /// array or object. 67 | void startArray(); 68 | 69 | /// Ends the current array. 70 | /// 71 | /// The array value is now complete, and should 72 | /// be treated as a value of a surrounding array or object. 73 | void endArray(); 74 | 75 | /// Called at the beginning of an object value. 76 | /// 77 | /// Each value added until a corresponding [endObject] 78 | /// is considered an entry value of this object, unless it's part of a nested 79 | /// array or object. 80 | /// Each such added value must be preceded by exactly one call to [addKey] 81 | /// which provides the corresponding key. 82 | void startObject(); 83 | 84 | /// Sets the key for the next value of an object. 85 | /// 86 | /// Should precede any value or array/object start inside an object. 87 | void addKey(String key); 88 | 89 | /// Ends the current object. 90 | /// 91 | /// The object value is now complete, and should 92 | /// be treated as a value of a surrounding array or object. 93 | void endObject(); 94 | } 95 | 96 | /// A JSON sink which emits JSON source or JSON-like structures. 97 | /// 98 | /// The [addSourceValue] method injects the representation of a value directly 99 | /// as the target type [T], e.g., `String` or `List` depending 100 | /// no what is being written to. Can be used with the value of 101 | /// [JsonReader.expectAnyValueSource] to avoid parsing a value. 102 | abstract class JsonWriter implements JsonSink { 103 | /// Adds a JSON value *as source* to the sink. 104 | /// 105 | /// Can be used where any of the `add` methods, like [addNumber], 106 | /// would add a value. 107 | /// The [source] becomes the next value. If the source object 108 | /// does not have a correct format for a JSON value, 109 | /// the result might be invalid, or require using 110 | /// [JsonReader.expectAnyValueSource] to read back. 111 | void addSourceValue(T source); 112 | } 113 | 114 | /// Creates a [JsonSink] which builds a JSON string. 115 | /// 116 | /// The string is written to [sink]. 117 | /// 118 | /// If [indent] is supplied, the resulting string will be "pretty printed" 119 | /// with array and object entries on lines of their own 120 | /// and indented by multiples of the [indent] string. 121 | /// If the [indent] string is not valid JSON whitespace, 122 | /// the result written to [sink] will not be valid JSON source. 123 | /// 124 | /// If [asciiOnly] is set to `true`, string values will have all non-ASCII 125 | /// characters escaped. If not, only control characters, quotes and backslashes 126 | /// are escaped. 127 | /// 128 | /// The returned sink is not reusable. After it has written a single JSON 129 | /// structure, it should not be used again. 130 | JsonWriter jsonStringWriter(StringSink sink, 131 | {String? indent, bool asciiOnly = false}) { 132 | if (indent == null) return JsonStringWriter(sink, asciiOnly: asciiOnly); 133 | return JsonPrettyStringWriter(sink, indent, asciiOnly: asciiOnly); 134 | } 135 | 136 | /// Creates a [JsonSink] which builds a byte representation of the JSON 137 | /// structure. 138 | /// 139 | /// The bytes are written to [sink], which is closed when a complete JSON 140 | /// value / object structure has been written. 141 | /// 142 | /// If [asciiOnly] is true, string values will escape any non-ASCII 143 | /// character. If false or unspecified and [encoding] is [utf8], only 144 | /// control characters are escaped. 145 | /// 146 | /// The resulting byte representation is a minimal JSON text with no 147 | /// whitespace between tokens. 148 | JsonWriter> jsonByteWriter(Sink> sink, 149 | {Encoding encoding = utf8, bool? asciiOnly}) { 150 | return JsonByteWriter(sink, encoding: encoding, asciiOnly: false); 151 | } 152 | 153 | /// Creates a [JsonSink] which builds a Dart JSON object structure. 154 | /// 155 | /// After adding values corresponding to a JSON structure to the sink, 156 | /// the [result] callback is called with the resulting object structure. 157 | /// 158 | /// When [result] is called, the returned sink is reset and can be reused. 159 | JsonWriter jsonObjectWriter(void Function(Object?) result) => 160 | JsonObjectWriter(result); 161 | 162 | /// Wraps a [JsonSink] in a validating layer. 163 | /// 164 | /// A [JsonSink] is an API which requires methods to be called in a specific 165 | /// order, but implementations are allowed to not check this. 166 | /// Calling methods in an incorrect order may throw, 167 | /// or it may return spurious values. 168 | /// 169 | /// The returned sink wraps [sink] and intercepts all method calls. 170 | /// It throws immediately if the calls are not in a proper order. 171 | /// 172 | /// This function is mainly intended for testing code which writes to 173 | /// sinks, to ensure that they make calls in the 174 | /// 175 | /// If [allowReuse] is set to true, the sink is assumed to be *reusable*, 176 | /// meaning that after completely writing a JSON structure, it resets 177 | /// and accepts following JSON structures. If not, then no method 178 | /// may be called after completing a single JSON structure. 179 | JsonSink validateJsonSink(JsonSink sink, {bool allowReuse = false}) { 180 | return ValidatingJsonSink(sink, allowReuse); 181 | } 182 | 183 | /// A [JsonSink] which accepts any calls and ignores them. 184 | /// 185 | /// Can be used if a sink is needed, but the result of the sink 186 | /// operations is not important. 187 | const JsonSink nullJsonSink = NullJsonSink(); 188 | 189 | /// Interface which classes that write themselves to [JsonSink]s can implement. 190 | abstract interface class JsonWritable { 191 | /// Writes a JSON representation of this object to [target]. 192 | void writeJson(JsonSink target); 193 | } 194 | 195 | extension JsonWritableAddWritable on JsonSink { 196 | /// Writes [writable] to this sink. 197 | /// 198 | /// Convenience function to allow doing `writable.writeJson(sink)` 199 | /// inside a sequence cascade calls on the sink. 200 | void addWritable(JsonWritable writable) { 201 | writable.writeJson(this); 202 | } 203 | } 204 | 205 | /// Convenience functions for adding object properties to a [JsonSink]. 206 | /// 207 | /// For hand-written code that adds values to a [JsonSink]. 208 | extension JsonSinkAddEntry on JsonSink { 209 | /// Adds a key and string value entry to the current JSON object. 210 | /// 211 | /// Same as calling [JsonSink.addKey] followed by [JsonSink.addString]. 212 | void addStringEntry(String key, String value) { 213 | this 214 | ..addKey(key) 215 | ..addString(value); 216 | } 217 | 218 | /// Adds a key and boolean value entry to the current JSON object. 219 | /// 220 | /// Same as calling [JsonSink.addKey] followed by [JsonSink.addBool]. 221 | void addBoolEntry(String key, bool value) { 222 | this 223 | ..addKey(key) 224 | ..addBool(value); 225 | } 226 | 227 | /// Adds a key and number value entry to the current JSON object. 228 | /// 229 | /// Same as calling [JsonSink.addKey] followed by [JsonSink.addNumber]. 230 | void addNumberEntry(String key, num value) { 231 | this 232 | ..addKey(key) 233 | ..addNumber(value); 234 | } 235 | 236 | /// Adds a key and writable value entry to the current JSON object. 237 | /// 238 | /// Same as calling [JsonSink.addKey] followed by 239 | /// [JsonWritableAddWritable.addWritable]. 240 | void addWritableEntry(String key, JsonWritable value) { 241 | this.addKey(key); 242 | value.writeJson(this); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /lib/src/json/sink/byte_writer.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // Copyright 2020 Google LLC 15 | // 16 | // Licensed under the Apache License, Version 2.0 (the "License"); 17 | // you may not use this file except in compliance with the License. 18 | // You may obtain a copy of the License at 19 | // 20 | // https://www.apache.org/licenses/LICENSE-2.0 21 | // 22 | // Unless required by applicable law or agreed to in writing, software 23 | // distributed under the License is distributed on an "AS IS" BASIS, 24 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | // See the License for the specific language governing permissions and 26 | // limitations under the License. 27 | 28 | import 'dart:convert'; 29 | 30 | import "sink.dart"; 31 | 32 | /// A [JsonSink] which builds a binary representation of the JSON 33 | /// structure. 34 | /// 35 | /// The resulting string representation is a minimal JSON text with no 36 | /// whitespace between tokens. 37 | final class JsonByteWriter implements JsonWriter> { 38 | final Encoding _encoding; 39 | 40 | /// String characters with value above this are hex-encoded, 41 | /// below they are emitted as-is. 42 | final int _encodeLimit; 43 | final _NonClosingIntListSinkBase _target; 44 | StringConversionSink? _sink; 45 | String _separator = ""; 46 | int _depth = 0; 47 | 48 | /// Creates a [JsonSink] which builds a byte representation of the JSON 49 | /// structure. 50 | /// 51 | /// The bytes are written to [sink], which is closed when a complete JSON 52 | /// value / object structure has been written. 53 | /// 54 | /// If [asciiOnly] is true, string values will escape any non-ASCII 55 | /// character. 56 | /// If false, which characters are converted is determined by the encoding. 57 | /// The recognized encodings are [ascii], [latin1] and [utf8]. 58 | /// All other encodings will currently make all non-ASCII characters be 59 | /// encoded. 60 | /// 61 | /// The resulting byte representation is a minimal JSON text with no 62 | /// whitespace between tokens. 63 | JsonByteWriter( 64 | Sink> target, { 65 | Encoding encoding = utf8, 66 | bool asciiOnly = false, 67 | }) : _encoding = encoding, 68 | _target = target is ByteConversionSink 69 | ? _NonClosingByteConversionSink(target) 70 | : _NonClosingIntListSink(target), 71 | _encodeLimit = _estimateEncodingLimit(asciiOnly, encoding); 72 | 73 | static int _estimateEncodingLimit(bool asciiOnly, Encoding encoding) { 74 | if (!asciiOnly) { 75 | if (identical(encoding, utf8)) return 0x10FFFF; 76 | if (identical(encoding, latin1)) return 0xFF; 77 | } 78 | return 0x7F; 79 | } 80 | 81 | /// Ensures that [_sink] contains an encoding sink wrapping [_target]]. 82 | /// 83 | /// We wrap [_target] in wrapper which swallows close operations. 84 | /// This allows us to close the [_sink], which flushes any buffered 85 | /// data in the encoding sink. 86 | StringConversionSink _ensureSink() => 87 | _sink ??= _encoding.encoder.startChunkedStringConversion(_target); 88 | 89 | void _closeAtEnd() { 90 | if (_depth == 0) { 91 | _sink?.close(); 92 | // Because `_sink` is encoding to a `_NonClosingSink`, also close 93 | // `_target` manually. 94 | _target._close(); 95 | } 96 | } 97 | 98 | @override 99 | void addBool(bool value) { 100 | var sink = _ensureSink(); 101 | sink.add(_separator); 102 | if (value) { 103 | sink.add("true"); 104 | } else { 105 | sink.add("false"); 106 | } 107 | _separator = ","; 108 | _closeAtEnd(); 109 | } 110 | 111 | @override 112 | void endArray() { 113 | _ensureSink().add("]"); 114 | _separator = ","; 115 | _depth--; 116 | _closeAtEnd(); 117 | } 118 | 119 | @override 120 | void endObject() { 121 | _ensureSink().add("}"); 122 | _separator = ","; 123 | _depth--; 124 | _closeAtEnd(); 125 | } 126 | 127 | @override 128 | void addKey(String key) { 129 | var sink = _ensureSink(); 130 | sink.add(_separator); 131 | _writeString(sink, key, _encodeLimit); 132 | _separator = ":"; 133 | } 134 | 135 | @override 136 | void addNull() { 137 | _ensureSink() 138 | ..add(_separator) 139 | ..add("null"); 140 | _separator = ","; 141 | _closeAtEnd(); 142 | } 143 | 144 | @override 145 | void addNumber(num value) { 146 | _ensureSink() 147 | ..add(_separator) 148 | ..add(value.toString()); 149 | _separator = ","; 150 | _closeAtEnd(); 151 | } 152 | 153 | @override 154 | void startArray() { 155 | _ensureSink() 156 | ..add(_separator) 157 | ..add("["); 158 | _separator = ""; 159 | _depth++; 160 | } 161 | 162 | @override 163 | void startObject() { 164 | _ensureSink() 165 | ..add(_separator) 166 | ..add("{"); 167 | _separator = ""; 168 | _depth++; 169 | } 170 | 171 | @override 172 | void addString(String value) { 173 | var sink = _ensureSink(); 174 | sink.add(_separator); 175 | _writeString(sink, value, _encodeLimit); 176 | _separator = ","; 177 | _closeAtEnd(); 178 | } 179 | 180 | @override 181 | void addSourceValue(List source) { 182 | // The `_target` sink is wrapped in `_NonClosingSink` in the constructor, 183 | // and here, because we close the `_sink` as a way to flush previous 184 | // encoding content before adding these bytes. 185 | // Then following [add]s will allocate a new encoding sink when necessary. 186 | var sink = _sink; 187 | if (sink != null) { 188 | // Write to and flush current encoding. 189 | sink 190 | ..add(_separator) 191 | ..close(); 192 | _sink = null; 193 | } else if (_separator.isNotEmpty) { 194 | // No current encoding sink, so nothing to flush. 195 | // Just encode the separator manually, and add it to the target sink. 196 | _target.add(_encoding.encode(_separator)); 197 | } 198 | _target.add(source); 199 | _separator = ","; 200 | _closeAtEnd(); 201 | } 202 | } 203 | 204 | /// Writes [string] as a JSON string value to [sink]. 205 | /// 206 | /// String characters above [encodeLimit] are written as `\u....` escapes. 207 | /// The [encodeLimit] is on of 0x7F (ASCII only), 0xFF (Latin-1) and 208 | /// 0x10FFFF (UTF-8). 209 | /// This ensures that the emitted characters are valid for the encoding. 210 | /// (Unrecognized encodings emit only ASCII.) 211 | void _writeString(StringConversionSink sink, String string, int encodeLimit) { 212 | if (string.isEmpty) { 213 | sink.add('""'); 214 | return; 215 | } 216 | sink.add('"'); 217 | var start = 0; 218 | for (var i = 0; i < string.length; i++) { 219 | var char = string.codeUnitAt(i); 220 | // 0x22 is `"`, 0x5c is `\`. 221 | if (char < 0x20 || char == 0x22 || char == 0x5c || char > encodeLimit) { 222 | if (i > start) sink.addSlice(string, start, i, false); 223 | switch (char) { 224 | case 0x08: 225 | sink.add(r"\b"); 226 | break; 227 | case 0x09: 228 | sink.add(r"\t"); 229 | break; 230 | case 0x0a: 231 | sink.add(r"\n"); 232 | break; 233 | case 0x0c: 234 | sink.add(r"\f"); 235 | break; 236 | case 0x0d: 237 | sink.add(r"\r"); 238 | break; 239 | case 0x22: 240 | sink.add(r'\"'); 241 | break; 242 | case 0x5c: 243 | sink.add(r"\\"); 244 | break; 245 | default: 246 | sink.add(char < 0x100 247 | ? (char < 0x10 ? r"\u000" : r"\u00") 248 | : (char < 0x1000 ? r"\u0" : r"\u")); 249 | sink.add(char.toRadixString(16)); 250 | } 251 | start = i + 1; 252 | } 253 | } 254 | if (start < string.length) sink.addSlice(string, start, string.length, false); 255 | sink.add('"'); 256 | } 257 | 258 | abstract class _NonClosingIntListSinkBase extends ByteConversionSink { 259 | abstract final Sink> _sink; 260 | 261 | @override 262 | void add(List data) => _sink.add(data); 263 | 264 | @override 265 | void close() { 266 | // do nothing 267 | } 268 | 269 | // Actually close. 270 | void _close() { 271 | _sink.close(); 272 | } 273 | } 274 | 275 | /// Wrap a [Sink] such that [close] will not close the underlying sink. 276 | class _NonClosingIntListSink extends _NonClosingIntListSinkBase { 277 | @override 278 | final Sink> _sink; 279 | 280 | _NonClosingIntListSink(this._sink); 281 | } 282 | 283 | class _NonClosingByteConversionSink extends _NonClosingIntListSinkBase { 284 | @override 285 | final ByteConversionSink _sink; 286 | 287 | _NonClosingByteConversionSink(this._sink); 288 | 289 | @override 290 | void addSlice(List chunk, int start, int end, bool isLast) { 291 | _sink.addSlice(chunk, start, end, false); 292 | } 293 | } 294 | 295 | extension on Converter> { 296 | /// Starts a chunked conversion. 297 | /// 298 | /// This calls [startChunkedConversion] and wraps in a [StringConversionSink] 299 | /// if necessary. Most implementations of [Encoding.encoder] has a 300 | /// specialization that returns [StringConversionSink]. 301 | StringConversionSink startChunkedStringConversion(Sink> sink) { 302 | final stringSink = startChunkedConversion(sink); 303 | if (stringSink is StringConversionSink) { 304 | return stringSink; 305 | } 306 | return StringConversionSink.from(stringSink); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /lib/src/json/reader/reader_validator.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "../json_structure_validator.dart"; 16 | import "../sink/sink.dart"; 17 | import "reader.dart"; 18 | import "util.dart"; 19 | 20 | /// A validating JSON reader which checks the member invocation sequence. 21 | /// 22 | /// The members must only be used in situations where the operation is. 23 | final class ValidatingJsonReader 24 | implements JsonReader { 25 | final JsonStructureValidator _validator = JsonStructureValidator(); 26 | final JsonReader _reader; 27 | // If in an array, whether `hasNext` has been called. 28 | bool _needsHasNext = false; 29 | 30 | ValidatingJsonReader(JsonReader reader) : _reader = reader; 31 | 32 | void _checkValueAllowed() { 33 | if (_needsHasNext || !_validator.allowsValue) { 34 | throw StateError("Value not allowed: $_needsHasNext"); 35 | } 36 | } 37 | 38 | void _checkKeyAllowed() { 39 | if (!_validator.allowsKey) { 40 | throw StateError("Key not allowed"); 41 | } 42 | } 43 | 44 | @override 45 | FormatException fail(String message) => _reader.fail(message); 46 | 47 | @override 48 | bool checkArray() { 49 | _checkValueAllowed(); 50 | return _reader.checkArray(); 51 | } 52 | 53 | @override 54 | bool checkBool() { 55 | _checkValueAllowed(); 56 | return _reader.checkBool(); 57 | } 58 | 59 | @override 60 | bool checkNull() { 61 | _checkValueAllowed(); 62 | return _reader.checkNull(); 63 | } 64 | 65 | @override 66 | bool checkNum() { 67 | _checkValueAllowed(); 68 | return _reader.checkNum(); 69 | } 70 | 71 | @override 72 | bool checkObject() { 73 | _checkValueAllowed(); 74 | return _reader.checkObject(); 75 | } 76 | 77 | @override 78 | bool checkString() { 79 | _checkValueAllowed(); 80 | return _reader.checkString(); 81 | } 82 | 83 | @override 84 | JsonReader copy() { 85 | return _reader.copy(); 86 | } 87 | 88 | @override 89 | Null expectAnyValue(JsonSink sink) { 90 | _checkValueAllowed(); 91 | _reader.expectAnyValue(sink); 92 | _validator.value(); 93 | _needsHasNext = _validator.isArray; 94 | } 95 | 96 | @override 97 | SourceSlice expectAnyValueSource() { 98 | _checkValueAllowed(); 99 | var result = _reader.expectAnyValueSource(); 100 | _validator.value(); 101 | _needsHasNext = _validator.isArray; 102 | return result; 103 | } 104 | 105 | @override 106 | void expectArray() { 107 | _checkValueAllowed(); 108 | _reader.expectArray(); 109 | _validator.startArray(); 110 | _needsHasNext = true; 111 | } 112 | 113 | @override 114 | bool expectBool() { 115 | _checkValueAllowed(); 116 | var result = _reader.expectBool(); 117 | _validator.value(); 118 | _needsHasNext = _validator.isArray; 119 | return result; 120 | } 121 | 122 | @override 123 | double expectDouble() { 124 | _checkValueAllowed(); 125 | var result = _reader.expectDouble(); 126 | _validator.value(); 127 | _needsHasNext = _validator.isArray; 128 | return result; 129 | } 130 | 131 | @override 132 | int expectInt() { 133 | _checkValueAllowed(); 134 | var result = _reader.expectInt(); 135 | _validator.value(); 136 | _needsHasNext = _validator.isArray; 137 | return result; 138 | } 139 | 140 | @override 141 | Null expectNull() { 142 | _checkValueAllowed(); 143 | _reader.expectNull(); 144 | _validator.value(); 145 | _needsHasNext = _validator.isArray; 146 | } 147 | 148 | @override 149 | num expectNum() { 150 | _checkValueAllowed(); 151 | var result = _reader.expectNum(); 152 | _validator.value(); 153 | _needsHasNext = _validator.isArray; 154 | return result; 155 | } 156 | 157 | @override 158 | void expectObject() { 159 | _checkValueAllowed(); 160 | _reader.expectObject(); 161 | _validator.startObject(); 162 | } 163 | 164 | @override 165 | String expectString([List? candidates]) { 166 | _checkValueAllowed(); 167 | var result = _reader.expectString(candidates); 168 | _validator.value(); 169 | _needsHasNext = _validator.isArray; 170 | return result; 171 | } 172 | 173 | @override 174 | int expectStringIndex(List candidates) { 175 | _checkValueAllowed(); 176 | var result = _reader.expectStringIndex(candidates); 177 | _validator.value(); 178 | _needsHasNext = _validator.isArray; 179 | return result; 180 | } 181 | 182 | @override 183 | bool hasNext() { 184 | if (!_needsHasNext) { 185 | throw StateError("Cannot use hasNext"); 186 | } 187 | _needsHasNext = false; 188 | if (_reader.hasNext()) { 189 | return true; 190 | } 191 | _validator.endArray(); 192 | return false; 193 | } 194 | 195 | @override 196 | String? nextKey() { 197 | if (!_validator.allowsKey) { 198 | throw StateError("Does not allow key"); 199 | } 200 | var result = _reader.nextKey(); 201 | if (result == null) { 202 | _validator.endObject(); 203 | _needsHasNext = _validator.isArray; 204 | } else { 205 | _validator.key(); 206 | } 207 | return result; 208 | } 209 | 210 | @override 211 | bool hasNextKey() { 212 | if (!_validator.allowsKey) { 213 | throw StateError("Does not allow key"); 214 | } 215 | var result = _reader.hasNextKey(); 216 | if (!result) { 217 | _validator.endObject(); 218 | _needsHasNext = _validator.isArray; 219 | } 220 | return result; 221 | } 222 | 223 | @override 224 | SourceSlice? nextKeySource() { 225 | if (!_validator.allowsKey) { 226 | throw StateError("Does not allow key"); 227 | } 228 | var result = _reader.nextKeySource(); 229 | if (result == null) { 230 | _validator.endObject(); 231 | } else { 232 | _validator.key(); 233 | } 234 | _needsHasNext = _validator.isArray; 235 | return result; 236 | } 237 | 238 | @override 239 | Null skipAnyValue() { 240 | _validator.value(); 241 | _reader.skipAnyValue(); 242 | _needsHasNext = _validator.isArray; 243 | } 244 | 245 | @override 246 | void endArray() { 247 | if (!_validator.insideArray) { 248 | throw StateError("Not in array"); 249 | } 250 | _reader.endArray(); 251 | while (_validator.isObject) { 252 | if (_validator.allowsValue) { 253 | _validator.value(); 254 | } 255 | _validator.endObject(); 256 | } 257 | assert(_validator.isArray); 258 | _validator.endArray(); 259 | _needsHasNext = _validator.isArray; 260 | } 261 | 262 | @override 263 | void endObject() { 264 | if (!_validator.insideObject) { 265 | throw StateError("Not in object"); 266 | } 267 | _reader.endObject(); 268 | while (_validator.isArray) { 269 | _validator.endArray(); 270 | } 271 | assert(_validator.isObject); 272 | if (_validator.allowsValue) { 273 | // After key. 274 | _validator.value(); 275 | } 276 | _validator.endObject(); 277 | _needsHasNext = _validator.isArray; 278 | } 279 | 280 | @override 281 | bool skipObjectEntry() { 282 | _checkKeyAllowed(); 283 | if (!_reader.skipObjectEntry()) { 284 | _validator.endObject(); 285 | return false; 286 | } 287 | return true; 288 | } 289 | 290 | @override 291 | bool tryArray() { 292 | _checkValueAllowed(); 293 | if (_reader.tryArray()) { 294 | _validator.startArray(); 295 | _needsHasNext = true; 296 | return true; 297 | } 298 | return false; 299 | } 300 | 301 | @override 302 | bool? tryBool() { 303 | _checkValueAllowed(); 304 | var result = _reader.tryBool(); 305 | if (result != null) { 306 | _validator.value(); 307 | _needsHasNext = _validator.isArray; 308 | } 309 | return result; 310 | } 311 | 312 | @override 313 | double? tryDouble() { 314 | _checkValueAllowed(); 315 | var result = _reader.tryDouble(); 316 | if (result != null) { 317 | _validator.value(); 318 | _needsHasNext = _validator.isArray; 319 | } 320 | return result; 321 | } 322 | 323 | @override 324 | int? tryInt() { 325 | _checkValueAllowed(); 326 | var result = _reader.tryInt(); 327 | if (result != null) { 328 | _validator.value(); 329 | _needsHasNext = _validator.isArray; 330 | } 331 | return result; 332 | } 333 | 334 | @override 335 | String? tryKey(List candidates) { 336 | if (!areSorted(candidates)) { 337 | throw ArgumentError("Candidates are not sorted"); 338 | } 339 | _checkKeyAllowed(); 340 | var result = _reader.tryKey(candidates); 341 | if (result != null) { 342 | _validator.key(); 343 | _needsHasNext = _validator.isArray; 344 | } 345 | return result; 346 | } 347 | 348 | @override 349 | int? tryKeyIndex(List candidates) { 350 | if (!areSorted(candidates)) { 351 | throw ArgumentError("Candidates are not sorted"); 352 | } 353 | _checkKeyAllowed(); 354 | var result = _reader.tryKeyIndex(candidates); 355 | if (result != null) { 356 | _validator.key(); 357 | _needsHasNext = _validator.isArray; 358 | } 359 | return result; 360 | } 361 | 362 | @override 363 | bool tryNull() { 364 | _checkValueAllowed(); 365 | if (_reader.tryNull()) { 366 | _validator.value(); 367 | _needsHasNext = _validator.isArray; 368 | return true; 369 | } 370 | return false; 371 | } 372 | 373 | @override 374 | num? tryNum() { 375 | _checkValueAllowed(); 376 | var result = _reader.tryNum(); 377 | if (result != null) { 378 | _validator.value(); 379 | _needsHasNext = _validator.isArray; 380 | } 381 | return result; 382 | } 383 | 384 | @override 385 | bool tryObject() { 386 | _checkValueAllowed(); 387 | var result = _reader.tryObject(); 388 | if (result) { 389 | _validator.startObject(); 390 | } 391 | return result; 392 | } 393 | 394 | @override 395 | String? tryString([List? candidates]) { 396 | _checkValueAllowed(); 397 | var result = _reader.tryString(candidates); 398 | if (result != null) { 399 | _validator.value(); 400 | _needsHasNext = _validator.isArray; 401 | } 402 | return result; 403 | } 404 | 405 | @override 406 | int? tryStringIndex(List candidates) { 407 | _checkValueAllowed(); 408 | var result = _reader.tryStringIndex(candidates); 409 | if (result != null) { 410 | _validator.value(); 411 | _needsHasNext = _validator.isArray; 412 | } 413 | return result; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /lib/src/json/processor/processor.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // A generalized processor for JSON source which takes elements from a 16 | // reader, then leaves the processing to user overridable methods. 17 | 18 | import "../reader/reader.dart"; 19 | import "../sink/sink.dart"; 20 | 21 | /// A generalized JSON processor. 22 | /// 23 | /// The processor traverses JSON-like data as provided by a [JsonReader]. 24 | /// It dispatches to individual `process` methods for each 25 | /// kind of JSON value, but doesn't process the value 26 | /// by default. 27 | /// These methods can be overridden in subclasses to do something useful. 28 | /// For example, the [JsonSinkProcessor] defaults to forwarding 29 | /// each kind of JSON data to a [JsonSink]. 30 | /// 31 | /// The processor is similar to a [JsonSink] 32 | /// in that there are methods for each kind of JSON data, 33 | /// but instead of passing the JSON values to the individual `add` methods, 34 | /// the `process` methods of the processsor takes a reader 35 | /// which is ready to provide the value. 36 | /// 37 | /// The process-method can either read the value, skip it, 38 | /// or even read the source using [JsonReader.expectAnyValueSource] 39 | /// and handle it itself. 40 | /// 41 | /// Each process-method takes a `key` which is non-null when the 42 | /// value is a JSON-object value. This allows the processor 43 | /// to easily skip entire entries, perhaps even based on the 44 | /// key name. 45 | /// 46 | /// An example implementation of [processNum] could be: 47 | /// ```dart 48 | /// void processNum(JsonReader reader, String? key) { 49 | /// if (key != null && key.startsWith("x-")) { 50 | /// // Ignore key and value. 51 | /// reader.skipAnyValue(); 52 | /// } else { 53 | /// // Add key and value to a JsonSink. 54 | /// if (key != null) sink.addKey(key); 55 | /// sink.addNumber(reader.expectNum()); 56 | /// } 57 | /// } 58 | /// ``` 59 | abstract base class JsonProcessor { 60 | /// Process a JSON-value. 61 | /// 62 | /// Dispatches to one of [processNull], [processNum], 63 | /// [processString], [processBool], [processArray] 64 | /// or [processObject] depending on what the next 65 | /// value of the reader is. 66 | /// If there is no value, or the reader has values 67 | /// which do not match a JSON type, the [processUnknown] 68 | /// method is called instead. This allows handling of 69 | /// unknown values in readers supporting that. 70 | void processValue(Reader reader, [String? key]) { 71 | if (reader.checkArray()) { 72 | if (processArray(reader, key)) { 73 | while (reader.hasNext()) { 74 | processValue(reader); 75 | } 76 | endArray(key); 77 | } 78 | } else if (reader.checkObject()) { 79 | if (processObject(reader, key)) { 80 | while (reader.hasNextKey()) { 81 | processObjectEntry(reader); 82 | } 83 | endObject(key); 84 | } 85 | } else if (reader.checkString()) { 86 | processString(reader, key); 87 | } else if (reader.checkNum()) { 88 | processNum(reader, key); 89 | } else if (reader.checkBool()) { 90 | processBool(reader, key); 91 | } else if (reader.checkNull()) { 92 | processNull(reader, key); 93 | } else { 94 | processUnknown(reader, key); 95 | } 96 | } 97 | 98 | /// Invoked for a reader which has no value or an unrecognized value. 99 | /// 100 | /// A reader for malformed input may have no value where one is expected, 101 | /// and a reader of an object structure may have a value which isn't 102 | /// one of the JSON values. This method is called in those cases. 103 | /// 104 | /// If [key] is non-null, it's the key of the value in the current object. 105 | /// The key is always `null` outside of an object. 106 | /// 107 | /// Defaults to throwing. 108 | void processUnknown(Reader reader, String? key) { 109 | throw StateError("No value"); 110 | } 111 | 112 | /// Called when the reader encounters the start of a JSON object. 113 | /// 114 | /// Returns whether to continue processing the contents of 115 | /// the object. 116 | /// 117 | /// This call should either consume the object start 118 | /// (using `reader.expectObject()`) and return `true`, 119 | /// or consume the entire object value (e.g., using `reader.skipAnyValue()`) 120 | /// and return `false`. 121 | /// The default implementation does the former. 122 | /// 123 | /// If returning `true`, the processor will continue to 124 | /// process pairs of keys and values, and finish by calling 125 | /// [endObject] when there are no further entries. 126 | /// 127 | /// If [key] is non-null, it's the key of the value in the current object. 128 | /// The key is always `null` outside of an object. 129 | bool processObject(Reader reader, String? key) { 130 | reader.expectObject(); 131 | return true; 132 | } 133 | 134 | /// Called for each JSON object entry. 135 | /// 136 | /// Is called before each object entry key is processed. 137 | /// The call must process the key and value. 138 | /// The default behavior is to read the next key, 139 | /// then call [processValue] with the reader and that key. 140 | /// 141 | /// A subclass can override this method to skip the entire entry using 142 | /// [Reader.skipObjectEntry], before or after reading the key. 143 | void processObjectEntry(Reader reader) { 144 | processValue(reader, reader.nextKey()); 145 | } 146 | 147 | /// Called after all key/value pairs of the current object are processed. 148 | /// 149 | /// If [key] is non-null, it's the key of the completed value 150 | /// in the current object. This is the same key as passed to the corresponding 151 | /// [processObject] call. 152 | /// The key is always `null` outside of an object. 153 | void endObject(String? key) {} 154 | 155 | /// Called when the reader encounters a JSON array. 156 | /// 157 | /// Returns whether to continue processing the contents of 158 | /// the array. 159 | /// 160 | /// This call should either consume the array start 161 | /// (using `reader.expectArray()`) and return `true`, 162 | /// or consume the entire array (e.g., using `reader.skipAnyValue()`) 163 | /// and return `false`. 164 | /// The default implementation does the former. 165 | /// 166 | /// If returning `true`, the processor will continue to 167 | /// process individualk values of the array using 168 | /// the typed `process`-functions, and finish with 169 | /// [endArray] when there are no further values. 170 | /// 171 | /// If [key] is non-null, it's the key of the value in the current object. 172 | /// The key is always `null` outside of an object. 173 | bool processArray(Reader reader, String? key) { 174 | reader.expectArray(); 175 | return true; 176 | } 177 | 178 | /// Called when an array has no more elements. 179 | /// 180 | /// If [key] is non-null, it's the key of the completed value 181 | /// in the current object. This is the same key as passed to the corresponding 182 | /// [startObject] call. 183 | /// The key is always `null` outside of an object. 184 | void endArray(String? key) {} 185 | 186 | /// Called when the next value is a `null` value. 187 | /// 188 | /// The call should consume the value, for example by `reader.expectNull()`, 189 | /// `reader.skipAnyValue()` or `reader.expectAnyValueSource()`. 190 | /// 191 | /// If [key] is non-null, it's the key of the value in the current object. 192 | /// The key is always `null` outside of an object. 193 | void processNull(Reader reader, String? key) { 194 | reader.expectNull(); 195 | } 196 | 197 | /// Called when the next value is a string value. 198 | /// 199 | /// The call should consume the value, for example by `reader.expectString()`, 200 | /// `reader.skipAnyValue()` or `reader.expectAnyValueSource()`. 201 | /// 202 | /// If [key] is non-null, it's the key of the value in the current object. 203 | /// The key is always `null` outside of an object. 204 | void processString(Reader reader, String? key) { 205 | reader.expectString(); 206 | } 207 | 208 | /// Called when the next value is a boolean value. 209 | /// 210 | /// The call should consume the value, for example by `reader.expectBool()`, 211 | /// `reader.skipAnyValue()` or `reader.expectAnyValueSource()`. 212 | /// 213 | /// If [key] is non-null, it's the key of the value in the current object. 214 | /// The key is always `null` outside of an object. 215 | void processBool(Reader reader, String? key) { 216 | reader.expectBool(); 217 | } 218 | 219 | /// Called when the next value is a number value. 220 | /// 221 | /// The call should consume the value, for example by `reader.expectNum()`, 222 | /// `reader.skipAnyValue()` or `reader.expectAnyValueSource()`. 223 | /// 224 | /// If [key] is non-null, it's the key of the value in the current object. 225 | /// The key is always `null` outside of an object. 226 | void processNum(Reader reader, String? key) { 227 | reader.expectNum(); 228 | } 229 | } 230 | 231 | /// A generalized JSON processor which forwards data to a JSON sink. 232 | /// 233 | /// Allows individual `process` methods to be overridden for special 234 | /// behavior, like detecting specific keys and treating values differently, 235 | /// or detecting specific values and parsing them differently. 236 | /// 237 | /// The default behavior of the process-methods is to call [JsonSink.addKey] 238 | /// if the `key` is non-`null`, then expect a value and call the sink's 239 | /// corresponding `add` method with the value. 240 | /// Example: 241 | /// ```dart 242 | /// void processString(Reader reader, String? key) { 243 | /// if (key != null) sink.addKey(key); 244 | /// sink.addString(reader.expectString()); 245 | /// } 246 | /// ``` 247 | /// If overridden, the overriding method should make sure to call `addKey` 248 | /// on the sink first when the `key` is non-null, if it intends to add 249 | /// any value (and not add the key if it intends to skip the value). 250 | base class JsonSinkProcessor 251 | extends JsonProcessor { 252 | /// The target sink. 253 | final Sink sink; 254 | 255 | /// Creat a JSON processor forwarding events to [sink]. 256 | JsonSinkProcessor(this.sink); 257 | 258 | @override 259 | bool processObject(Reader reader, String? key) { 260 | if (key != null) sink.addKey(key); 261 | reader.expectObject(); 262 | sink.startObject(); 263 | return true; 264 | } 265 | 266 | @override 267 | void endObject(String? key) { 268 | sink.endObject(); 269 | } 270 | 271 | @override 272 | bool processArray(Reader reader, String? key) { 273 | if (key != null) sink.addKey(key); 274 | reader.expectArray(); 275 | sink.startArray(); 276 | return true; 277 | } 278 | 279 | @override 280 | void endArray(String? key) { 281 | sink.endArray(); 282 | } 283 | 284 | @override 285 | void processNull(Reader reader, String? key) { 286 | if (key != null) sink.addKey(key); 287 | reader.expectNull(); 288 | sink.addNull(); 289 | } 290 | 291 | @override 292 | void processBool(Reader reader, String? key) { 293 | if (key != null) sink.addKey(key); 294 | sink.addBool(reader.expectBool()); 295 | } 296 | 297 | @override 298 | void processNum(Reader reader, String? key) { 299 | if (key != null) sink.addKey(key); 300 | sink.addNumber(reader.expectNum()); 301 | } 302 | 303 | @override 304 | void processString(Reader reader, String? key) { 305 | if (key != null) sink.addKey(key); 306 | sink.addString(reader.expectString()); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /lib/src/json/reader/object_reader.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "../sink/sink.dart"; 16 | import "reader.dart"; 17 | import "util.dart"; 18 | 19 | /// A [JsonReader] which traverses a JSON-like object structure. 20 | /// 21 | /// A JSON-like object structure is one of: 22 | /// * `null`, 23 | /// * a number, 24 | /// * a boolean, 25 | /// * a string, 26 | /// * a list of JSON-like object structures or 27 | /// * a map from strings to JSON-like object structures. 28 | final class JsonObjectReader implements JsonReader { 29 | /// The next object to access. 30 | /// 31 | /// Is set to `#_none` when there are no next object available. 32 | /// This happens after reading the entire source object, 33 | /// after entering an array or after reading an array value, 34 | /// but before calling [hasNext] or [endArray], 35 | /// or after entering an object or reading an object value, 36 | /// but before calling [expectKey], [tryKey] or [endObject]. 37 | Object? _next; 38 | 39 | /// Stack of the currently entered objects and arrays. 40 | /// 41 | /// Contains an iterator for the elements of the array or 42 | /// the keys of the map, which is used to find the next 43 | /// one when needed. 44 | /// 45 | /// Stack elements are automatically popped from the stack 46 | /// when they have been completely iterated. 47 | _Stack? _stack; 48 | 49 | /// Creates a reader for the [object] value. 50 | JsonObjectReader(Object? object) : _next = object; 51 | 52 | /// Used by [copy] to create a copy of this reader's state. 53 | JsonObjectReader._(this._next, this._stack); 54 | 55 | @override 56 | FormatException fail(String message) => 57 | FormatException(message, _next == #_next ? null : _next); 58 | 59 | @override 60 | bool checkArray() { 61 | return _next is List; 62 | } 63 | 64 | @override 65 | bool checkBool() { 66 | return _next is bool; 67 | } 68 | 69 | @override 70 | bool checkNull() { 71 | return _next == null; 72 | } 73 | 74 | @override 75 | bool checkNum() { 76 | return _next is num; 77 | } 78 | 79 | @override 80 | bool checkObject() { 81 | return _next is Map; 82 | } 83 | 84 | @override 85 | bool checkString() { 86 | return _next is String; 87 | } 88 | 89 | @override 90 | Object? expectAnyValueSource() { 91 | var result = _next; 92 | if (result == #_none) throw StateError("No value"); 93 | _next = #_none; 94 | return result; 95 | } 96 | 97 | Object _error(String message) { 98 | if (_next == #_none) { 99 | return StateError("No value"); 100 | } 101 | return FormatException(message, _next); 102 | } 103 | 104 | @override 105 | void expectArray() => tryArray() || (throw _error("Not a JSON array")); 106 | 107 | @override 108 | bool expectBool() => tryBool() ?? (throw _error("Not an boolean")); 109 | 110 | @override 111 | double expectDouble() => tryDouble() ?? (throw _error("Not a double")); 112 | 113 | @override 114 | int expectInt() => tryInt() ?? (throw _error("Not an integer")); 115 | 116 | @override 117 | Null expectNull() => tryNull() ? null : (throw _error("Not null")); 118 | 119 | @override 120 | num expectNum() => tryNum() ?? (throw _error("Not a number")); 121 | 122 | @override 123 | void expectObject() => tryObject() || (throw _error("Not a JSON object")); 124 | 125 | @override 126 | String expectString([List? candidates]) => 127 | tryString(candidates) ?? (throw _error("Not a string")); 128 | 129 | @override 130 | int expectStringIndex(List candidates) => 131 | tryStringIndex(candidates) ?? (throw _error("Not a string")); 132 | 133 | @override 134 | bool hasNext() { 135 | if (_next == #_none) { 136 | var stack = _stack; 137 | _ListStack? list; 138 | if (stack != null && (list = stack.asList) != null) { 139 | if (list!.hasNext) { 140 | _next = list.moveNext(); 141 | return true; 142 | } 143 | _stack = stack.next; 144 | _next = #_none; 145 | return false; 146 | } 147 | } 148 | throw StateError("Not before a JSON array element"); 149 | } 150 | 151 | @override 152 | String? nextKey() { 153 | if (_next == #_none) { 154 | var stack = _stack; 155 | if (stack != null && stack.isMap) { 156 | var map = stack.asMap!; 157 | var key = map.nextKey(); 158 | if (key != null) { 159 | _next = map.valueOf(key); 160 | return key; 161 | } 162 | _stack = stack.next; 163 | _next = #_none; 164 | return null; 165 | } 166 | } 167 | throw StateError("Not before a JSON object key"); 168 | } 169 | 170 | @override 171 | bool hasNextKey() { 172 | if (_next == #_none) { 173 | var stack = _stack; 174 | if (stack != null && stack.isMap) { 175 | var map = stack.asMap!; 176 | var key = map.peekKey(); 177 | if (key != null) { 178 | return true; 179 | } 180 | _stack = stack.next; 181 | _next = #_none; 182 | return false; 183 | } 184 | } 185 | throw StateError("Not before a JSON object key"); 186 | } 187 | 188 | @override 189 | String? nextKeySource() => nextKey(); 190 | 191 | @override 192 | bool tryArray() { 193 | var current = _next; 194 | if (current is List) { 195 | _next = #_none; 196 | _stack = _ListStack(current, _stack); 197 | return true; 198 | } 199 | if (current == #_none) { 200 | throw StateError("No value"); 201 | } 202 | return false; 203 | } 204 | 205 | @override 206 | bool? tryBool() { 207 | var current = _next; 208 | if (current is bool) { 209 | _next = #_none; 210 | return current; 211 | } 212 | if (current == #_none) { 213 | throw StateError("No value"); 214 | } 215 | return null; 216 | } 217 | 218 | @override 219 | double? tryDouble() { 220 | var current = _next; 221 | if (current is num) { 222 | _next = #_none; 223 | return current.toDouble(); 224 | } 225 | if (current == #_none) { 226 | throw StateError("No value"); 227 | } 228 | return null; 229 | } 230 | 231 | @override 232 | int? tryInt() { 233 | var current = _next; 234 | if (current is int) { 235 | _next = #_none; 236 | return current; 237 | } 238 | return null; 239 | } 240 | 241 | @override 242 | String? tryKey(List candidates) { 243 | assert(areSorted(candidates), 244 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 245 | if (_next == #_none) { 246 | var stack = _stack; 247 | _MapStack? map; 248 | if (stack != null && (map = stack.asMap) != null) { 249 | var key = map!.peekKey(); 250 | if (key != null) { 251 | var index = candidates.indexOf(key); 252 | if (index >= 0) { 253 | map.moveNext(); 254 | _next = map.valueOf(key); 255 | return candidates[index]; 256 | } 257 | } 258 | return null; 259 | } 260 | } 261 | throw StateError("Not before a JSON object key"); 262 | } 263 | 264 | @override 265 | int? tryKeyIndex(List candidates) { 266 | assert(areSorted(candidates), 267 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 268 | if (_next == #_none) { 269 | var stack = _stack; 270 | _MapStack? map; 271 | if (stack != null && (map = stack.asMap) != null) { 272 | var key = map!.peekKey(); 273 | if (key != null) { 274 | var index = candidates.indexOf(key); 275 | if (index >= 0) { 276 | map.moveNext(); 277 | _next = map.valueOf(key); 278 | return index; 279 | } 280 | } 281 | return null; 282 | } 283 | } 284 | throw StateError("Not before a JSON object key"); 285 | } 286 | 287 | @override 288 | bool tryNull() { 289 | var current = _next; 290 | if (current == null) { 291 | _next = #_none; 292 | return true; 293 | } 294 | if (current == #_none) { 295 | throw StateError("No value"); 296 | } 297 | return false; 298 | } 299 | 300 | @override 301 | num? tryNum() { 302 | var current = _next; 303 | if (current is num) { 304 | _next = #_none; 305 | return current; 306 | } 307 | if (current == #_none) { 308 | throw StateError("No value"); 309 | } 310 | return null; 311 | } 312 | 313 | @override 314 | bool tryObject() { 315 | var current = _next; 316 | if (current is Map) { 317 | _next = #_none; 318 | _stack = _MapStack(current, _stack); 319 | return true; 320 | } 321 | if (current == #_none) { 322 | throw StateError("No value"); 323 | } 324 | return false; 325 | } 326 | 327 | @override 328 | String? tryString([List? candidates]) { 329 | var current = _next; 330 | if (current is String) { 331 | if (candidates != null) { 332 | var index = candidates.indexOf(current); 333 | if (index < 0) return null; 334 | _next = #_none; 335 | return candidates[index]; 336 | } 337 | _next = #_none; 338 | return current; 339 | } 340 | if (current == #_none) { 341 | throw StateError("No value"); 342 | } 343 | return null; 344 | } 345 | 346 | @override 347 | int? tryStringIndex(List candidates) { 348 | var current = _next; 349 | if (current is String) { 350 | var index = candidates.indexOf(current); 351 | if (index < 0) return null; 352 | _next = #_none; 353 | return index; 354 | } 355 | if (current == #_none) { 356 | throw StateError("No value"); 357 | } 358 | return null; 359 | } 360 | 361 | @override 362 | Null skipAnyValue() { 363 | if (_next == #_none) { 364 | throw StateError("No value"); 365 | } 366 | _next = #_none; 367 | } 368 | 369 | @override 370 | void endArray() { 371 | var stack = _stack; 372 | while (stack != null) { 373 | if (stack.isList) { 374 | _stack = stack.next; 375 | _next = #_none; 376 | return; 377 | } 378 | stack = stack.next; 379 | } 380 | throw StateError("Not inside a JSON array"); 381 | } 382 | 383 | @override 384 | void endObject() { 385 | var stack = _stack; 386 | while (stack != null) { 387 | if (stack.isMap) { 388 | _stack = stack.next; 389 | _next = #_none; 390 | return; 391 | } 392 | stack = stack.next!; 393 | } 394 | throw StateError("Not inside a JSON object"); 395 | } 396 | 397 | @override 398 | bool skipObjectEntry() { 399 | if (_next == #_none) { 400 | var stack = _stack?.asMap; 401 | if (stack != null) { 402 | if (stack.nextKey() == null) { 403 | _stack = stack.next; 404 | return false; 405 | } 406 | return true; 407 | } 408 | } 409 | throw StateError("Not before a JSON object key"); 410 | } 411 | 412 | @override 413 | JsonObjectReader copy() => JsonObjectReader._(_next, _stack?.copy()); 414 | 415 | @override 416 | Null expectAnyValue(JsonSink sink) { 417 | void emitValue() { 418 | if (tryObject()) { 419 | sink.startObject(); 420 | var key = nextKeySource(); 421 | while (key != null) { 422 | sink.addKey(key); 423 | emitValue(); 424 | key = nextKey(); 425 | } 426 | sink.endObject(); 427 | return; 428 | } 429 | if (tryArray()) { 430 | sink.startArray(); 431 | while (hasNext()) { 432 | emitValue(); 433 | } 434 | sink.endArray(); 435 | return; 436 | } 437 | if (tryNull()) { 438 | sink.addNull(); 439 | return; 440 | } 441 | var number = tryNum(); 442 | if (number != null) { 443 | sink.addNumber(number); 444 | return; 445 | } 446 | var boolean = tryBool(); 447 | if (boolean != null) { 448 | sink.addBool(boolean); 449 | return; 450 | } 451 | var string = tryString(); 452 | if (string != null) { 453 | sink.addString(string); 454 | return; 455 | } 456 | throw _error("Not a JSON value"); 457 | } 458 | 459 | emitValue(); 460 | } 461 | } 462 | 463 | abstract class _Stack { 464 | final _Stack? next; 465 | _Stack(this.next); 466 | 467 | bool get isMap => false; 468 | bool get isList => false; 469 | _MapStack? get asMap => null; 470 | _ListStack? get asList => null; 471 | 472 | _Stack copy(); 473 | } 474 | 475 | class _ListStack extends _Stack { 476 | final List elements; 477 | int index = 0; 478 | _ListStack(List list, super.parent) : elements = list; 479 | 480 | @override 481 | bool get isList => true; 482 | @override 483 | _ListStack get asList => this; 484 | 485 | bool get hasNext => index < elements.length; 486 | dynamic peek() => hasNext ? elements[index] : null; 487 | dynamic moveNext() => hasNext ? elements[index++] : null; 488 | 489 | @override 490 | _ListStack copy() => _ListStack(elements, next?.copy())..index = index; 491 | } 492 | 493 | class _MapStack extends _Stack { 494 | final Map map; 495 | final List keys; 496 | int index; 497 | 498 | _MapStack(Map map, _Stack? parent) 499 | : this._(map, map.keys.toList(), 0, parent); 500 | 501 | _MapStack._(this.map, this.keys, this.index, _Stack? parent) : super(parent); 502 | 503 | String? nextKey() => (index < keys.length) ? keys[index++] : null; 504 | 505 | String? peekKey() => (index < keys.length) ? keys[index] : null; 506 | 507 | void moveNext() { 508 | index++; 509 | } 510 | 511 | dynamic valueOf(String key) => map[key]; 512 | 513 | @override 514 | bool get isMap => true; 515 | @override 516 | _MapStack get asMap => this; 517 | 518 | @override 519 | _MapStack copy() => _MapStack._(map, keys, index, next?.copy()); 520 | } 521 | -------------------------------------------------------------------------------- /test/jsonreader_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "dart:convert"; 16 | import "dart:typed_data"; 17 | 18 | import "package:jsontool/jsontool.dart"; 19 | import "package:test/test.dart"; 20 | 21 | void main() { 22 | for (var kind in ["string", "utf8", "object"]) { 23 | group(kind, () { 24 | var reader = { 25 | "string": mkStringReader, 26 | "utf8": mkByteReader, 27 | "object": mkObjectReader, 28 | }[kind]!; 29 | testReader(reader); 30 | }); 31 | } 32 | 33 | test("StringSlice", () { 34 | var slice = StringSlice("abcdefghijklmnop", 4, 12); 35 | expect(slice.length, 8); 36 | expect(slice.toString(), "efghijkl"); 37 | expect(slice.substring(2, 6), "ghij"); 38 | 39 | expect(slice.indexOf("kl"), 6); 40 | expect(slice.indexOf("kl", 4), 6); 41 | expect(slice.indexOf("kl", 4, 7), -1); 42 | expect(slice.indexOf("ef"), 0); 43 | expect(slice.indexOf("ef", 1), -1); 44 | expect(slice.indexOf("klm"), -1); 45 | 46 | expect(slice.contains("kl"), true); 47 | expect(slice.contains("klm"), false); 48 | 49 | var subslice = slice.subslice(2, 6); 50 | expect(subslice.length, 4); 51 | expect(subslice.toString(), "ghij"); 52 | }); 53 | } 54 | 55 | void testReader(JsonReader Function(String source) read) { 56 | test("parse int", () { 57 | var g1 = read("42"); 58 | expect(g1.expectInt(), 42); 59 | var g2 = read("-42"); 60 | expect(g2.expectInt(), -42); 61 | var g3 = read("true"); 62 | expect(() => g3.expectInt(), throwsFormatException); 63 | }); 64 | test("parse num", () { 65 | var g1 = read("42"); 66 | expect(g1.expectNum(), same(42)); 67 | var g2 = read("-42.55e+1"); 68 | expect(g2.expectNum(), -425.5); 69 | var g3 = read("true"); 70 | expect(() => g3.expectNum(), throwsFormatException); 71 | }); 72 | test("parse double", () { 73 | var g1 = read("42"); 74 | expect(g1.expectDouble(), same(42.0)); 75 | var g2 = read("-42.55e+1"); 76 | expect(g2.expectDouble(), -425.5); 77 | var g3 = read("true"); 78 | expect(() => g3.expectDouble(), throwsFormatException); 79 | }); 80 | test("parse bool", () { 81 | var g1 = read("true"); 82 | expect(g1.expectBool(), true); 83 | var g2 = read("false"); 84 | expect(g2.expectBool(), false); 85 | var g3 = read("42"); 86 | expect(() => g3.expectBool(), throwsFormatException); 87 | }); 88 | test("parse string", () { 89 | var g1 = read(r'"a"'); 90 | expect(g1.expectString(), "a"); 91 | var g2 = read(r'""'); 92 | expect(g2.expectString(), ""); 93 | var g2a = read(r'"\n"'); 94 | expect(g2a.expectString(), "\n"); 95 | var g3 = read(r'"\b\t\n\r\f\\\"\/\ufffd"'); 96 | expect(g3.expectString(), '\b\t\n\r\f\\"/\ufffd'); 97 | }); 98 | 99 | test("parse array", () { 100 | var g1 = read(r'[12, "str", true]'); 101 | g1.expectArray(); 102 | expect(g1.hasNext(), true); 103 | expect(g1.expectInt(), 12); 104 | expect(g1.hasNext(), true); 105 | expect(g1.expectString(), "str"); 106 | expect(g1.hasNext(), true); 107 | expect(g1.expectBool(), true); 108 | expect(g1.hasNext(), false); 109 | }); 110 | 111 | test("parse array empty", () { 112 | var g1 = read(r'[]'); 113 | g1.expectArray(); 114 | expect(g1.hasNext(), false); 115 | }); 116 | 117 | test("parse array nested", () { 118 | var g1 = read(r'[[12, 13], [], ["str", ["str2"]], 1]'); 119 | g1.expectArray(); // [ 120 | expect(g1.hasNext(), true); 121 | g1.expectArray(); // [[ 122 | expect(g1.hasNext(), true); // [[ 123 | expect(g1.expectInt(), 12); 124 | expect(g1.hasNext(), true); // [[, 125 | expect(g1.expectInt(), 13); 126 | expect(g1.hasNext(), false); // [[,] 127 | expect(g1.hasNext(), true); // [[,], 128 | g1.expectArray(); // [[,],[ 129 | expect(g1.hasNext(), false); // [[,],[] 130 | expect(g1.hasNext(), true); // [[,],[], 131 | g1.expectArray(); // [[,],[], [ 132 | expect(g1.hasNext(), true); 133 | expect(g1.expectString(), "str"); 134 | g1.endArray(); // [[,],[], [...] 135 | expect(g1.hasNext(), true); // [[,],[], [...], 136 | expect(g1.expectInt(), 1); 137 | expect(g1.hasNext(), false); // [[,],[], [...],] 138 | }); 139 | 140 | test("parse object using nextKey", () { 141 | var g1 = read(r' { "a": true, "b": 42 } '); 142 | g1.expectObject(); 143 | expect(g1.nextKey(), "a"); 144 | expect(g1.expectBool(), true); 145 | expect(g1.nextKey(), "b"); 146 | expect(g1.expectInt(), 42); 147 | expect(g1.nextKey(), null); 148 | }); 149 | 150 | test("parse object using hasNextKey", () { 151 | var g1 = read(r' { "a": true, "b": 42 } '); 152 | g1.expectObject(); 153 | expect(g1.hasNextKey(), true); 154 | expect(g1.nextKey(), "a"); 155 | expect(g1.expectBool(), true); 156 | expect(g1.hasNextKey(), true); 157 | expect(g1.nextKey(), "b"); 158 | expect(g1.expectInt(), 42); 159 | expect(g1.hasNextKey(), false); 160 | }); 161 | 162 | test("parse empty object", () { 163 | var g1 = read(r' { } '); 164 | g1.expectObject(); 165 | expect(g1.nextKey(), null); 166 | }); 167 | 168 | test("parse nested object", () { 169 | var g1 = read(r' { "a" : {"b": true}, "c": { "d": 42 } } '); 170 | g1.expectObject(); 171 | expect(g1.nextKey(), "a"); 172 | g1.expectObject(); 173 | expect(g1.hasNextKey(), true); 174 | expect(g1.nextKey(), "b"); 175 | expect(g1.expectBool(), true); 176 | expect(g1.hasNextKey(), false); 177 | expect(g1.nextKey(), "c"); 178 | g1.expectObject(); 179 | expect(g1.nextKey(), "d"); 180 | expect(g1.expectInt(), 42); 181 | expect(g1.nextKey(), null); 182 | expect(g1.nextKey(), null); 183 | }); 184 | 185 | test("whitepsace", () { 186 | var ws = " \n\r\t"; 187 | // JSON: {"a":[1,2.5]} with all whitespaces between all tokens. 188 | var g1 = read('$ws{$ws"a"$ws:$ws[${ws}1$ws,${ws}2.5$ws]$ws}'); 189 | g1.expectObject(); 190 | expect(g1.hasNextKey(), true); 191 | expect(g1.nextKey(), "a"); 192 | g1.expectArray(); 193 | expect(g1.hasNext(), true); 194 | expect(g1.expectInt(), 1); 195 | expect(g1.hasNext(), true); 196 | expect(g1.expectDouble(), 2.5); 197 | expect(g1.hasNext(), false); 198 | expect(g1.nextKey(), null); 199 | }); 200 | 201 | test("peekKey", () { 202 | var g1 = read(r'{"a": 42, "abe": 42, "abc": 42, "b": 42}'); 203 | const candidates = ["a", "abc", "abe"]; // Sorted. 204 | g1.expectObject(); 205 | expect(g1.tryKey(candidates), same("a")); 206 | expect(g1.expectInt(), 42); 207 | expect(g1.tryKey(candidates), same("abe")); 208 | expect(g1.expectInt(), 42); 209 | expect(g1.tryKey(candidates), same("abc")); 210 | expect(g1.expectInt(), 42); 211 | expect(g1.tryKey(candidates), null); 212 | expect(g1.nextKey(), "b"); 213 | expect(g1.expectInt(), 42); 214 | expect(g1.tryKey(candidates), null); 215 | expect(g1.nextKey(), null); 216 | }); 217 | 218 | test("skipAnyValue", () { 219 | var g1 = read(r'{"a":[[[[{"a":2},null,true,false,[],{},"YES"]]]],"b":2}'); 220 | g1.expectObject(); 221 | expect(g1.nextKey(), "a"); 222 | g1.skipAnyValue(); 223 | expect(g1.nextKey(), "b"); 224 | expect(g1.expectInt(), 2); 225 | expect(g1.nextKey(), null); 226 | }); 227 | 228 | test("expectAnyValue", () { 229 | var anyValue = r'[[[[{"a":2},null,true,false,[],{},"YES"]]]]'; 230 | 231 | var g1 = read('{"a":$anyValue,"b":2}'); 232 | g1.expectObject(); 233 | expect(g1.nextKey(), "a"); 234 | 235 | var out = StringBuffer(); 236 | var w = jsonStringWriter(out); 237 | g1.expectAnyValue(w); 238 | var skipped = out.toString(); 239 | expect(skipped, anyValue); 240 | 241 | expect(g1.nextKey(), "b"); 242 | expect(g1.expectInt(), 2); 243 | expect(g1.nextKey(), null); 244 | }); 245 | 246 | test("expectAnyValueSource", () { 247 | var g1 = read(r'{"a":["test"],"b":2}'); 248 | g1.expectObject(); 249 | var key = g1.nextKeySource(); 250 | var skipped = g1.expectAnyValueSource(); 251 | expect(g1.nextKey(), "b"); 252 | expect(g1.expectInt(), 2); 253 | expect(g1.nextKey(), null); 254 | 255 | if (g1 is JsonReader) { 256 | expect(key.toString(), r'"a"'); 257 | expect(skipped.toString(), r'["test"]'); 258 | } else if (g1 is JsonReader) { 259 | expect(key, r'"a"'.codeUnits); 260 | expect(skipped, r'["test"]'.codeUnits); 261 | } else { 262 | expect(key, "a"); 263 | expect(skipped, ["test"]); 264 | } 265 | }); 266 | 267 | test("Skip object entry", () { 268 | var g1 = read(r'[{"a":["test"],"b":42,"c":"str"},37]'); 269 | g1.expectArray(); 270 | expect(g1.hasNext(), true); 271 | g1.expectObject(); 272 | expect(g1.tryKey(["a", "c"]), "a"); 273 | g1.skipAnyValue(); 274 | expect(g1.tryKey(["a", "c"]), null); 275 | expect(g1.skipObjectEntry(), true); 276 | expect(g1.tryKey(["a", "c"]), "c"); 277 | g1.skipAnyValue(); 278 | expect(g1.tryKey(["a", "c"]), null); 279 | expect(g1.skipObjectEntry(), false); 280 | expect(g1.hasNext(), true); 281 | expect(g1.expectInt(), 37); 282 | expect(g1.hasNext(), false); 283 | }); 284 | 285 | test("copy", () { 286 | var g1 = read(r'{"a": 1, "b": {"c": ["d"]}, "c": 2}'); 287 | expect(g1.tryObject(), true); 288 | expect(g1.nextKey(), "a"); 289 | expect(g1.expectInt(), 1); 290 | expect(g1.nextKey(), "b"); 291 | var g2 = g1.copy(); 292 | expect(g1.checkObject(), true); 293 | g1.skipAnyValue(); 294 | expect(g1.nextKey(), "c"); 295 | expect(g1.expectInt(), 2); 296 | expect(g1.nextKey(), null); 297 | 298 | expect(g2.tryObject(), true); 299 | expect(g2.nextKey(), "c"); 300 | expect(g2.tryArray(), true); 301 | expect(g2.hasNext(), true); 302 | expect(g2.expectString(), "d"); 303 | expect(g2.hasNext(), false); 304 | expect(g2.nextKey(), null); 305 | 306 | expect(g2.nextKey(), "c"); 307 | expect(g2.expectInt(), 2); 308 | expect(g2.nextKey(), null); 309 | }); 310 | 311 | group("validating reader,", () { 312 | test("non-composite", () { 313 | var reader = read('"x"'); 314 | var validator = validateJsonReader(reader); 315 | expectValue(validator); 316 | expect(validator.tryString(), "x"); 317 | expectDone(validator); 318 | }); 319 | 320 | test("object first", () { 321 | var reader = read('{"x":[1, 2.5, true], "y": 1, "z": 2}'); 322 | var validator = validateJsonReader(reader); 323 | 324 | // Expect value, not inside array or object. 325 | expectValue(validator); 326 | expect(validator.tryObject(), true); 327 | expectKey(validator); 328 | expect(validator.nextKey(), "x"); 329 | expectValue(validator, insideObject: true); 330 | expect(validator.tryArray(), true); 331 | expectHasNext(validator, insideObject: true); 332 | expect(validator.hasNext(), true); 333 | expectValue(validator, insideArray: true, insideObject: true); 334 | expect(validator.tryInt(), 1); 335 | expectHasNext(validator, insideObject: true); 336 | validator.endArray(); 337 | expectKey(validator); 338 | expect(validator.nextKey(), "y"); 339 | expectValue(validator, insideObject: true); 340 | validator.endObject(); 341 | expectDone(validator); 342 | }); 343 | 344 | test("array first", () { 345 | var reader = read('[{"x":[1, 2.5, true], "y": 1, "z": 2}]'); 346 | var validator = validateJsonReader(reader); 347 | 348 | // Expect value, not inside array or object. 349 | expectValue(validator); 350 | expect(validator.tryArray(), true); 351 | expectHasNext(validator); 352 | expect(validator.hasNext(), true); 353 | expectValue(validator, insideArray: true); 354 | expect(validator.tryObject(), true); 355 | expectKey(validator, insideArray: true); 356 | expect(validator.nextKey(), "x"); 357 | expectValue(validator, insideArray: true, insideObject: true); 358 | expect(validator.tryArray(), true); 359 | expectHasNext(validator, insideObject: true); 360 | expect(validator.hasNext(), true); 361 | expectValue(validator, insideArray: true, insideObject: true); 362 | expect(validator.tryInt(), 1); 363 | expectHasNext(validator, insideObject: true); 364 | validator.endArray(); 365 | expectKey(validator, insideArray: true); 366 | expect(validator.nextKey(), "y"); 367 | expectValue(validator, insideArray: true, insideObject: true); 368 | validator.endObject(); 369 | expectHasNext(validator); 370 | expect(validator.hasNext(), false); 371 | expectDone(validator); 372 | }); 373 | }); 374 | 375 | test("String candidates", () { 376 | var reader = read('{"a": "b", "c": "d"}'); 377 | var keys2 = ["a", "c"]; 378 | var values2 = ["b", "d"]; 379 | reader.expectObject(); 380 | expect(reader.tryKey(values2), null); 381 | expect(reader.tryKey(keys2), same("a")); 382 | expect(reader.tryString(keys2), null); 383 | expect(reader.tryString(values2), same("b")); 384 | expect(reader.tryKey(keys2), same("c")); 385 | expect(reader.expectString(values2), same("d")); 386 | reader.endObject(); 387 | }); 388 | 389 | test("String candidates longer", () { 390 | var reader = read('{"aaa": "aab", "aac": "aad"}'); 391 | var keys2 = ["aaa", "aac"]; 392 | var values2 = ["aab", "aad"]; 393 | reader.expectObject(); 394 | expect(reader.tryKey(values2), null); 395 | expect(reader.tryKey(keys2), same("aaa")); 396 | expect(reader.tryString(keys2), null); 397 | expect(reader.tryString(values2), same("aab")); 398 | expect(reader.tryKey(keys2), same("aac")); 399 | expect(reader.expectString(values2), same("aad")); 400 | reader.endObject(); 401 | }); 402 | 403 | test("String candidates similar suffix", () { 404 | var reader = read('{"aab": "aab"}'); 405 | var strings = ["aac", "bab"]; 406 | var correct = ["aab"]; 407 | reader.expectObject(); 408 | expect(reader.tryKey(strings), null); 409 | expect(reader.tryKey(correct), same("aab")); 410 | expect(reader.tryString(strings), null); 411 | expect(reader.tryString(correct), same("aab")); 412 | reader.endObject(); 413 | }); 414 | } 415 | 416 | JsonReader mkStringReader(String source) => JsonReader.fromString(source); 417 | JsonReader mkByteReader(String source) => 418 | JsonReader.fromUtf8(utf8.encode(source)); 419 | JsonReader mkObjectReader(String source) => 420 | JsonReader.fromObject(jsonDecode(source)); 421 | 422 | // Methods used to test validating reader. 423 | void expectNoValue(JsonReader validator) { 424 | expect(() => validator.checkString(), throwsStateError); 425 | expect(() => validator.tryString(), throwsStateError); 426 | expect(() => validator.expectString(), throwsStateError); 427 | expect(() => validator.checkArray(), throwsStateError); 428 | expect(() => validator.tryArray(), throwsStateError); 429 | expect(() => validator.expectArray(), throwsStateError); 430 | expect(() => validator.checkObject(), throwsStateError); 431 | expect(() => validator.tryObject(), throwsStateError); 432 | expect(() => validator.expectObject(), throwsStateError); 433 | expect(() => validator.expectAnyValue(nullJsonSink), throwsStateError); 434 | expect(() => validator.expectAnyValueSource(), throwsStateError); 435 | } 436 | 437 | void expectNoKey(JsonReader validator) { 438 | expect(() => validator.nextKey(), throwsStateError); 439 | expect(() => validator.tryKey(["a"]), throwsStateError); 440 | expect(() => validator.skipObjectEntry(), throwsStateError); 441 | } 442 | 443 | void expectNoHasNext(JsonReader validator) { 444 | expect(() => validator.hasNext(), throwsStateError); 445 | } 446 | 447 | void expectValue(JsonReader validator, 448 | {bool insideArray = false, bool insideObject = false}) { 449 | expectNoHasNext(validator); 450 | expectNoKey(validator); 451 | if (!insideArray) { 452 | expect(() => validator.endArray(), throwsStateError); 453 | } 454 | if (!insideObject) { 455 | expect(() => validator.endObject(), throwsStateError); 456 | } 457 | } 458 | 459 | void expectKey(JsonReader validator, {bool insideArray = false}) { 460 | expectNoHasNext(validator); 461 | if (!insideArray) { 462 | expect(() => validator.endArray(), throwsStateError); 463 | } 464 | expectNoValue(validator); 465 | } 466 | 467 | void expectHasNext(JsonReader validator, {bool insideObject = false}) { 468 | expectNoValue(validator); 469 | expectNoKey(validator); 470 | if (!insideObject) { 471 | expect(() => validator.endObject(), throwsStateError); 472 | } 473 | } 474 | 475 | void expectDone(JsonReader validator) { 476 | expectNoHasNext(validator); 477 | expectNoValue(validator); 478 | expectNoKey(validator); 479 | } 480 | -------------------------------------------------------------------------------- /lib/src/json/reader/reader.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "dart:typed_data"; 16 | 17 | import "../sink/sink.dart"; 18 | import "byte_reader.dart"; 19 | import "object_reader.dart"; 20 | import "reader_validator.dart"; 21 | import "string_reader.dart"; 22 | 23 | export "string_reader.dart" show StringSlice; 24 | 25 | /// A JSON reader which provides pull-based access to individual JSON tokens. 26 | /// 27 | /// A JSON reader is a low-level API for deconstructing JSON source without 28 | /// creating unnecessary intermediate values. 29 | /// As a low-level API, it is not attempting to provide all possible 30 | /// conveniences, and it does not validate that it is being used 31 | /// correctly. 32 | /// It is possible to call methods in an order which does not correspond 33 | /// to a valid JSON structure. The user is intended to make sure this doesn't 34 | /// happen. (However, the [validateJsonReader] can be used to add extra 35 | /// validation/ to a reader for testing.) 36 | /// 37 | /// The JSON reader scans JSON source code from start to end. 38 | /// It provides access to the next token, either an individual value or the 39 | /// start of a JSON object or array. Inside an object or array, it allows 40 | /// iterating through the entries or elements, or skipping to the end. 41 | /// It also allows completely skipping the next JSON value, which recursively 42 | /// skips objects and arrays. 43 | /// 44 | /// * `expect` methods predict the type of the next value, 45 | /// and throws if that kind of value is not the next found. 46 | /// This consumes the value for non-composite values 47 | /// and prepares for iterating elements or entries for object or arrays. 48 | /// Examples: [expectString], [expectObject]. 49 | /// * `try` methods checks whether the next value is of the expected 50 | /// kind, and if so, it works like the correspod `expect` method. 51 | /// If not, the return value represents this failure in some way 52 | /// appropriate to the return type (a `null` value if the `expect` method 53 | /// returns a value, a boolean `true`/`false` if the `expect` method 54 | /// is a `void` method). 55 | /// Examples: [tryString], [tryInt], [tryArray]. 56 | /// * `check` methods checks whether the next value is of the expected 57 | /// type, but does not consume (or parse) it. 58 | /// There are no `checkInt` or `checkDouble` methods, only a [checkNum], 59 | /// because distinguishing the two will require parsing. 60 | /// 61 | /// Methods may throw a [FormatException]. If that happens, the state 62 | /// of the reader is unspecified, and it should not be used again. 63 | /// 64 | /// The `expect` functions will throw if the next value is not of the 65 | /// expected kind. 66 | /// Both `expect` and `try` functions, and the iteration functions, *may* 67 | /// throw if the input is not valid JSON. Some errors prevent further progress, 68 | /// others may be ignored. 69 | /// The [check] functions never throw. 70 | /// 71 | /// When an array has been entered using [expectArray] or [tryArray], 72 | /// the individual elements should be iterated using [hasNext]. 73 | /// Example: 74 | /// ```dart 75 | /// var json = JsonReader.fromString(source); 76 | /// // I know it's a list of strings. 77 | /// var result = []; 78 | /// json.expectArray(); 79 | /// while (json.hasNext()) { 80 | /// result.add(json.expectString()); 81 | /// } 82 | /// ``` 83 | /// When [hasNext] returns true, the reader is in position to 84 | /// read the next value in the array. 85 | /// When [hasNext] returns false, the reader has exited the 86 | /// array. 87 | /// You can also stop the array iteration at any time by 88 | /// calling [endArray]. This will ignore any further values 89 | /// in the array and exit it. 90 | /// 91 | /// When an object has been entered using [expectObject] or [tryObject], 92 | /// it can be iterated using [nextKey]. 93 | /// Example: 94 | /// ```dart 95 | /// var json = JsonReader.fromString(source); 96 | /// // I know it's an object with string values 97 | /// var result = {}; 98 | /// json.expectObject(); 99 | /// String key; 100 | /// while ((key = json.nextKey()) != null) { 101 | /// result[key] = json.expectString(); 102 | /// } 103 | /// ``` 104 | /// When [nextKey] returns a string, the reader is in position to 105 | /// read the corresponding value in the object. 106 | /// When [nextKey] returns `null`, the reader has exited the 107 | /// object. 108 | /// The [tryKey] method can check the next key against a number 109 | /// of known keys. If the next key is not one of the candidates 110 | /// passed to the method, nothing happens. Then the [skipMapEntry] 111 | /// method can be used to ignore the following key/value pair. 112 | /// You can also stop the array iteration at any time by 113 | /// calling [endObject]. This will ignore any further keys or values 114 | /// in the object and exit it. 115 | /// 116 | /// Correct nesting of arrays or objects is handled by the caller. 117 | /// The reader may not maintain any state except how far it has 118 | /// come in the input. 119 | /// Calling methods out of order will cause unspecified behavior. 120 | /// 121 | /// The [skipAnyValue] will skip the next value completely, even if it's 122 | /// an object or array. 123 | /// The [expectAnyValueSource] will skip the next value completely, 124 | /// but return a representation of the *source* of that value 125 | /// in a format corresponding to the original source, 126 | /// as determined by the reader implementation. 127 | /// 128 | /// A reader is not necessarily *validating*. 129 | /// If the input is not valid JSON, the behavior is unspecified. 130 | abstract interface class JsonReader { 131 | /// Creates a JSON reader from a string containing JSON source. 132 | /// 133 | /// Returns a [StringSlice] from [expectAnyValueSource]. 134 | static JsonReader fromString(String source) => 135 | JsonStringReader(source); 136 | 137 | /// Creates a JSON reader from a UTF-8 encoded JSON source 138 | /// 139 | /// Returns a [Uint8List] view from [expectAnyValueSource]. 140 | static JsonReader fromUtf8(Uint8List source) => 141 | JsonByteReader(source); 142 | 143 | /// Creates a JSON reader from a JSON-like object structure. 144 | /// 145 | /// This reader is not actually scanning JSON source code, 146 | /// it merely provides a similar API for accessing JSON which 147 | /// has already been parsed into an object structure. 148 | /// It may not be as efficient as the source based reader. 149 | /// A souce based reader, like [JsonReader.fromString] or 150 | /// [JsonReader.fromUtf8], is preferred when it can be used. 151 | /// 152 | /// A JSON-like object structure is either a number, string, 153 | /// boolean or null value, or a list of JSON-like object 154 | /// structures, or a map from strings to JSON-like object 155 | /// structures. 156 | /// 157 | /// Returns the current object from [expectAnyValueSource], 158 | /// whether it's JSON-like or not. 159 | /// If all the `check`-methods return false where a value 160 | /// is expected, then it's likely because a non-JSON-like 161 | /// object is embedded in the object structure. 162 | static JsonReader fromObject(Object? source) => 163 | JsonObjectReader(source); 164 | 165 | /// Creates a [FormatException] at the current point in the input. 166 | /// 167 | /// Includes the source and position in source (if available) and 168 | /// the provided [message], to create a `FormatException` at the 169 | /// current position. 170 | FormatException fail(String message); 171 | 172 | /// Consumes the next value which must be `null`. 173 | Null expectNull(); 174 | 175 | /// Consumes the next value if it is `null`. 176 | /// 177 | /// Returns `true` if a `null` was consumed and `false` if not. 178 | bool tryNull(); 179 | 180 | /// Whether the next value is null. 181 | bool checkNull(); 182 | 183 | /// Consumes the next value which must be `true` or `false`. 184 | bool expectBool(); 185 | 186 | /// The next value, if it is `true` or `false`. 187 | /// 188 | /// If the next value is a boolean, then it is 189 | /// consumed and returned. 190 | /// Returns `null` and does not consume anything 191 | /// if there is no next value or the next value 192 | /// is not a boolean. 193 | bool? tryBool(); 194 | 195 | /// Whether the next value is a boolean. 196 | bool checkBool(); 197 | 198 | /// The next value, which must be a number. 199 | /// 200 | /// The next value must be a valid JSON number. 201 | /// It is returned as an [int] if the number 202 | /// has no decimal point or exponent, otherwise 203 | /// as a [double] (as if parsed by [num.parse]). 204 | num expectNum(); 205 | 206 | /// The next value, if it is a number. 207 | /// 208 | /// If the next value is a valid JSON number, 209 | /// it is returned as an [int] if the number 210 | /// has no decimal point or exponent, otherwise 211 | /// as a [double] (as if parsed by [num.parse]). 212 | /// Returns `null` if the next value is not a number. 213 | num? tryNum(); 214 | 215 | /// Whether the next value is a number. 216 | bool checkNum(); 217 | 218 | /// Return the next value which must be an integer. 219 | /// 220 | /// The next value must be a valid JSON number 221 | /// with no decimal point or exponent. 222 | /// It is returned as an [int] (as if parsed by [int.parse]). 223 | int expectInt(); 224 | 225 | /// Return the next value if it is an integer. 226 | /// 227 | /// If the next value is a valid JSON number 228 | /// with no decimal point or exponent, 229 | /// it is returned as an [int] (as if parsed by [int.parse]). 230 | /// Returns `null` if the next value is not an integer. 231 | int? tryInt(); 232 | 233 | /// The next value, which must be a number. 234 | /// 235 | /// The next value must be a valid JSON number. 236 | /// It is returned as a [double] (as if parsed by [double.parse]). 237 | double expectDouble(); 238 | 239 | /// The next value, if it is a number. 240 | /// 241 | /// If the next value is a valid JSON number, 242 | /// it is returned as a [double] (as if parsed by [double.parse]). 243 | /// Returns `null` if the next value is not a number. 244 | double? tryDouble(); 245 | 246 | /// The next value, which must be a string. 247 | /// 248 | /// If [candidates] is supplied, the next string must be one of the 249 | /// candidate strings, and must not contain any escape sequences. 250 | /// The [candidates] must be a *sorted* list of ASCII strings. 251 | /// 252 | /// Equivalent to [tryString] except that it throws where 253 | /// [tryString] would return `null`. 254 | String expectString([List? candidates]); 255 | 256 | /// The index of the next value, which must be a string, in [candidates]. 257 | /// 258 | /// The next value must be a string, it must be one of the strings in 259 | /// [candidates], and it must not contain any escape sequences 260 | /// The [candidates] must be a *sorted* list of ASCII strings. 261 | /// 262 | /// Equivalent to [tryStringIndex] except that it throws where 263 | /// [tryString] would return `null`. 264 | int expectStringIndex(List candidates); 265 | 266 | /// The next value, if it is a string. 267 | /// 268 | /// Returns the next value of the reader if it is a valid JSON string. 269 | /// Returns `null` if the next value is not a string. 270 | /// 271 | /// If [candidates] are supplied, the next string is only accepted if it 272 | /// is one of the strings in [candidates] *and* the string representation 273 | /// does not contain any escape sequences. 274 | /// The [candidates] *must* be a non-empty *sorted* list of ASCII 275 | /// strings. 276 | /// If the next value is one of the strings of [candidates], then the 277 | /// string value from [candidates] is returned, otherwise the 278 | /// result is `null`. 279 | /// 280 | /// Example: 281 | /// ```dart 282 | /// // Parsing the JSON text: {"type": "bool", "value": true} 283 | /// ... 284 | /// reader.expectObject(); 285 | /// var key = reader.nextKey(); 286 | /// if (key == "type") { 287 | /// var type = reader.tryString(["bool", "int", "string"]); 288 | /// if (identical(type, "bool")) { 289 | /// key = reader.nextKey(); 290 | /// assert(key == "value"); 291 | /// bool value = reader.expectBool(); 292 | /// ... 293 | /// } ... 294 | /// } 295 | /// reader.endObjet(); 296 | /// ``` 297 | /// Using the [candidates] parameter avoids allocating a new string 298 | /// if you are sure that it will have one of a small number of known 299 | /// values. 300 | String? tryString([List? candidates]); 301 | 302 | /// The index of the next value in [candidates], if it is there. 303 | /// 304 | /// If the next value is a string, it is one of the strings in [candidates] 305 | /// *and* the string representation does not contain any escape sequences, 306 | /// then the index of the value in [candidiates] is returned. 307 | /// 308 | /// Otherwise returns `null`, and no value is consumed. 309 | /// 310 | /// The [candidates] *must* be a non-empty *sorted* list of ASCII 311 | /// strings. 312 | /// 313 | /// Example: 314 | /// ```dart 315 | /// // Parsing the JSON text: {"type": "bool", "value": true} 316 | /// ... 317 | /// reader.expectObject(); 318 | /// var key = reader.nextKey(); 319 | /// if (key == "type") { 320 | /// var type = reader.tryStringIndex(["bool", "int", "string"]); 321 | /// switch (type) { 322 | /// case 0: // type was "bool" ... 323 | /// key = reader.nextKey(); 324 | /// assert(key == "value"); 325 | /// bool value = reader.expectBool(); 326 | /// // ... 327 | /// case 1: // type was "int" ... 328 | /// case 2: // type was "string" ... 329 | /// } ... 330 | /// } 331 | /// reader.endObjet(); 332 | /// ``` 333 | int? tryStringIndex(List candidates); 334 | 335 | /// Whether the next value is a string. 336 | bool checkString(); 337 | 338 | /// Enters the next value, which must be an array. 339 | /// 340 | /// The array should then be iterated using [hasNext] or [endArray]. 341 | void expectArray(); 342 | 343 | /// Enters the next value if it is an array. 344 | /// 345 | /// Returns `true` if the reader entered an array 346 | /// and `false` if not. 347 | /// 348 | /// The entered array should then be iterated using 349 | /// [hasNext] or [endArray]. 350 | bool tryArray(); 351 | 352 | /// Whether the next value is an array. 353 | bool checkArray(); 354 | 355 | /// Find the next array element in the current array. 356 | /// 357 | /// Must be called either after entering an array 358 | /// using [expectArray] or [tryArray] 359 | /// or after reading an array element. 360 | /// 361 | /// Returns true if there are more elements, 362 | /// and prepares the reader for reading the next 363 | /// array element. This element must then be 364 | /// consumed ([expectInt], etc) or skipped ([skipAnyValue]) 365 | /// before this function can be called again. 366 | /// 367 | /// Returns false if there is no next element, 368 | /// and this also exits the array. 369 | bool hasNext(); 370 | 371 | /// Skips the remainder of the current object. 372 | /// 373 | /// Exits the current array, ignoring any further 374 | /// elements. 375 | /// 376 | /// An array is "current" after entering it 377 | /// using [tryArray] or [expectArray], 378 | /// and until exiting by having [hasNext] return false 379 | /// or by calling [endArray]. 380 | /// Entering another array makes that current until 381 | /// that array is exited. 382 | void endArray(); 383 | 384 | /// Enters the next value, which must be an object. 385 | /// 386 | /// The object should then be iterated using 387 | /// [nextKey], [skipObjectEntry] or [endObject]. 388 | void expectObject(); 389 | 390 | /// Enters the next value if it is an object. 391 | /// 392 | /// Returns `true` if the reader entered an object 393 | /// and `false` if not. 394 | /// 395 | /// The entered object should then be iterated using 396 | /// [nextKey], [skipObjectEntry] or [endObject]. 397 | bool tryObject(); 398 | 399 | /// Whether the next value is an object. 400 | bool checkObject(); 401 | 402 | /// Check whether there is a next key, close object if not. 403 | /// 404 | /// Must only be used while reading an object. 405 | /// If the current object has more properties, 406 | /// then this function returns `true` and does nothing else. 407 | /// The next call to [nextKey] is then guaranteed to 408 | /// return a string. 409 | /// 410 | /// If the current object has no more properties, 411 | /// then this function returns `false` *and* the object 412 | /// is completed as if a call to [nextKey] had returned `null`. 413 | /// 414 | /// This behavior allows for usage patterns like: 415 | /// ```dart 416 | /// var result = {}; 417 | /// while (reader.hasNextKey()) { 418 | /// result[reader.nextKey()!] = reader.expectString(); 419 | /// } 420 | /// ``` 421 | /// without needing to introduce an extra variable to hold the key 422 | /// and then check if the key is `null`. 423 | bool hasNextKey(); 424 | 425 | /// The next key of the current object. 426 | /// 427 | /// Must only be used while reading an object. 428 | /// If the current object has more properties, 429 | /// then the key of the next property is returned, 430 | /// and the reader is ready to read the 431 | /// corresponding value. 432 | /// 433 | /// Returns `null` if there are no further entries, 434 | /// and exits the object. 435 | String? nextKey(); 436 | 437 | /// The source of the nex key of the current object. 438 | /// 439 | /// Must only be used while reading an object. 440 | /// If the current object has more properties, 441 | /// then the source of the key of the next property is returned, 442 | /// and the reader is ready to read the 443 | /// corresponding value. 444 | /// 445 | /// This operation can be used if the key is expected to 446 | /// be complicated or long, and there is no need for the 447 | /// key as a string object. Allocating the source slice is 448 | /// not free either, so for short ASCII strings, it can 449 | /// easily be more efficient to just use [nextKey]. 450 | /// The returned source starts at the leading quote and 451 | /// ends after the trailing quote of the key string. 452 | /// 453 | /// Returns `null` if there are no further entries, 454 | /// and exits the object. 455 | SourceSlice? nextKeySource(); 456 | 457 | /// The next object key, if it is in the list of candidates. 458 | /// 459 | /// Like [nextKey] except that it only matches if the next key 460 | /// is one of the strings in [candidates] *and* the key string 461 | /// value does not contain any escapes. 462 | /// 463 | /// The [candidates] *must* be a non-empty *sorted* list of ASCII 464 | /// strings. 465 | /// 466 | /// This is intended for simple key strings, which is what 467 | /// most JSON uses. 468 | /// If a match is found, the string object in the [candidates] list 469 | /// is returned rather than creating a new string. 470 | String? tryKey(List candidates); 471 | 472 | /// The index of the next object key in candidates, if it is there. 473 | /// 474 | /// Like [tryKey] except that it returns the index of the match in 475 | /// [candidates] instead of the string. 476 | /// 477 | /// The [candidates] *must* be a non-empty *sorted* list of ASCII 478 | /// strings. 479 | /// 480 | /// This is intended for simple key strings, which is what 481 | /// most JSON uses. 482 | int? tryKeyIndex(List candidates); 483 | 484 | /// Skips the next map entry, if there is one. 485 | /// 486 | /// Can be used in the same situations as [nextKey] or [tryKey], 487 | /// but skips the key and the following value. 488 | /// 489 | /// Returns `true` if an entry was skipped. 490 | /// Returns `false` if there are no further entries 491 | /// and exits the object. 492 | bool skipObjectEntry(); 493 | 494 | /// Skips the remainder of the current object. 495 | /// 496 | /// Exits the current object, ignoring any further 497 | /// keys or values. 498 | /// 499 | /// An object is "current" after entering it 500 | /// using [tryObject] or [expectObject], 501 | /// and until exiting by having [nextKey] return `null` 502 | /// or calling [endObject]. 503 | /// Entering another object makes that current until 504 | /// that object is exited. 505 | void endObject(); 506 | 507 | /// Skips the next value. 508 | /// 509 | /// This skips and consumes the entire next value. 510 | /// If the value is an array or object, all the 511 | /// nested elements or entries are skipped too. 512 | /// 513 | /// Example: 514 | /// ```dart 515 | /// var g = JsonGet(r'[{"a": 42}, "Here"]'); 516 | /// g.expectArray(); 517 | /// g.hasNext(); // true 518 | /// g.skipAnyValue(); 519 | /// g.hasNext(); // true; 520 | /// g.expectString(); // "Here" 521 | /// ``` 522 | Null skipAnyValue(); 523 | 524 | /// Skips the next value. 525 | /// 526 | /// This skips and consumes the entire next value. 527 | /// If the value is an array or object, all the 528 | /// nested elements or entries are skipped too. 529 | /// 530 | /// Returns a representation of the source corresponding 531 | /// to the skipped value. The kind of value returned 532 | /// depends on the implementation and source format. 533 | /// 534 | /// There must be a next value. 535 | SourceSlice expectAnyValueSource(); 536 | 537 | /// Skips the next value. 538 | /// 539 | /// This skips and consumes the entire next value. 540 | /// If the value is an array or object, all the 541 | /// nested elements or entries are skipped too. 542 | /// 543 | /// Parses the JSON structure of the skipped value 544 | /// and emits it on the [sink]. 545 | Null expectAnyValue(JsonSink sink); 546 | 547 | /// Creates a copy of the state of the current reader. 548 | /// 549 | /// This can be used to, for example, create a copy, 550 | /// then skip a value using [skipAnyValue], and then 551 | /// later come back to the copy reader and read the 552 | /// value anyway. 553 | JsonReader copy(); 554 | } 555 | 556 | /// Makes a [JsonReader] validate the order of its operations. 557 | /// 558 | /// The methods of a reader can be called in any order, including 559 | /// some which do not correspond to any JSON structure. 560 | /// Readers can be non-validating and accept any such incorrect 561 | /// ordering of operations. 562 | /// 563 | /// The returned reader will forward all methods to [reader], 564 | /// but will ensure that methods are not called in an order 565 | /// which does not correspond to a JSON structure. 566 | JsonReader validateJsonReader(JsonReader reader) => 567 | ValidatingJsonReader(reader); 568 | -------------------------------------------------------------------------------- /lib/src/json/reader/byte_reader.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "dart:typed_data"; 16 | 17 | import "../sink/sink.dart"; 18 | import "reader.dart"; 19 | import "util.dart"; 20 | 21 | /// A non-validating UTF-8 byte-based [JsonReader]. 22 | final class JsonByteReader implements JsonReader { 23 | /// The _source string being read. 24 | final Uint8List _source; 25 | 26 | /// The current position in the _source string. 27 | int _index; 28 | 29 | /// Creates scanner for UTF-8 encoded JSON [source]. 30 | JsonByteReader(Uint8List source) 31 | : _source = source, 32 | _index = 0; 33 | 34 | /// Used by [copy] to create a new byte reader with the same state. 35 | JsonByteReader._(this._source, this._index); 36 | 37 | FormatException _error(String message, [int? index]) => 38 | FormatException(message, _source, index ?? _index); 39 | 40 | @override 41 | FormatException fail(String message) => _error(message); 42 | 43 | @override 44 | void expectObject() { 45 | if (!tryObject()) throw _error("Not a JSON object"); 46 | } 47 | 48 | @override 49 | bool tryObject() { 50 | var char = _nextNonWhitespaceChar(); 51 | if (char == $lbrace) { 52 | _index++; 53 | return true; 54 | } 55 | return false; 56 | } 57 | 58 | @override 59 | String? nextKey() { 60 | var nextKey = _nextKeyStart(); 61 | if (nextKey == $rbrace) { 62 | _index++; 63 | return null; 64 | } 65 | if (nextKey == $quot) { 66 | var key = _scanString(); 67 | _expectColon(); 68 | return key; 69 | } 70 | throw _error("Not a string"); 71 | } 72 | 73 | @override 74 | bool hasNextKey() { 75 | var nextKey = _nextKeyStart(); 76 | if (nextKey == $rbrace) { 77 | _index++; 78 | return false; 79 | } 80 | if (nextKey == $quot) { 81 | return true; 82 | } 83 | throw _error("Not a string"); 84 | } 85 | 86 | @override 87 | Uint8List? nextKeySource() { 88 | var nextKey = _nextKeyStart(); 89 | if (nextKey == $rbrace) { 90 | _index++; 91 | return null; 92 | } 93 | if (nextKey == $quot) { 94 | var start = _index; 95 | _index++; 96 | if (!_skipString()) { 97 | throw _error("Unterminated string"); 98 | } 99 | var end = _index; 100 | _expectColon(); 101 | return _source.buffer 102 | .asUint8List(_source.offsetInBytes + start, end - start); 103 | } 104 | throw _error("Not a string"); 105 | } 106 | 107 | @override 108 | String? tryKey(List candidates) { 109 | assert(areSorted(candidates), 110 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 111 | var nextKey = _nextKeyStart(); 112 | if (nextKey == $rbrace) return null; 113 | if (nextKey != $quot) throw _error("Not a string"); 114 | var result = _tryCandidateString(candidates); 115 | if (result != null) { 116 | _expectColon(); 117 | return candidates[result]; 118 | } 119 | return null; 120 | } 121 | 122 | @override 123 | int? tryKeyIndex(List candidates) { 124 | assert(areSorted(candidates), 125 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 126 | var nextKey = _nextKeyStart(); 127 | if (nextKey == $rbrace) return null; 128 | if (nextKey != $quot) throw _error("Not a string"); 129 | var result = _tryCandidateString(candidates); 130 | if (result != null) { 131 | _expectColon(); 132 | return result; 133 | } 134 | return null; 135 | } 136 | 137 | /// Finds the start of the next key. 138 | /// 139 | /// Returns the code point for `"` if a string/key is found. 140 | /// Returns the code point for `}` if the end of object is found. 141 | /// Returns something else otherwise. 142 | int _nextKeyStart() { 143 | var char = _nextNonWhitespaceChar(); 144 | if (char == $rbrace) { 145 | return char; 146 | } 147 | if (char == $comma) { 148 | assert(_prevNonWhitespaceChar() != $lbrace, throw _error("Not a value")); 149 | _index++; 150 | char = _nextNonWhitespaceChar(); 151 | } 152 | return char; 153 | } 154 | 155 | void _expectColon() { 156 | var char = _nextNonWhitespaceChar(); 157 | if (char != $colon) throw _error("Not a colon"); 158 | _index++; 159 | } 160 | 161 | /// Checks for a string literal containing an element of candidates. 162 | /// 163 | /// Must be positioned at a `"` character, and must not be empty. 164 | /// The candidates must be sorted ASCII strings, and must not be empty. 165 | int? _tryCandidateString(List candidates) { 166 | var min = 0; 167 | var max = candidates.length; 168 | var i = 0; 169 | // The candidates in the range [min;max[ all start with the source 170 | // characters up to `i`. 171 | var start = _index + 1; 172 | while (start + i < _source.length) { 173 | var char = _source[start + i]; 174 | if (char == $backslash) return null; 175 | if (char == $quot) break; 176 | String candidate; 177 | while ((candidate = candidates[min]).length <= i || 178 | candidate.codeUnitAt(i) != char) { 179 | min++; 180 | if (min == max) return null; 181 | } 182 | var cursor = min + 1; 183 | while (cursor < max && candidates[cursor].codeUnitAt(i) == char) { 184 | cursor++; 185 | } 186 | max = cursor; 187 | i++; 188 | } 189 | var candidate = candidates[min]; 190 | if (candidate.length == i) { 191 | _index = start + i + 1; 192 | return min; 193 | } 194 | return null; 195 | } 196 | 197 | @override 198 | bool skipObjectEntry() { 199 | var nextChar = _nextNonWhitespaceChar(); 200 | var index = _index; 201 | if (nextChar == $quot) { 202 | _index++; 203 | _skipString(); 204 | if (_nextNonWhitespaceChar() == $colon) { 205 | _index++; 206 | _skipValue(); 207 | return true; 208 | } 209 | } else if (nextChar == $rbrace) { 210 | _index++; 211 | return false; 212 | } 213 | throw _error("Not an Object entry", index); 214 | } 215 | 216 | @override 217 | void endObject() { 218 | _skipUntil($rbrace); 219 | } 220 | 221 | @override 222 | void expectArray() { 223 | if (!tryArray()) throw _error("Not an array"); 224 | } 225 | 226 | @override 227 | bool tryArray() { 228 | var char = _nextNonWhitespaceChar(); 229 | if (char == $lbracket) { 230 | _index++; 231 | return true; 232 | } 233 | return false; 234 | } 235 | 236 | @override 237 | bool hasNext() { 238 | var char = _nextNonWhitespaceChar(); 239 | if (char == $comma) { 240 | assert( 241 | _prevNonWhitespaceChar() != $lbracket, throw _error("Not a value")); 242 | _index++; 243 | return true; 244 | } 245 | if (char != $rbracket) { 246 | return true; 247 | } 248 | _index++; 249 | return false; 250 | } 251 | 252 | @override 253 | void endArray() { 254 | _skipUntil($rbracket); 255 | } 256 | 257 | /// Skips all values until seeing [endChar], which is always one of `]` and `}`. 258 | /// 259 | /// Returns true if successful and false if reaching end-of-input without 260 | /// finding the end. 261 | bool _skipUntil(int endChar) { 262 | while (_index < _source.length) { 263 | var char = _source[_index++]; 264 | if (char == endChar) return true; 265 | if (char == $quot) { 266 | if (!_skipString()) return false; 267 | } else if (char == $lbrace) { 268 | if (!_skipUntil($rbrace)) return false; 269 | } else if (char == $lbracket) { 270 | if (!_skipUntil($rbracket)) return false; 271 | } 272 | } 273 | return false; 274 | } 275 | 276 | /// Skips to the end of a string. 277 | /// 278 | /// The start [_index] should be just after the leading quote 279 | /// character of a string literal. 280 | bool _skipString() { 281 | while (_index < _source.length) { 282 | var char = _source[_index++]; 283 | if (char == $quot) return true; 284 | if (char == $backslash) _index++; 285 | } 286 | return false; 287 | } 288 | 289 | @override 290 | String expectString([List? candidates]) { 291 | assert(candidates == null || candidates.isNotEmpty, 292 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 293 | assert(candidates == null || areSorted(candidates), 294 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 295 | var char = _nextNonWhitespaceChar(); 296 | if (char != $quot) { 297 | throw _error("Not a string"); 298 | } 299 | if (candidates != null) { 300 | var result = _tryCandidateString(candidates); 301 | if (result == null) { 302 | throw _error("Not an expected string"); 303 | } 304 | return candidates[result]; 305 | } 306 | return _scanString(); 307 | } 308 | 309 | @override 310 | int expectStringIndex(List candidates) { 311 | assert(candidates.isNotEmpty, 312 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 313 | assert(areSorted(candidates), 314 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 315 | var char = _nextNonWhitespaceChar(); 316 | if (char != $quot) { 317 | throw _error("Not a string"); 318 | } 319 | var result = _tryCandidateString(candidates); 320 | if (result == null) { 321 | throw _error("Not an expected string"); 322 | } 323 | return result; 324 | } 325 | 326 | String _scanString() { 327 | assert(_source[_index] == $quot); 328 | _index++; 329 | StringBuffer? buffer; 330 | var start = _index; 331 | while (_index < _source.length) { 332 | var char = _source[_index++]; 333 | if (char == $quot) { 334 | var slice = _stringFrom(start, _index - 1); 335 | if (buffer == null) return slice; 336 | return (buffer..write(slice)).toString(); 337 | } 338 | if (char == $backslash) { 339 | var buf = (buffer ??= StringBuffer()); 340 | buf.write(_stringFrom(start, _index - 1)); 341 | if (_index < _source.length) { 342 | char = _source[_index++]; 343 | start = _scanEscape(buf, char); 344 | } else { 345 | throw _error("Invalid escape"); 346 | } 347 | } else if (char >= 0x80) { 348 | var buf = (buffer ??= StringBuffer()); 349 | if (start < _index - 1) buf.write(_stringFrom(start, _index - 1)); 350 | if (char >= 0xC0) { 351 | if (char < 0xE0) { 352 | char = _scanUtf8(1, char & 0x1f, 0x80); 353 | } else if (char < 0xF0) { 354 | char = _scanUtf8(2, char & 0x0f, 0x800); 355 | } else { 356 | char = _scanUtf8(3, char & 0x07, 0x10000); 357 | } 358 | } else { 359 | throw _badUtf8(); 360 | } 361 | buf.writeCharCode(char); 362 | start = _index; 363 | } 364 | } 365 | throw _error("Unterminated string"); 366 | } 367 | 368 | int _scanUtf8(int count, int base, int min) { 369 | assert(count > 0); 370 | var start = _index; 371 | if (_source.length < _index + count) { 372 | throw _badUtf8(); 373 | } 374 | do { 375 | var char = _source[_index++]; 376 | if (char & 0xC0 != 0x80) { 377 | // Not the expected follow-up byte. 378 | throw _badUtf8(); 379 | } 380 | base = (base << 6) + (char & 0x3f); 381 | count--; 382 | } while (count > 0); 383 | if (base < min) { 384 | // Overlong UTF-8 encoding. 385 | _index = start; 386 | throw _badUtf8(); 387 | } 388 | return base; 389 | } 390 | 391 | FormatException _badUtf8() => 392 | throw FormatException("Invalid UTF-8", _source, _index - 1); 393 | 394 | String _stringFrom(int start, int end) => 395 | String.fromCharCodes(_source, start, end); 396 | 397 | int _scanEscape(StringBuffer buffer, int escapeChar) { 398 | switch (escapeChar) { 399 | case $u: 400 | if (_index + 4 <= _source.length) { 401 | var value = 0; 402 | for (var i = 0; i < 4; i++) { 403 | var char = _source[_index + i]; 404 | var digit = char ^ $0; 405 | if (digit <= 9) { 406 | value = value * 16 + digit; 407 | } else { 408 | char |= 0x20; 409 | if (char >= $a && char <= $f) { 410 | value = value * 16 + char - ($a - 10); 411 | } else { 412 | throw _error("Invalid escape", _index - 1); 413 | } 414 | } 415 | } 416 | buffer.writeCharCode(value); 417 | _index += 4; 418 | break; 419 | } 420 | throw _error("Invalid escape", _index - 1); 421 | case $quot: 422 | case $slash: 423 | case $backslash: 424 | return _index - 1; 425 | case $t: 426 | buffer.write("\t"); 427 | break; 428 | case $r: 429 | buffer.write("\r"); 430 | break; 431 | case $n: 432 | buffer.write("\n"); 433 | break; 434 | case $f: 435 | buffer.write("\f"); 436 | break; 437 | case $b: 438 | buffer.write("\b"); 439 | break; 440 | default: 441 | throw _error("Invalid escape", _index - 1); 442 | } 443 | return _index; 444 | } 445 | 446 | @override 447 | bool? tryBool() { 448 | var char = _nextNonWhitespaceChar(); 449 | if (char == $t) { 450 | assert(_startsWith("rue", _source, _index + 1)); 451 | _index += 4; 452 | return true; 453 | } else if (char == $f) { 454 | assert(_startsWith("alse", _source, _index + 1)); 455 | _index += 5; 456 | return false; 457 | } 458 | return null; 459 | } 460 | 461 | @override 462 | bool expectBool() => tryBool() ?? (throw _error("Not a boolean")); 463 | 464 | @override 465 | bool tryNull() { 466 | var char = _nextNonWhitespaceChar(); 467 | if (char == $n) { 468 | assert(_startsWith("ull", _source, _index + 1)); 469 | _index += 4; 470 | return true; 471 | } 472 | return false; 473 | } 474 | 475 | @override 476 | Null expectNull() { 477 | tryNull() || (throw _error("Not null")); 478 | } 479 | 480 | @override 481 | int expectInt() => _scanInt(true)!; 482 | 483 | @override 484 | int? tryInt() => _scanInt(false); 485 | 486 | int? _scanInt(bool throws) { 487 | var char = _nextNonWhitespaceChar(); 488 | _index++; 489 | var sign = 1; 490 | if (char == $minus || char == $plus) { 491 | // $minus is 0x2d, $plus is $2b 492 | sign = 0x2c - char; 493 | if (_index < _source.length) { 494 | char = _source[_index++]; 495 | } else { 496 | _index--; 497 | if (!throws) return null; 498 | throw _error("Not an integer"); 499 | } 500 | } 501 | var result = char ^ $0; 502 | if (result > 9) { 503 | _index--; 504 | if (!throws) return null; 505 | throw _error("Not an integer"); 506 | } 507 | while (_index < _source.length) { 508 | char = _source[_index]; 509 | var digit = char ^ $0; 510 | if (digit <= 9) { 511 | result = result * 10 + digit; 512 | _index++; 513 | } else { 514 | if (char == $dot || (char | 0x20) == $e /* eE */) { 515 | // It's a double. 516 | if (!throws) return null; 517 | throw _error("Not an integer"); 518 | } 519 | break; 520 | } 521 | } 522 | return sign >= 0 ? result : -result; 523 | } 524 | 525 | @override 526 | num expectNum() => _scanNumber(false, true)!; 527 | 528 | @override 529 | num? tryNum() => _scanNumber(false, false); 530 | 531 | @override 532 | double expectDouble() => _scanNumber(true, true) as dynamic; 533 | 534 | @override 535 | double? tryDouble() => _scanNumber(true, false) as dynamic; 536 | 537 | num? _scanNumber(bool asDouble, bool throws) { 538 | var char = _nextNonWhitespaceChar(); 539 | var start = _index; 540 | var index = start + 1; 541 | var sign = 1; 542 | if (char == $minus || char == $plus) { 543 | start = index; 544 | sign = 0x2c - char; // -1 if '-', +1 if '+' 545 | if (index < _source.length) { 546 | char = _source[index++]; 547 | } else { 548 | if (!throws) return null; 549 | throw _error("Not an number", index); 550 | } 551 | } 552 | var result = char ^ $0; 553 | if (result > 9) { 554 | if (!throws) return null; 555 | throw _error("Not an number", index - 1); 556 | } 557 | var isInt = !asDouble; 558 | while (index < _source.length) { 559 | char = _source[index]; 560 | var digit = char ^ $0; 561 | if (isInt && digit <= 9) { 562 | result = result * 10 + digit; 563 | index++; 564 | } else if (digit <= 9 || 565 | (char | 0x20) == $e || 566 | char == $dot || 567 | char == $plus || 568 | char == $minus) { 569 | isInt = false; 570 | index++; 571 | } else { 572 | break; 573 | } 574 | } 575 | if (isInt) { 576 | _index = index; 577 | return sign >= 0 ? result : -result; 578 | } 579 | var slice = String.fromCharCodes(_source, start, index); 580 | var doubleResult = 0.0; 581 | if (throws) { 582 | try { 583 | doubleResult = double.parse(slice); 584 | } on FormatException catch (e) { 585 | var offset = e.offset; 586 | throw FormatException( 587 | "Not a number", _source, offset == null ? null : start + offset); 588 | } 589 | } else { 590 | var result = double.tryParse(slice); 591 | if (result == null) return null; 592 | doubleResult = result; 593 | } 594 | _index = index; 595 | return sign >= 0 ? doubleResult : -doubleResult; 596 | } 597 | 598 | int _nextNonWhitespaceChar() { 599 | while (_index < _source.length) { 600 | var char = _source[_index]; 601 | if (char <= 0x20) { 602 | assert(isWhitespace(char), throw _error("Not valid character")); 603 | _index++; 604 | continue; 605 | } 606 | return char; 607 | } 608 | return -1; 609 | } 610 | 611 | /// Previous non-whitespace character, or -1 if none. 612 | /// 613 | /// Does not update [_index]. 614 | /// Only used for error checking in debug mode. 615 | int _prevNonWhitespaceChar() { 616 | var index = _index; 617 | while (index > 0) { 618 | var char = _source[--index]; 619 | if (isWhitespace(char)) { 620 | continue; 621 | } 622 | return char; 623 | } 624 | return -1; 625 | } 626 | 627 | @override 628 | bool checkNum() { 629 | var char = _nextNonWhitespaceChar(); 630 | return char ^ $0 <= 9 || char == $minus; 631 | } 632 | 633 | @override 634 | bool checkBool() { 635 | var char = _nextNonWhitespaceChar(); 636 | return char == $t || char == $f; 637 | } 638 | 639 | @override 640 | bool checkString() => _nextNonWhitespaceChar() == $quot; 641 | 642 | @override 643 | bool checkObject() => _nextNonWhitespaceChar() == $lbrace; 644 | 645 | @override 646 | bool checkArray() => _nextNonWhitespaceChar() == $lbracket; 647 | 648 | @override 649 | bool checkNull() => _nextNonWhitespaceChar() == $n; 650 | 651 | @override 652 | Null skipAnyValue() { 653 | _skipValue(); 654 | } 655 | 656 | void _skipValue() { 657 | var char = _nextNonWhitespaceChar(); 658 | _index++; 659 | if (char == $lbrace) { 660 | _skipUntil($rbrace); 661 | } else if (char == $lbracket) { 662 | _skipUntil($rbracket); 663 | } else if (char == $quot) { 664 | _skipString(); 665 | } else if (char ^ $0 <= 9 || char == $minus) { 666 | _skipNumber(); 667 | } else if (char == $f || char == $t || char == $n) { 668 | _skipWord(); 669 | } else { 670 | _index--; 671 | throw _error("Not a value"); 672 | } 673 | } 674 | 675 | // Skips past characters that may occur in a number. 676 | void _skipNumber() { 677 | while (_index < _source.length) { 678 | var char = _source[_index]; 679 | if (char ^ $0 <= 9 || 680 | char == $dot || 681 | char | 0x20 == $e || 682 | char == $minus || 683 | char == $plus) { 684 | _index++; 685 | } else { 686 | break; 687 | } 688 | } 689 | } 690 | 691 | // Skips past letters. 692 | void _skipWord() { 693 | while (_index < _source.length) { 694 | var char = _source[_index] | 0x20; 695 | if (char >= $a && char <= $z) { 696 | _index++; 697 | } else { 698 | break; 699 | } 700 | } 701 | } 702 | 703 | @override 704 | Uint8List expectAnyValueSource() { 705 | var next = _nextNonWhitespaceChar(); 706 | if (next < 0) return throw _error("Not a value"); 707 | var start = _index; 708 | _skipValue(); 709 | var end = _index; 710 | return _source.buffer 711 | .asUint8List(_source.offsetInBytes + start, end - start); 712 | } 713 | 714 | @override 715 | String? tryString([List? candidates]) { 716 | assert(candidates == null || candidates.isNotEmpty, 717 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 718 | assert(candidates == null || areSorted(candidates), 719 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 720 | if (_nextNonWhitespaceChar() == $quot) { 721 | if (candidates != null) { 722 | var index = _tryCandidateString(candidates); 723 | if (index == null) return null; 724 | return candidates[index]; 725 | } 726 | return _scanString(); 727 | } 728 | return null; 729 | } 730 | 731 | @override 732 | int? tryStringIndex(List candidates) { 733 | assert(candidates.isNotEmpty, 734 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 735 | assert(areSorted(candidates), 736 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 737 | if (_nextNonWhitespaceChar() == $quot) { 738 | return _tryCandidateString(candidates); 739 | } 740 | return null; 741 | } 742 | 743 | bool _startsWith(String asciiChars, Uint8List list, int index) { 744 | var length = asciiChars.length; 745 | if (list.length < index + length) return false; 746 | for (var i = 0; i < length; i++) { 747 | if (list[index + i] != asciiChars.codeUnitAt(i)) return false; 748 | } 749 | return true; 750 | } 751 | 752 | @override 753 | JsonByteReader copy() => JsonByteReader._(_source, _index); 754 | 755 | @override 756 | Null expectAnyValue(JsonSink sink) { 757 | var char = _nextNonWhitespaceChar(); 758 | if (char <= 0x7f) { 759 | switch (jsonCharacters.codeUnitAt(char)) { 760 | case 0: // $lbracket: 761 | _index++; 762 | sink.startArray(); 763 | while (hasNext()) { 764 | expectAnyValue(sink); 765 | } 766 | sink.endArray(); 767 | return; 768 | case 1: // $lbrace: 769 | _index++; 770 | sink.startObject(); 771 | var key = nextKey(); 772 | while (key != null) { 773 | sink.addKey(key); 774 | expectAnyValue(sink); 775 | key = nextKey(); 776 | } 777 | sink.endObject(); 778 | return; 779 | case 2: // $quot: 780 | sink.addString(_scanString()); 781 | return; 782 | case 3: // $t: 783 | assert(_startsWith("rue", _source, _index + 1)); 784 | _index += 4; 785 | sink.addBool(true); 786 | return; 787 | case 4: // $f: 788 | assert(_startsWith("alse", _source, _index + 1)); 789 | _index += 5; 790 | sink.addBool(false); 791 | return; 792 | case 5: // $n: 793 | assert(_startsWith("ull", _source, _index + 1)); 794 | _index += 4; 795 | sink.addNull(); 796 | return; 797 | case 6: // $0-9, $minus: 798 | sink.addNumber(_scanNumber(false, true)!); 799 | return; 800 | } 801 | } 802 | throw _error("Not a JSON value"); 803 | } 804 | } 805 | -------------------------------------------------------------------------------- /lib/src/json/reader/string_reader.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "../sink/sink.dart"; 16 | import "reader.dart"; 17 | import "util.dart"; 18 | 19 | /// A non-validating string-based [JsonReader]. 20 | final class JsonStringReader implements JsonReader { 21 | /// The source string being read. 22 | final String _source; 23 | 24 | /// The current position in the _source string. 25 | int _index; 26 | 27 | /// Creates scanner for JSON [source]. 28 | JsonStringReader(String source) : this._(source, 0); 29 | 30 | /// Used by [copy] to create a copy of this reader's state. 31 | JsonStringReader._(this._source, this._index); 32 | 33 | FormatException _error(String message, [int? index]) => 34 | FormatException(message, _source, index ?? _index); 35 | 36 | @override 37 | FormatException fail(String message) => _error(message); 38 | 39 | @override 40 | void expectObject() { 41 | if (!tryObject()) throw _error("Not an object"); 42 | } 43 | 44 | @override 45 | bool tryObject() { 46 | var char = _nextNonWhitespaceChar(); 47 | if (char == $lbrace) { 48 | _index++; 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | @override 55 | String? nextKey() { 56 | var nextKey = _nextKeyStart(); 57 | if (nextKey == $rbrace) { 58 | _index++; 59 | return null; 60 | } 61 | if (nextKey == $quot) { 62 | var key = _scanString(); 63 | _expectColon(); 64 | return key; 65 | } 66 | throw _error("Not a string"); 67 | } 68 | 69 | @override 70 | bool hasNextKey() { 71 | var nextKey = _nextKeyStart(); 72 | if (nextKey == $rbrace) { 73 | _index++; 74 | return false; 75 | } 76 | if (nextKey == $quot) { 77 | return true; 78 | } 79 | throw _error("Not a string"); 80 | } 81 | 82 | @override 83 | StringSlice? nextKeySource() { 84 | var nextKey = _nextKeyStart(); 85 | if (nextKey == $rbrace) { 86 | _index++; 87 | return null; 88 | } 89 | if (nextKey == $quot) { 90 | var start = _index; 91 | _index++; 92 | if (!_skipString()) { 93 | throw _error("Unterminated string"); 94 | } 95 | var end = _index; 96 | _expectColon(); 97 | return StringSlice(_source, start, end); 98 | } 99 | throw _error("Not a string"); 100 | } 101 | 102 | @override 103 | String? tryKey(List candidates) { 104 | assert(areSorted(candidates), 105 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 106 | var nextKey = _nextKeyStart(); 107 | if (nextKey == $rbrace) return null; 108 | if (nextKey != $quot) throw _error("Not a string"); 109 | var result = _tryCandidateString(candidates); 110 | if (result != null) { 111 | _expectColon(); 112 | return candidates[result]; 113 | } 114 | return null; 115 | } 116 | 117 | @override 118 | int? tryKeyIndex(List candidates) { 119 | assert(areSorted(candidates), 120 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 121 | var nextKey = _nextKeyStart(); 122 | if (nextKey == $rbrace) return null; 123 | if (nextKey != $quot) throw _error("Not a string"); 124 | var result = _tryCandidateString(candidates); 125 | if (result != null) { 126 | _expectColon(); 127 | return result; 128 | } 129 | return null; 130 | } 131 | 132 | /// Finds the start of the next key. 133 | /// 134 | /// Returns the code point for `"` if a string/key is found. 135 | /// Returns the code point for `}` if the end of object is found. 136 | /// Returns something else otherwise. 137 | int _nextKeyStart() { 138 | var char = _nextNonWhitespaceChar(); 139 | if (char == $rbrace) { 140 | return char; 141 | } 142 | if (char == $comma) { 143 | assert(_prevNonWhitespaceChar() != $lbrace, throw _error("Not a value")); 144 | _index++; 145 | char = _nextNonWhitespaceChar(); 146 | } 147 | return char; 148 | } 149 | 150 | void _expectColon() { 151 | var char = _nextNonWhitespaceChar(); 152 | if (char != $colon) throw _error("Not a colon"); 153 | _index++; 154 | } 155 | 156 | /// Checks for a string literal containing an element of candidates. 157 | /// 158 | /// Must be positioned at a `"` character. 159 | /// The candidates must be sorted ASCII strings, and must not be empty. 160 | /// 161 | /// Returns the index of a mathcing string in [candidiates], 162 | /// or `null` if not match. 163 | int? _tryCandidateString(List candidates) { 164 | var min = 0; 165 | var max = candidates.length; 166 | var start = _index + 1; 167 | var i = 0; 168 | while (start + i < _source.length) { 169 | var char = _source.codeUnitAt(start + i); 170 | if (char == $backslash) return null; 171 | if (char == $quot) break; 172 | String candidate; 173 | while ((candidate = candidates[min]).length <= i || 174 | candidate.codeUnitAt(i) != char) { 175 | min++; 176 | if (min == max) return null; 177 | } 178 | var cursor = min + 1; 179 | while (cursor < max && candidates[cursor].codeUnitAt(i) == char) { 180 | cursor++; 181 | } 182 | max = cursor; 183 | i++; 184 | } 185 | var candidate = candidates[min]; 186 | if (candidate.length == i) { 187 | _index = start + i + 1; 188 | return min; 189 | } 190 | return null; 191 | } 192 | 193 | @override 194 | bool skipObjectEntry() { 195 | var nextChar = _nextNonWhitespaceChar(); 196 | var index = _index; 197 | if (nextChar == $quot) { 198 | _index++; 199 | _skipString(); 200 | if (_nextNonWhitespaceChar() == $colon) { 201 | _index++; 202 | _skipValue(); 203 | return true; 204 | } 205 | } else if (nextChar == $rbrace) { 206 | _index++; 207 | return false; 208 | } 209 | throw _error("Not an Object entry", index); 210 | } 211 | 212 | @override 213 | void endObject() { 214 | _skipUntil($rbrace); 215 | } 216 | 217 | @override 218 | void expectArray() { 219 | if (!tryArray()) throw _error("Not an array"); 220 | } 221 | 222 | @override 223 | bool tryArray() { 224 | var char = _nextNonWhitespaceChar(); 225 | if (char == $lbracket) { 226 | _index++; 227 | return true; 228 | } 229 | return false; 230 | } 231 | 232 | @override 233 | bool hasNext() { 234 | var char = _nextNonWhitespaceChar(); 235 | if (char == $comma) { 236 | assert( 237 | _prevNonWhitespaceChar() != $lbracket, throw _error("Not a value")); 238 | _index++; 239 | return true; 240 | } 241 | if (char != $rbracket) { 242 | return true; 243 | } 244 | _index++; 245 | return false; 246 | } 247 | 248 | @override 249 | void endArray() { 250 | _skipUntil($rbracket); 251 | } 252 | 253 | /// Skips all values until seeing [endChar], which is always one of `]` and `}`. 254 | /// 255 | /// Returns true if successful and false if reaching end-of-input without 256 | /// finding the end. 257 | bool _skipUntil(int endChar) { 258 | while (_index < _source.length) { 259 | var char = _source.codeUnitAt(_index++); 260 | if (char == endChar) return true; 261 | if (char == $quot) { 262 | if (!_skipString()) return false; 263 | } else if (char == $lbrace) { 264 | if (!_skipUntil($rbrace)) return false; 265 | } else if (char == $lbracket) { 266 | if (!_skipUntil($rbracket)) return false; 267 | } 268 | } 269 | return false; 270 | } 271 | 272 | /// Skips to the end of a string. 273 | /// 274 | /// The start [_index] should be just after the leading quote 275 | /// character of a string literal. 276 | bool _skipString() { 277 | while (_index < _source.length) { 278 | var char = _source.codeUnitAt(_index++); 279 | if (char == $quot) return true; 280 | if (char == $backslash) _index++; 281 | } 282 | return false; 283 | } 284 | 285 | @override 286 | String expectString([List? candidates]) { 287 | assert(candidates == null || candidates.isNotEmpty, 288 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 289 | assert(candidates == null || areSorted(candidates), 290 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 291 | var char = _nextNonWhitespaceChar(); 292 | if (char != $quot) { 293 | throw _error("Not a string"); 294 | } 295 | if (candidates != null) { 296 | var result = _tryCandidateString(candidates); 297 | if (result == null) { 298 | throw _error("Not an expected string"); 299 | } 300 | return candidates[result]; 301 | } 302 | return _scanString(); 303 | } 304 | 305 | @override 306 | int expectStringIndex(List candidates) { 307 | assert(candidates.isNotEmpty, 308 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 309 | assert(areSorted(candidates), 310 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 311 | var char = _nextNonWhitespaceChar(); 312 | if (char != $quot) { 313 | throw _error("Not a string"); 314 | } 315 | var result = _tryCandidateString(candidates); 316 | if (result == null) { 317 | throw _error("Not an expected string"); 318 | } 319 | return result; 320 | } 321 | 322 | String _scanString() { 323 | assert(_source.codeUnitAt(_index) == $quot); 324 | _index++; 325 | StringBuffer? buffer; 326 | var start = _index; 327 | while (_index < _source.length) { 328 | var char = _source.codeUnitAt(_index++); 329 | if (char == $quot) { 330 | var slice = _source.substring(start, _index - 1); 331 | if (buffer == null) return slice; 332 | return (buffer..write(slice)).toString(); 333 | } 334 | if (char == $backslash) { 335 | var buf = (buffer ??= StringBuffer()); 336 | buf.write(_source.substring(start, _index - 1)); 337 | if (_index < _source.length) { 338 | char = _source.codeUnitAt(_index++); 339 | start = _scanEscape(buf, char); 340 | } else { 341 | throw _error("Invalid escape"); 342 | } 343 | } 344 | } 345 | throw _error("Unterminated string"); 346 | } 347 | 348 | int _scanEscape(StringBuffer buffer, int escapeChar) { 349 | switch (escapeChar) { 350 | case $u: 351 | if (_index + 4 <= _source.length) { 352 | var value = 0; 353 | for (var i = 0; i < 4; i++) { 354 | var char = _source.codeUnitAt(_index + i); 355 | var digit = char ^ $0; 356 | if (digit <= 9) { 357 | value = value * 16 + digit; 358 | } else { 359 | char |= 0x20; 360 | if (char >= $a && char <= $f) { 361 | value = value * 16 + char - ($a - 10); 362 | } else { 363 | throw _error("Invalid escape", _index - 1); 364 | } 365 | } 366 | } 367 | buffer.writeCharCode(value); 368 | _index += 4; 369 | break; 370 | } 371 | throw _error("Invalid escape", _index - 1); 372 | case $quot: 373 | case $slash: 374 | case $backslash: 375 | return _index - 1; 376 | case $t: 377 | buffer.write("\t"); 378 | break; 379 | case $r: 380 | buffer.write("\r"); 381 | break; 382 | case $n: 383 | buffer.write("\n"); 384 | break; 385 | case $f: 386 | buffer.write("\f"); 387 | break; 388 | case $b: 389 | buffer.write("\b"); 390 | break; 391 | default: 392 | throw _error("Invalid escape", _index - 1); 393 | } 394 | return _index; 395 | } 396 | 397 | @override 398 | bool? tryBool() { 399 | var char = _nextNonWhitespaceChar(); 400 | if (char == $t) { 401 | assert(_source.startsWith("rue", _index + 1)); 402 | _index += 4; 403 | return true; 404 | } else if (char == $f) { 405 | assert(_source.startsWith("alse", _index + 1)); 406 | _index += 5; 407 | return false; 408 | } 409 | return null; 410 | } 411 | 412 | @override 413 | bool expectBool() => tryBool() ?? (throw _error("Not a boolean")); 414 | 415 | @override 416 | bool tryNull() { 417 | var char = _nextNonWhitespaceChar(); 418 | if (char == $n) { 419 | assert(_source.startsWith("ull", _index + 1)); 420 | _index += 4; 421 | return true; 422 | } 423 | return false; 424 | } 425 | 426 | @override 427 | Null expectNull() { 428 | tryNull() || (throw _error("Not null")); 429 | } 430 | 431 | @override 432 | int expectInt() => _scanInt(true)!; 433 | 434 | @override 435 | int? tryInt() => _scanInt(false); 436 | 437 | int? _scanInt(bool throws) { 438 | var char = _nextNonWhitespaceChar(); 439 | _index++; 440 | var sign = 1; 441 | if (char == $minus || char == $plus) { 442 | // $minus is 0x2d, $plus is $2b 443 | sign = 0x2c - char; 444 | if (_index < _source.length) { 445 | char = _source.codeUnitAt(_index++); 446 | } else { 447 | _index--; 448 | if (!throws) return null; 449 | throw _error("Not an integer"); 450 | } 451 | } 452 | var result = char ^ $0; 453 | if (result > 9) { 454 | _index--; 455 | if (!throws) return null; 456 | throw _error("Not an integer"); 457 | } 458 | while (_index < _source.length) { 459 | char = _source.codeUnitAt(_index); 460 | var digit = char ^ $0; 461 | if (digit <= 9) { 462 | result = result * 10 + digit; 463 | _index++; 464 | } else { 465 | if (char == $dot || (char | 0x20) == $e /* eE */) { 466 | // It's a double. 467 | if (!throws) return null; 468 | throw _error("Not an integer"); 469 | } 470 | break; 471 | } 472 | } 473 | return sign >= 0 ? result : -result; 474 | } 475 | 476 | @override 477 | num expectNum() => _scanNumber(false, true)!; 478 | 479 | @override 480 | num? tryNum() => _scanNumber(false, false); 481 | 482 | @override 483 | double expectDouble() => _scanNumber(true, true) as dynamic; 484 | 485 | @override 486 | double? tryDouble() => _scanNumber(true, false) as dynamic; 487 | 488 | num? _scanNumber(bool asDouble, bool throws) { 489 | var char = _nextNonWhitespaceChar(); 490 | var start = _index; 491 | var index = start + 1; 492 | var sign = 1; 493 | if (char == $minus || char == $plus) { 494 | start = index; 495 | sign = 0x2c - char; // -1 if '-', +1 if '+' 496 | if (index < _source.length) { 497 | char = _source.codeUnitAt(index++); 498 | } else { 499 | if (!throws) return null; 500 | throw _error("Not an number", index); 501 | } 502 | } 503 | var result = char ^ $0; 504 | if (result > 9) { 505 | if (!throws) return null; 506 | throw _error("Not an number", index - 1); 507 | } 508 | var isInt = !asDouble; 509 | while (index < _source.length) { 510 | char = _source.codeUnitAt(index); 511 | var digit = char ^ $0; 512 | if (isInt && digit <= 9) { 513 | result = result * 10 + digit; 514 | index++; 515 | } else if (digit <= 9 || 516 | (char | 0x20) == $e || 517 | char == $dot || 518 | char == $plus || 519 | char == $minus) { 520 | isInt = false; 521 | index++; 522 | } else { 523 | break; 524 | } 525 | } 526 | if (isInt) { 527 | _index = index; 528 | return sign >= 0 ? result : -result; 529 | } 530 | var slice = _source.substring(start, index); 531 | var doubleResult = 0.0; 532 | if (throws) { 533 | try { 534 | doubleResult = double.parse(slice); 535 | } on FormatException catch (e) { 536 | var offset = e.offset; 537 | throw FormatException( 538 | "Not a number", _source, offset == null ? null : start + offset); 539 | } 540 | } else { 541 | var result = double.tryParse(slice); 542 | if (result == null) return null; 543 | doubleResult = result; 544 | } 545 | _index = index; 546 | return sign >= 0 ? doubleResult : -doubleResult; 547 | } 548 | 549 | int _nextNonWhitespaceChar() { 550 | while (_index < _source.length) { 551 | var char = _source.codeUnitAt(_index); 552 | if (char <= 0x20) { 553 | assert(isWhitespace(char), throw _error("Not valid character")); 554 | _index++; 555 | continue; 556 | } 557 | return char; 558 | } 559 | return -1; 560 | } 561 | 562 | /// Previous non-whitespace character, or -1 if none. 563 | /// 564 | /// Does not update [_index]. 565 | /// Only used for error checking in debug mode. 566 | int _prevNonWhitespaceChar() { 567 | var index = _index; 568 | while (index > 0) { 569 | var char = _source.codeUnitAt(--index); 570 | if (isWhitespace(char)) { 571 | continue; 572 | } 573 | return char; 574 | } 575 | return -1; 576 | } 577 | 578 | @override 579 | bool checkNum() { 580 | var char = _nextNonWhitespaceChar(); 581 | return char ^ $0 <= 9 || char == $minus; 582 | } 583 | 584 | @override 585 | bool checkBool() { 586 | var char = _nextNonWhitespaceChar(); 587 | return char == $t || char == $f; 588 | } 589 | 590 | @override 591 | bool checkString() => _nextNonWhitespaceChar() == $quot; 592 | 593 | @override 594 | bool checkObject() => _nextNonWhitespaceChar() == $lbrace; 595 | 596 | @override 597 | bool checkArray() => _nextNonWhitespaceChar() == $lbracket; 598 | 599 | @override 600 | bool checkNull() => _nextNonWhitespaceChar() == $n; 601 | 602 | @override 603 | Null skipAnyValue() { 604 | _skipValue(); 605 | } 606 | 607 | void _skipValue() { 608 | var char = _nextNonWhitespaceChar(); 609 | _index++; 610 | if (char == $lbrace) { 611 | _skipUntil($rbrace); 612 | } else if (char == $lbracket) { 613 | _skipUntil($rbracket); 614 | } else if (char == $quot) { 615 | _skipString(); 616 | } else if (char ^ $0 <= 9 || char == $minus) { 617 | _skipNumber(); 618 | } else if (char == $f || char == $t || char == $n) { 619 | _skipWord(); 620 | } else { 621 | _index--; 622 | throw _error("Not a value"); 623 | } 624 | } 625 | 626 | // Skips past characters that may occur in a number. 627 | void _skipNumber() { 628 | while (_index < _source.length) { 629 | var char = _source.codeUnitAt(_index); 630 | if (char ^ $0 <= 9 || 631 | char == $dot || 632 | char | 0x20 == $e || 633 | char == $minus || 634 | char == $plus) { 635 | _index++; 636 | } else { 637 | break; 638 | } 639 | } 640 | } 641 | 642 | // Skips past letters. 643 | void _skipWord() { 644 | while (_index < _source.length) { 645 | var char = _source.codeUnitAt(_index) | 0x20; 646 | if (char >= $a && char <= $z) { 647 | _index++; 648 | } else { 649 | break; 650 | } 651 | } 652 | } 653 | 654 | @override 655 | StringSlice expectAnyValueSource() { 656 | var next = _nextNonWhitespaceChar(); 657 | if (next < 0) return throw _error("Not a value"); 658 | var start = _index; 659 | _skipValue(); 660 | var end = _index; 661 | return StringSlice(_source, start, end); 662 | } 663 | 664 | @override 665 | String? tryString([List? candidates]) { 666 | assert(candidates == null || candidates.isNotEmpty, 667 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 668 | assert(candidates == null || areSorted(candidates), 669 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 670 | if (_nextNonWhitespaceChar() == $quot) { 671 | if (candidates != null) { 672 | var index = _tryCandidateString(candidates); 673 | if (index != null) return candidates[index]; 674 | return null; 675 | } 676 | return _scanString(); 677 | } 678 | return null; 679 | } 680 | 681 | @override 682 | int? tryStringIndex(List candidates) { 683 | assert(candidates.isNotEmpty, 684 | throw ArgumentError.value(candidates, "candidates", "Are empty")); 685 | assert(areSorted(candidates), 686 | throw ArgumentError.value(candidates, "candidates", "Are not sorted")); 687 | if (_nextNonWhitespaceChar() == $quot) { 688 | return _tryCandidateString(candidates); 689 | } 690 | return null; 691 | } 692 | 693 | @override 694 | JsonStringReader copy() => JsonStringReader._(_source, _index); 695 | 696 | @override 697 | Null expectAnyValue(JsonSink sink) { 698 | var char = _nextNonWhitespaceChar(); 699 | if (char <= 0x7f) { 700 | switch (jsonCharacters.codeUnitAt(char)) { 701 | case 0: // $lbracket: 702 | _index++; 703 | sink.startArray(); 704 | while (hasNext()) { 705 | expectAnyValue(sink); 706 | } 707 | sink.endArray(); 708 | return; 709 | case 1: // $lbrace: 710 | _index++; 711 | sink.startObject(); 712 | var key = nextKey(); 713 | while (key != null) { 714 | sink.addKey(key); 715 | expectAnyValue(sink); 716 | key = nextKey(); 717 | } 718 | sink.endObject(); 719 | return; 720 | case 2: // $quot: 721 | sink.addString(_scanString()); 722 | return; 723 | case 3: // $t: 724 | assert(_source.startsWith("rue", _index + 1)); 725 | _index += 4; 726 | sink.addBool(true); 727 | return; 728 | case 4: // $f: 729 | assert(_source.startsWith("alse", _index + 1)); 730 | _index += 5; 731 | sink.addBool(false); 732 | return; 733 | case 5: // $n: 734 | assert(_source.startsWith("ull", _index + 1)); 735 | _index += 4; 736 | sink.addNull(); 737 | return; 738 | case 6: // $0-9, $minus: 739 | sink.addNumber(_scanNumber(false, true)!); 740 | return; 741 | } 742 | } 743 | throw _error("Not a JSON value"); 744 | } 745 | } 746 | 747 | /// A slice of a larger string. 748 | /// 749 | /// Represents the substring of [source] from [start] to [end]. 750 | /// Allows some operations on that substring without having to 751 | /// create it as a separate string. 752 | class StringSlice { 753 | /// The original string. 754 | final String source; 755 | 756 | /// The start of the slice. 757 | /// 758 | /// The _index of the first character after the start of the slice. 759 | final int start; 760 | 761 | /// The end of the slice. 762 | /// 763 | /// This is the _index of the first character after the end of the slice. 764 | final int end; 765 | 766 | /// Creates a slice of [source] from [start] to [end]. 767 | const StringSlice(this.source, this.start, this.end) 768 | : assert(0 <= start), 769 | assert(start <= end), 770 | assert(end <= source.length); 771 | 772 | /// length of the string slice. 773 | int get length => end - start; 774 | 775 | /// A substring of the string slice. 776 | String substring(int start, [int? end]) { 777 | end = RangeError.checkValidRange(start, end, this.end - this.start); 778 | return source.substring(this.start + start, this.start + end); 779 | } 780 | 781 | /// A sub-slice of the string slice. 782 | StringSlice subslice(int start, [int? end]) { 783 | end = RangeError.checkValidRange(start, end, this.end - this.start); 784 | return StringSlice(source, this.start + start, this.start + end); 785 | } 786 | 787 | /// The index of the first occurrence of [pattern] in this slice string. 788 | /// 789 | /// Returns `-1` if [pattern] does not occur inside this 790 | /// slice string at a position after [start]. 791 | int indexOf(String pattern, [int start = 0, int? end]) { 792 | end = RangeError.checkValidRange(start, end, this.end - this.start); 793 | return _indexOf(pattern, start, end); 794 | } 795 | 796 | int _indexOf(String pattern, int start, int end) { 797 | var last = end - pattern.length; 798 | for (var i = start; i <= last; i++) { 799 | if (source.startsWith(pattern, this.start + i)) return i; 800 | } 801 | return -1; 802 | } 803 | 804 | /// Whether the slice string contains [pattern]. 805 | bool contains(String pattern) => _indexOf(pattern, 0, end - start) >= 0; 806 | 807 | /// The slice string characters as a separate string. 808 | @override 809 | String toString() => source.substring(start, end); 810 | } 811 | --------------------------------------------------------------------------------