├── .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 |
--------------------------------------------------------------------------------