Skip to content

Commit 12e33d1

Browse files
authored
[json_syntax_generator] Add support for Map<String, SomeClass> (#2426)
Pub does not expose any of its json/yaml format parsers and unparsers. Instead of just navigating json/yaml in Dart code blindly, lets use a schema to navigate these. For the `pubspec_lock.yaml` we need support for `Map<String, SomeClass>` which we didn't have yet in hooks/code_assets. Additionally, the json keys and values used in the pubspec lock have `-` and ` ` inside their strings. These are not valid Dart identifer characters, so use these as separators for camel case names. I haven't figured out where the syntax classes themselves should live yet, so this PR is just adding it as an integration test for the generator for now.
1 parent e4827b3 commit 12e33d1

File tree

8 files changed

+1025
-10
lines changed

8 files changed

+1025
-10
lines changed

.github/pr-title-checker-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"[infra] ",
1111
"[jni] ",
1212
"[jnigen] ",
13+
"[json_syntax_generator] ",
1314
"[native_doc_dartifier] ",
1415
"[native_test_helpers] ",
1516
"[native_toolchain_c] ",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
build/

pkgs/json_syntax_generator/lib/src/generator/property_generator.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,53 @@ List<String> $validateName() {
259259
}
260260
return result;
261261
}
262+
''');
263+
case ClassDartType():
264+
final itemType = valueType;
265+
final typeName = itemType.toString();
266+
buffer.writeln('''
267+
$dartType get $fieldName {
268+
final jsonValue = _reader.optionalMap('$jsonKey');
269+
if (jsonValue == null) {
270+
return null;
271+
}
272+
return {
273+
for (final MapEntry(:key, :value) in jsonValue.entries)
274+
key: $typeName.fromJson(
275+
value as $jsonObjectDartType,
276+
path: [...path, key],
277+
)
278+
};
279+
}
280+
281+
set $setterName($dartType value) {
282+
if (value == null) {
283+
json.remove('$jsonKey');
284+
} else {
285+
json['$jsonKey'] = {
286+
for (final MapEntry(:key, :value) in value.entries) key: value.json,
287+
};
288+
}
289+
$sortOnKey
290+
}
291+
292+
List<String> $validateName() {
293+
final mapErrors = _reader.validateOptionalMap('$jsonKey');
294+
if (mapErrors.isNotEmpty) {
295+
return mapErrors;
296+
}
297+
final jsonValue = _reader.optionalMap(
298+
'$jsonKey',
299+
);
300+
if (jsonValue == null) {
301+
return [];
302+
}
303+
final result = <String>[];
304+
for (final value in $fieldName!.values) {
305+
result.addAll(value.validate());
306+
}
307+
return result;
308+
}
262309
''');
263310
case SimpleDartType():
264311
switch (valueType.typeName) {

pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -351,17 +351,21 @@ class SchemaAnalyzer {
351351
final additionalPropertiesBool =
352352
additionalPropertiesSchema.additionalPropertiesBool;
353353
if (additionalPropertiesBool != true) {
354-
throw UnimplementedError(
355-
'Expected an object with arbitrary properties.',
354+
_analyzeClass(additionalPropertiesSchema);
355+
final clazz = _classes[additionalPropertiesSchema.className]!;
356+
dartType = MapDartType(
357+
valueType: ClassDartType(classInfo: clazz, isNullable: false),
358+
isNullable: !required,
359+
);
360+
} else {
361+
dartType = MapDartType(
362+
valueType: const MapDartType(
363+
valueType: ObjectDartType(isNullable: true),
364+
isNullable: false,
365+
),
366+
isNullable: !required,
356367
);
357368
}
358-
dartType = MapDartType(
359-
valueType: const MapDartType(
360-
valueType: ObjectDartType(isNullable: true),
361-
isNullable: false,
362-
),
363-
isNullable: !required,
364-
);
365369
case null:
366370
if (schemas.additionalPropertiesBool != true) {
367371
throw UnimplementedError(
@@ -421,7 +425,11 @@ class SchemaAnalyzer {
421425
return '';
422426
}
423427

424-
final parts = string.replaceAll('/', '_').split('_');
428+
final parts = string
429+
.replaceAll('/', '_')
430+
.replaceAll(' ', '_')
431+
.replaceAll('-', '_')
432+
.split('_');
425433

426434
String remapCapitalization(String input) => nameOverrides[input] ?? input;
427435

pkgs/json_syntax_generator/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ dependencies:
2020
dev_dependencies:
2121
custom_lint: ^0.7.5
2222
dart_flutter_team_lints: ^3.5.2
23+
native_test_helpers:
24+
path: ../native_test_helpers/
2325
path: ^1.9.1
2426
repo_lint_rules:
2527
path: ../repo_lint_rules/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2025, 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+
import 'dart:convert';
6+
import 'dart:io';
7+
8+
import 'package:json_schema/json_schema.dart';
9+
import 'package:json_syntax_generator/json_syntax_generator.dart';
10+
import 'package:native_test_helpers/native_test_helpers.dart';
11+
import 'package:test/test.dart';
12+
13+
void main() {
14+
test('pubspec.lock schema', () {
15+
final packageRoot = findPackageRoot('json_syntax_generator');
16+
final schemaFile = File.fromUri(
17+
packageRoot.resolve('test_data/pubspec_lock/pubspec_lock.schema.json'),
18+
);
19+
final schemaJson = jsonDecode(schemaFile.readAsStringSync());
20+
final schema = JsonSchema.create(schemaJson as Object);
21+
22+
final analyzedSchema = SchemaAnalyzer(
23+
schema,
24+
nameOverrides: {'path': 'path\$'},
25+
).analyze();
26+
final output = SyntaxGenerator(
27+
analyzedSchema,
28+
header: '''
29+
// This file is generated, do not edit.
30+
''',
31+
).generate();
32+
final goldenUri = packageRoot.resolve(
33+
'test_data/pubspec_lock/pubspec_lock_syntax.g.dart',
34+
);
35+
final tempUri = goldenUri.resolve('pubspec_lock_syntax_temp.g.dart');
36+
final goldenFile = File.fromUri(goldenUri);
37+
final tempFile = File.fromUri(tempUri);
38+
tempFile.writeAsStringSync(output);
39+
final formatResult = Process.runSync(Platform.executable, [
40+
'format',
41+
tempFile.path,
42+
]);
43+
expect(formatResult.exitCode, equals(0));
44+
expect(
45+
tempFile.readAsStringSync().replaceAll('\r\n', '\n'),
46+
goldenFile.readAsStringSync().replaceAll('\r\n', '\n'),
47+
);
48+
tempFile.deleteSync();
49+
});
50+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Pubspec Lock File Schema",
4+
"description": "Schema for a Dart pubspec.lock file.",
5+
"$ref": "#/definitions/PubspecLockFile",
6+
"definitions": {
7+
"PubspecLockFile": {
8+
"type": "object",
9+
"required": [
10+
"sdks"
11+
],
12+
"properties": {
13+
"packages": {
14+
"type": "object",
15+
"description": "Details of the locked packages.",
16+
"additionalProperties": {
17+
"$ref": "#/definitions/Package"
18+
}
19+
},
20+
"sdks": {
21+
"$ref": "#/definitions/SDKs"
22+
}
23+
},
24+
"additionalProperties": false
25+
},
26+
"Package": {
27+
"type": "object",
28+
"required": [
29+
"dependency",
30+
"description",
31+
"source",
32+
"version"
33+
],
34+
"properties": {
35+
"dependency": {
36+
"$ref": "#/definitions/DependencyType"
37+
},
38+
"description": {
39+
"$ref": "#/definitions/PackageDescription"
40+
},
41+
"source": {
42+
"$ref": "#/definitions/PackageSource"
43+
},
44+
"version": {
45+
"$ref": "#/definitions/PackageVersion"
46+
}
47+
},
48+
"additionalProperties": false
49+
},
50+
"DependencyType": {
51+
"type": "string",
52+
"description": "The type of dependency.",
53+
"anyOf": [
54+
{
55+
"enum": [
56+
"transitive",
57+
"direct main"
58+
]
59+
},
60+
{
61+
"type": "string"
62+
}
63+
]
64+
},
65+
"PackageDescription": {
66+
"type": "object",
67+
"description": "Description of the package source."
68+
},
69+
"HostedPackageDescription": {
70+
"description": "For hosted packages.",
71+
"type": "object",
72+
"allOf": [
73+
{
74+
"$ref": "#/definitions/PackageDescription"
75+
}
76+
],
77+
"required": [
78+
"name",
79+
"sha256",
80+
"url"
81+
],
82+
"properties": {
83+
"name": {
84+
"type": "string",
85+
"description": "Name of the package."
86+
},
87+
"sha256": {
88+
"type": "string",
89+
"description": "SHA256 checksum of the package."
90+
},
91+
"url": {
92+
"type": "string",
93+
"format": "uri",
94+
"description": "URL of the package host."
95+
}
96+
},
97+
"additionalProperties": false
98+
},
99+
"GitPackageDescription": {
100+
"description": "For git packages.",
101+
"type": "object",
102+
"allOf": [
103+
{
104+
"$ref": "#/definitions/PackageDescription"
105+
}
106+
],
107+
"required": [
108+
"path",
109+
"ref",
110+
"resolved-ref",
111+
"url"
112+
],
113+
"properties": {
114+
"path": {
115+
"type": "string",
116+
"description": "Path within the git repository (if applicable)."
117+
},
118+
"ref": {
119+
"type": "string",
120+
"description": "Git reference (e.g., branch, tag, or commit hash)."
121+
},
122+
"resolved-ref": {
123+
"type": "string",
124+
"description": "Resolved git commit hash."
125+
},
126+
"url": {
127+
"type": "string",
128+
"format": "uri",
129+
"description": "URL of the git repository."
130+
}
131+
},
132+
"additionalProperties": false
133+
},
134+
"PathPackageDescription": {
135+
"description": "For path packages.",
136+
"type": "object",
137+
"allOf": [
138+
{
139+
"$ref": "#/definitions/PackageDescription"
140+
}
141+
],
142+
"required": [
143+
"path",
144+
"relative"
145+
],
146+
"properties": {
147+
"path": {
148+
"type": "string",
149+
"description": "Absolute or relative path to the package."
150+
},
151+
"relative": {
152+
"type": "boolean",
153+
"description": "Indicates if the path is relative to the lockfile."
154+
}
155+
},
156+
"additionalProperties": false
157+
},
158+
"PackageSource": {
159+
"type": "string",
160+
"description": "The source of the package.",
161+
"anyOf": [
162+
{
163+
"enum": [
164+
"hosted",
165+
"git",
166+
"path"
167+
]
168+
},
169+
{
170+
"type": "string"
171+
}
172+
]
173+
},
174+
"PackageVersion": {
175+
"type": "string",
176+
"description": "The locked version of the package."
177+
},
178+
"SDKs": {
179+
"type": "object",
180+
"description": "Details of the SDKs used.",
181+
"required": [
182+
"dart"
183+
],
184+
"properties": {
185+
"dart": {
186+
"$ref": "#/definitions/DartSDKVersion"
187+
}
188+
},
189+
"additionalProperties": false
190+
},
191+
"DartSDKVersion": {
192+
"type": "string",
193+
"description": "The Dart SDK version constraint."
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)