Skip to content

Commit 6ba5db2

Browse files
committed
Add JsonLiteral.asConst, updated CHANGELOG
Fix edge cases with String encoding in JsonLiteral generator And add tests
1 parent f15d01f commit 6ba5db2

10 files changed

+233
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## 0.2.2
22

33
* Enable support for `enum` values.
4+
* Added `asConst` to `JsonLiteral`.
5+
* Improved the handling of Dart-specific characters in JSON strings.
46

57
## 0.2.1
68

analysis_options.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ linter:
3535
- only_throw_errors
3636
- prefer_final_fields
3737
- prefer_is_not_empty
38-
# Waiting for linter 0.1.33 to land in SDK
39-
#- prefer_single_quotes
38+
- prefer_single_quotes
4039
- slash_for_doc_comments
4140
- type_init_formals

example/example.g.dart

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/src/json_literal.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
// TODO: option to make the literal a const
65
class JsonLiteral {
6+
/// The relative path from the Dart file with the annotation to the file
7+
/// containing the source JSON.
78
final String path;
89

9-
const JsonLiteral(this.path);
10+
/// `true` if the JSON literal should be written as a constant.
11+
final bool asConst;
12+
13+
const JsonLiteral(this.path, {bool asConst: false})
14+
: this.asConst = asConst ?? false;
1015
}

lib/src/json_literal_generator.dart

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,89 @@ class JsonLiteralGenerator extends GeneratorForAnnotation<JsonLiteral> {
2828
p.join(sourcePathDir, annotation.read('path').stringValue));
2929
var content = JSON.decode(await buildStep.readAsString(fileId));
3030

31-
var thing = JSON.encode(content);
31+
var asConst = annotation.read('asConst').boolValue;
3232

33-
var marked = _isConstType(content) ? 'const' : 'final';
33+
var thing = _jsonLiteralAsDart(content, asConst).toString();
34+
var marked = asConst ? 'const' : 'final';
3435

3536
return '$marked _\$${element.displayName}JsonLiteral = $thing;';
3637
}
3738
}
3839

39-
bool _isConstType(value) {
40-
return value == null || value is String || value is num || value is bool;
40+
/// Returns a [String] representing a valid Dart literal for [value].
41+
///
42+
/// If [asConst] is `true`, the returned [String] is encoded as a `const`
43+
/// literal.
44+
String _jsonLiteralAsDart(dynamic value, bool asConst) {
45+
if (value == null) return 'null';
46+
47+
if (value is String) return _jsonStringAsDart(value);
48+
49+
if (value is bool || value is num) return value.toString();
50+
51+
if (value is List) {
52+
var listItems =
53+
value.map((v) => _jsonLiteralAsDart(v, asConst)).join(',\n');
54+
return '${asConst ? 'const' : ''}[$listItems]';
55+
}
56+
57+
if (value is Map) return _jsonMapAsDart(value, asConst);
58+
59+
throw new StateError(
60+
'Should never get here – with ${value.runtimeType} - `$value`.');
61+
}
62+
63+
String _jsonMapAsDart(Map value, bool asConst) {
64+
var buffer = new StringBuffer();
65+
if (asConst) {
66+
buffer.write('const ');
67+
}
68+
buffer.write('{');
69+
70+
var first = true;
71+
value.forEach((String k, v) {
72+
if (first) {
73+
first = false;
74+
} else {
75+
buffer.writeln(',');
76+
}
77+
buffer.write(_jsonStringAsDart(k));
78+
buffer.write(':');
79+
buffer.write(_jsonLiteralAsDart(v, asConst));
80+
});
81+
82+
buffer.write('}');
83+
84+
return buffer.toString();
85+
}
86+
87+
String _jsonStringAsDart(String value) {
88+
var containsSingleQuote = value.contains("'");
89+
var contains$ = value.contains(r'$');
90+
91+
if (containsSingleQuote) {
92+
if (value.contains('"')) {
93+
// `value` contains both single and double quotes as well as `$`.
94+
// The only safe way to wrap the content is to escape all of the
95+
// problematic characters.
96+
var string = value
97+
.replaceAll('\$', '\\\$')
98+
.replaceAll('"', '\\"')
99+
.replaceAll("'", "\\'");
100+
return "'$string'";
101+
} else if (contains$) {
102+
// `value` contains "'" and "$", but not '"'.
103+
// Safely wrap it in a raw string within double-quotes.
104+
return 'r"$value"';
105+
}
106+
return '"$value"';
107+
} else if (contains$) {
108+
// `value` contains "$", but no "'"
109+
// wrap it in a raw string using single quotes
110+
return "r'$value'";
111+
}
112+
113+
// `value` contains no problematic characters - except for '"' maybe.
114+
// Wrap it in standard single-quotes.
115+
return "'$value'";
41116
}

test/json_literal_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:test/test.dart';
5+
import 'package:path/path.dart' as p;
6+
7+
import 'test_files/json_literal.dart';
8+
import 'test_utils.dart';
9+
10+
main() {
11+
test('literal round-trip', () {
12+
var dataFilePath =
13+
p.join(getPackagePath(), 'test', 'test_files', 'data.json');
14+
var dataFile = new File(dataFilePath);
15+
16+
var dataString = loudEncode(JSON.decode(dataFile.readAsStringSync()));
17+
// FYI: nice to re-write the test data when it's changed to keep it pretty
18+
// ... but not a good idea to ship this
19+
// dataFile.writeAsStringSync(dataString);
20+
var dartString = loudEncode(data);
21+
22+
expect(dartString, dataString);
23+
});
24+
}

test/test_files/data.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
"simple string",
3+
"'string with single quotes'",
4+
"\"string with double quotes\"",
5+
"'With singles and \"doubles\"'",
6+
"dollar $igns",
7+
"'single quotes and dollor $ig$'",
8+
"${'nice!'}",
9+
"\"\"hello\"\"",
10+
"\"\"$double quotes and dollar signs\"\"",
11+
"$scary with 'single quotes' and triple-doubles \"\"\"oh no!",
12+
null,
13+
true,
14+
false,
15+
5,
16+
5.5351,
17+
-5.5,
18+
{},
19+
{
20+
"null": null,
21+
"int": 42,
22+
"double": 42.0,
23+
"string": "string",
24+
"list": [],
25+
"bool": true
26+
}
27+
]

test/test_files/json_literal.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library json_serializable.example;
6+
7+
import 'package:json_serializable/annotations.dart';
8+
part 'json_literal.g.dart';
9+
10+
@JsonLiteral('data.json')
11+
List get data => _$dataJsonLiteral;
12+
13+
@JsonLiteral('data.json', asConst: true)
14+
List get asConst => _$asConstJsonLiteral;

test/test_files/json_literal.g.dart

Lines changed: 62 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/test_utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ void roundTripObject(object, factory(Map<String, dynamic> json)) {
8888
/// Prints out nested causes before throwing `JsonUnsupportedObjectError`.
8989
String loudEncode(object) {
9090
try {
91-
return const JsonEncoder.withIndent(' ').convert(object.toJson());
91+
return const JsonEncoder.withIndent(' ').convert(object);
9292
} on JsonUnsupportedObjectError catch (e) {
9393
var error = e;
9494
do {

0 commit comments

Comments
 (0)