Skip to content

Commit da90db5

Browse files
authored
chore: add openapi and info required fields (#98)
* chore: add openapi and info required fields~ * test: add test for schema/content errors * test: info.version * test: wrong type * test: fix optional test * test: empty paths
1 parent 25a4659 commit da90db5

9 files changed

Lines changed: 371 additions & 104 deletions

File tree

packages/gen/lib/src/context.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ class Api {
4242
String get fileName => '${name.toLowerCase()}_api';
4343
}
4444

45-
extension SpecGeneration on Spec {
45+
extension OpenApiGeneration on OpenApi {
4646
List<Api> get apis => tags
47+
.sorted()
4748
.map(
4849
(tag) => Api(
4950
name: tag,
@@ -681,7 +682,7 @@ class _Context {
681682
final Uri specUrl;
682683

683684
/// The spec being rendered.
684-
final Spec spec;
685+
final OpenApi spec;
685686

686687
/// The output directory.
687688
final Directory outDir;
@@ -939,7 +940,7 @@ void renderSpec({
939940
required Uri specUri,
940941
required String packageName,
941942
required Directory outDir,
942-
required Spec spec,
943+
required OpenApi spec,
943944
required RefRegistry refRegistry,
944945
Directory? templateDir,
945946
RunProcess? runProcess,

packages/gen/lib/src/parser.dart

Lines changed: 125 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,83 @@ import 'package:space_gen/src/logger.dart';
55
import 'package:space_gen/src/spec.dart';
66
import 'package:space_gen/src/string.dart';
77

8+
T _required<T>(ParseContext context, Json json, String key) {
9+
final value = json[key];
10+
if (value == null) {
11+
throw FormatException(
12+
'Required key not found: $key in ${context.pointer}: $json',
13+
);
14+
}
15+
return value as T;
16+
}
17+
18+
void _expect(bool condition, ParseContext context, Json json, String message) {
19+
if (!condition) {
20+
throw FormatException('$message in ${context.pointer}');
21+
}
22+
}
23+
24+
T? _optional<T>(ParseContext context, Json json, String key) {
25+
final value = json[key];
26+
if (value is T?) {
27+
return value;
28+
}
29+
throw FormatException(
30+
'Key $key is not of type $T: $value (in ${context.pointer})',
31+
);
32+
}
33+
34+
// void _unimplemented(Json json, String key) {
35+
// final value = json[key];
36+
// if (value != null) {
37+
// throw UnimplementedError('Unsupported key: $key in $json');
38+
// }
39+
// }
40+
41+
void _ignored(ParseContext context, Json json, String key) {
42+
final value = json[key];
43+
if (value != null) {
44+
logger.detail('Ignoring key: $key in ${context.pointer}');
45+
}
46+
}
47+
48+
void _warn(ParseContext context, Json json, String message) {
49+
logger.warn('$message in ${context.pointer}');
50+
}
51+
52+
void _error(ParseContext context, Json json, String message) {
53+
throw FormatException('$message in ${context.pointer}');
54+
}
55+
856
/// Parse a parameter from a json object.
957
Parameter parseParameter({required Json json, required ParseContext context}) {
10-
final schema = _optional<Json>(json, 'schema');
58+
final schema = _optional<Json>(context, json, 'schema');
1159
final hasSchema = schema != null;
12-
final hasContent = _optional<Json>(json, 'content') != null;
60+
final hasContent = _optional<Json>(context, json, 'content') != null;
1361

1462
// Common fields.
15-
final name = _required<String>(json, 'name');
16-
final description = _optional<String>(json, 'description');
17-
final required = _optional<bool>(json, 'required') ?? false;
18-
final sendIn = SendIn.fromJson(_required<String>(json, 'in'));
19-
_ignored(json, 'deprecated');
20-
_ignored(json, 'allowEmptyValue');
63+
final name = _required<String>(context, json, 'name');
64+
final description = _optional<String>(context, json, 'description');
65+
final required = _optional<bool>(context, json, 'required') ?? false;
66+
final sendIn = SendIn.fromJson(_required<String>(context, json, 'in'));
67+
_ignored(context, json, 'deprecated');
68+
_ignored(context, json, 'allowEmptyValue');
2169

2270
final SchemaRef type;
2371
if (hasSchema) {
2472
if (hasContent) {
25-
_error(json, 'Parameter cannot have both schema and content');
73+
_error(context, json, 'Parameter cannot have both schema and content');
2674
}
2775
// Schema fields.
2876
type = parseSchemaOrRef(json: schema, context: context.key('schema'));
29-
_ignored(json, 'style');
30-
_ignored(json, 'explode');
31-
_ignored(json, 'allowReserved');
32-
_ignored(json, 'example');
33-
_ignored(json, 'examples');
77+
_ignored(context, json, 'style');
78+
_ignored(context, json, 'explode');
79+
_ignored(context, json, 'allowReserved');
80+
_ignored(context, json, 'example');
81+
_ignored(context, json, 'examples');
3482
} else {
3583
if (!hasSchema && !hasContent) {
36-
_error(json, 'Parameter must have either schema or content');
84+
_error(context, json, 'Parameter must have either schema or content');
3785
}
3886
// Content fields.
3987
// Use an explicit throw so Dart can see `type` is always set.
@@ -78,16 +126,16 @@ Schema parseSchema(Json json, ParseContext context) {
78126
);
79127
}
80128

81-
_ignored(json, 'nullable');
82-
_ignored(json, 'readOnly');
83-
_ignored(json, 'writeOnly');
84-
_ignored(json, 'discriminator');
85-
_ignored(json, 'xml');
86-
_ignored(json, 'example');
87-
_ignored(json, 'examples');
88-
_ignored(json, 'externalDocs');
129+
_ignored(context, json, 'nullable');
130+
_ignored(context, json, 'readOnly');
131+
_ignored(context, json, 'writeOnly');
132+
_ignored(context, json, 'discriminator');
133+
_ignored(context, json, 'xml');
134+
_ignored(context, json, 'example');
135+
_ignored(context, json, 'examples');
136+
_ignored(context, json, 'externalDocs');
89137

90-
final defaultValue = _optional<dynamic>(json, 'default');
138+
final defaultValue = _optional<dynamic>(context, json, 'default');
91139

92140
final required = json['required'] as List<dynamic>? ?? [];
93141
final description = json['description'] as String? ?? '';
@@ -238,13 +286,13 @@ Map<String, SchemaRef> parseProperties({
238286
}
239287

240288
RequestBody parseRequestBody(Json requestBodyJson, ParseContext context) {
241-
final content = _required<Json>(requestBodyJson, 'content');
242-
final applicationJson = _required<Json>(content, 'application/json');
289+
final content = _required<Json>(context, requestBodyJson, 'content');
290+
final applicationJson = _required<Json>(context, content, 'application/json');
243291
final schema = parseSchemaOrRef(
244-
json: _required<Json>(applicationJson, 'schema'),
292+
json: _required<Json>(context, applicationJson, 'schema'),
245293
context: context.addSnakeName('request').key('requestBody'),
246294
);
247-
_ignored(requestBodyJson, 'description');
295+
_ignored(context, requestBodyJson, 'description');
248296

249297
final isRequired = requestBodyJson['required'] as bool? ?? false;
250298
final body = RequestBody(
@@ -270,12 +318,13 @@ Endpoint parseEndpoint({
270318
final context = parentContext.addSnakeName(snakeName);
271319

272320
final responses = parseResponses(
273-
_optional<Json>(json, 'responses'),
321+
_optional<Json>(context, json, 'responses'),
274322
context.key('responses'),
275323
);
276-
final tags = _optional<List<dynamic>>(json, 'tags');
324+
final tags = _optional<List<dynamic>>(context, json, 'tags');
277325
final tag = tags?.firstOrNull as String? ?? 'Default';
278-
final parametersJson = _optional<List<dynamic>>(json, 'parameters') ?? [];
326+
final parametersJson =
327+
_optional<List<dynamic>>(context, json, 'parameters') ?? [];
279328
final parameters = parametersJson
280329
.cast<Json>()
281330
.indexed
@@ -407,17 +456,48 @@ Components parseComponents(Json? json, ParseContext context) {
407456
return Components(schemas: schemas, requestBodies: requestBodies);
408457
}
409458

410-
Spec parseSpec(Json json, ParseContext context) {
411-
final servers = _required<List<dynamic>>(json, 'servers');
459+
Info parseInfo(Json json, ParseContext context) {
460+
final title = _required<String>(context, json, 'title');
461+
final version = _required<String>(context, json, 'version');
462+
_ignored(context, json, 'summary');
463+
_ignored(context, json, 'description');
464+
_ignored(context, json, 'termsOfService');
465+
_ignored(context, json, 'contact');
466+
_ignored(context, json, 'license');
467+
return Info(title, version);
468+
}
469+
470+
OpenApi parseOpenApi(Json json, ParseContext context) {
471+
final minimumVersion = Version.parse('3.1.0');
472+
final versionString = _required<String>(context, json, 'openapi');
473+
final version = Version.parse(versionString);
474+
if (version < minimumVersion) {
475+
_warn(
476+
context,
477+
json,
478+
'$version may not be supported, only tested with 3.1.0',
479+
);
480+
}
481+
482+
final infoJson = _required<Json>(context, json, 'info');
483+
final info = parseInfo(infoJson, context.key('info'));
484+
485+
final servers = _required<List<dynamic>>(context, json, 'servers');
412486
final firstServer = servers.first as Json;
413-
final serverUrl = _required<String>(firstServer, 'url');
487+
final serverUrl = _required<String>(context, firstServer, 'url');
414488

415-
final paths = _required<Json>(json, 'paths');
489+
final paths = _required<Json>(context, json, 'paths');
416490
final endpoints = <Endpoint>[];
417491
for (final pathEntry in paths.entries) {
418492
final path = pathEntry.key;
419-
_expect(path.isNotEmpty, json, 'Path cannot be empty');
420-
_expect(path.startsWith('/'), json, 'Path must start with /: $path');
493+
final pathContext = context.key('paths').key(path);
494+
_expect(path.isNotEmpty, pathContext, json, 'Path cannot be empty');
495+
_expect(
496+
path.startsWith('/'),
497+
pathContext,
498+
json,
499+
'Path must start with /: $path',
500+
);
421501
final pathValue = pathEntry.value as Json;
422502
for (final method in Method.values) {
423503
final methodValue = pathValue[method.key] as Json?;
@@ -426,7 +506,7 @@ Spec parseSpec(Json json, ParseContext context) {
426506
}
427507
endpoints.add(
428508
parseEndpoint(
429-
parentContext: context.key('paths').key(path).key(method.key),
509+
parentContext: pathContext.key(method.key),
430510
path: path,
431511
json: methodValue,
432512
method: method,
@@ -438,47 +518,13 @@ Spec parseSpec(Json json, ParseContext context) {
438518
json['components'] as Json?,
439519
context.key('components'),
440520
);
441-
return Spec(Uri.parse(serverUrl), endpoints, components);
442-
}
443-
444-
T _required<T>(Json json, String key) {
445-
final value = json[key];
446-
if (value == null) {
447-
throw FormatException('Required key not found: $key in $json');
448-
}
449-
return value as T;
450-
}
451-
452-
void _expect(bool condition, Json json, String message) {
453-
if (!condition) {
454-
throw FormatException('$message in $json');
455-
}
456-
}
457-
458-
T? _optional<T>(Json json, String key) {
459-
final value = json[key];
460-
if (value is T?) {
461-
return value;
462-
}
463-
throw FormatException('Key $key is not of type $T: $value (from $json)');
464-
}
465-
466-
// void _unimplemented(Json json, String key) {
467-
// final value = json[key];
468-
// if (value != null) {
469-
// throw UnimplementedError('Unsupported key: $key in $json');
470-
// }
471-
// }
472-
473-
void _ignored(Json json, String key) {
474-
final value = json[key];
475-
if (value != null) {
476-
logger.detail('Ignoring key: $key in $json');
477-
}
478-
}
479-
480-
void _error(Json json, String message) {
481-
throw FormatException('$message in $json');
521+
return OpenApi(
522+
serverUrl: Uri.parse(serverUrl),
523+
version: version,
524+
info: info,
525+
endpoints: endpoints,
526+
components: components,
527+
);
482528
}
483529

484530
class RefRegistry {

packages/gen/lib/src/render.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:collection/collection.dart';
12
import 'package:file/file.dart';
23
import 'package:space_gen/src/context.dart';
34
import 'package:space_gen/src/loader.dart';
@@ -6,15 +7,15 @@ import 'package:space_gen/src/parser.dart';
67
import 'package:space_gen/src/spec.dart';
78
import 'package:space_gen/src/visitor.dart';
89

9-
void _printSpecStats(ParseContext parseContext, Spec spec) {
10+
void _printSpecStats(ParseContext parseContext, OpenApi spec) {
1011
logger.detail('Registered schemas:');
1112
for (final uri in parseContext.refRegistry.uris) {
1213
logger.detail(' - $uri');
1314
}
1415

1516
// Print stats about the spec.
1617
logger.detail('Spec:');
17-
for (final api in spec.tags) {
18+
for (final api in spec.tags.sorted()) {
1819
logger.detail(' - $api');
1920
final endpoints = spec.endpoints.where((e) => e.tag == api);
2021
for (final endpoint in endpoints) {
@@ -37,7 +38,7 @@ Future<void> loadAndRenderSpec({
3738
final cache = Cache(fs);
3839
final parseContext = ParseContext.initial(specUri);
3940
final specJson = await cache.load(specUri);
40-
final spec = parseSpec(specJson, parseContext);
41+
final spec = parseOpenApi(specJson, parseContext);
4142
_printSpecStats(parseContext, spec);
4243

4344
// Pre-warm the cache. Rendering assumes all refs are present in the cache.

0 commit comments

Comments
 (0)