Skip to content

Commit

Permalink
Add support for deep collection casting in the generator (#95)
Browse files Browse the repository at this point in the history
* add support for deep casting nested collection types in the generator

* add deep cast test

* regenerate
  • Loading branch information
jakemac53 authored Oct 11, 2024
1 parent 87ce3dc commit b13df4b
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 36 deletions.
14 changes: 10 additions & 4 deletions pkgs/dart_model/lib/src/dart_model.g.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// This file is generated. To make changes edit tool/dart_model_generator
// then run from the repo root: dart tool/dart_model_generator/bin/main.dart

// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/deep_cast_map.dart';
// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/json_buffer/json_buffer_builder.dart';
// ignore: implementation_imports,unused_import,prefer_relative_imports
Expand Down Expand Up @@ -160,7 +162,8 @@ extension type Interface.fromJson(Map<String, Object?> node) implements Object {
(node['metadataAnnotations'] as List).cast();

/// Map of members by name.
Map<String, Member> get members => (node['members'] as Map).cast();
Map<String, Member> get members =>
(node['members'] as Map).cast<String, Member>();

/// The type of the expression `this` when used in this interface.
NamedTypeDesc get thisType => node['thisType'] as NamedTypeDesc;
Expand All @@ -181,7 +184,8 @@ extension type Library.fromJson(Map<String, Object?> node) implements Object {
));

/// Scopes by name.
Map<String, Interface> get scopes => (node['scopes'] as Map).cast();
Map<String, Interface> get scopes =>
(node['scopes'] as Map).cast<String, Interface>();
}

/// Member of a scope.
Expand Down Expand Up @@ -242,7 +246,8 @@ extension type Model.fromJson(Map<String, Object?> node) implements Object {
));

/// Libraries by URI.
Map<String, Library> get uris => (node['uris'] as Map).cast();
Map<String, Library> get uris =>
(node['uris'] as Map).cast<String, Library>();

/// The resolved static type hierarchy.
TypeHierarchy get types => node['types'] as TypeHierarchy;
Expand Down Expand Up @@ -630,7 +635,8 @@ extension type TypeHierarchy.fromJson(Map<String, Object?> node)
));

/// Map of qualified interface names to their resolved named type.
Map<String, TypeHierarchyEntry> get named => (node['named'] as Map).cast();
Map<String, TypeHierarchyEntry> get named =>
(node['named'] as Map).cast<String, TypeHierarchyEntry>();
}

/// Entry of an interface in Dart's type hierarchy, along with supertypes.
Expand Down
110 changes: 110 additions & 0 deletions pkgs/dart_model/lib/src/deep_cast_map.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection';

extension DeepCastMap<SK, SV> on Map<SK, SV> {
/// A lazy deep cast map for `this`, where the values are deeply cast using
/// the provided [castValue] function, and they keys are normally cast.
Map<K, V> deepCast<K, V>(V Function(SV) castValue) =>
_DeepCastMap(this, castValue);
}

/// Like a `CastMap`, except it can perform deep casts on values using a
/// provided conversion function.
class _DeepCastMap<SK, SV, K, V> extends MapBase<K, V> {
final Map<SK, SV> _source;

final V Function(SV) _castValue;

_DeepCastMap(this._source, this._castValue);

@override
bool containsValue(Object? value) => _source.containsValue(value);

@override
bool containsKey(Object? key) => _source.containsKey(key);

@override
V? operator [](Object? key) {
final value = _source[key];
if (value == null) return null;
return _castValue(value);
}

@override
void operator []=(K key, V value) {
_source[key as SK] = value as SV;
}

@override
V putIfAbsent(K key, V Function() ifAbsent) =>
_castValue(_source.putIfAbsent(key as SK, () => ifAbsent() as SV));

@override
V? remove(Object? key) {
final removed = _source.remove(key);
if (removed == null) return null;
return _castValue(removed);
}

@override
void clear() {
_source.clear();
}

@override
void forEach(void Function(K key, V value) f) {
_source.forEach((SK key, SV value) {
f(key as K, _castValue(value));
});
}

@override
Iterable<K> get keys => _source.keys.cast();

@override
Iterable<V> get values => _source.values.map(_castValue);

@override
int get length => _source.length;

@override
bool get isEmpty => _source.isEmpty;

@override
bool get isNotEmpty => _source.isNotEmpty;

@override
V update(K key, V Function(V value) update, {V Function()? ifAbsent}) {
return _castValue(_source.update(
key as SK, (SV value) => update(_castValue(value)) as SV,
ifAbsent: (ifAbsent == null) ? null : () => ifAbsent() as SV));
}

@override
void updateAll(V Function(K key, V value) update) {
_source.updateAll(
(SK key, SV value) => update(key as K, _castValue(value)) as SV);
}

@override
Iterable<MapEntry<K, V>> get entries {
return _source.entries.map<MapEntry<K, V>>((MapEntry<SK, SV> e) =>
MapEntry<K, V>(e.key as K, _castValue(e.value)));
}

@override
void addEntries(Iterable<MapEntry<K, V>> entries) {
for (var entry in entries) {
_source[entry.key as SK] = entry.value as SV;
}
}

@override
void removeWhere(bool Function(K key, V value) test) {
_source
.removeWhere((SK key, SV value) => test(key as K, _castValue(value)));
}
}
42 changes: 42 additions & 0 deletions pkgs/dart_model/test/deep_cast_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:dart_model/src/deep_cast_map.dart';
import 'package:test/test.dart';

void main() {
test('can perform deep casts on maps', () {
final initial = <dynamic, dynamic>{
'x': <dynamic>[1, 2, 3]
};
expect(initial, isNot(isA<Map<String, List<int>>>()));
final typed =
initial.deepCast<String, List<int>>((v) => (v as List).cast<int>());
expect(typed, isA<Map<String, List<int>>>());
expect(typed['x']!, isA<List<int>>());
expect(typed['x']!, [1, 2, 3]);
});

test('can perform really deep casts on maps', () {
final initial = <dynamic, dynamic>{
'x': <dynamic, dynamic>{
'y': <dynamic>[1, 2, 3]
},
};
expect(initial, isNot(isA<Map<String, Map<String, List<int>>>>()));

final typed = initial.deepCast<String, Map<String, List<int>>>((v) =>
(v as Map).deepCast<String, List<int>>((v) => (v as List).cast()));
expect(typed, isA<Map<String, Map<String, List<int>>>>());

expect(initial['x'], isNot(isA<Map<String, List<int>>>()));
final x = typed['x']!;
expect(x, isA<Map<String, List<int>>>());

expect((initial['x'] as Map)['y'], isNot(isA<List<int>>()));
final y = x['y']!;
expect(y, isA<List<int>>());
expect(y, [1, 2, 3]);
});
}
2 changes: 2 additions & 0 deletions pkgs/macro_service/lib/src/handshake.g.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// This file is generated. To make changes edit tool/dart_model_generator
// then run from the repo root: dart tool/dart_model_generator/bin/main.dart

// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/deep_cast_map.dart';
// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/json_buffer/json_buffer_builder.dart';
// ignore: implementation_imports,unused_import,prefer_relative_imports
Expand Down
2 changes: 2 additions & 0 deletions pkgs/macro_service/lib/src/macro_service.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/dart_model.g.dart';
// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/deep_cast_map.dart';
// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/json_buffer/json_buffer_builder.dart';
// ignore: implementation_imports,unused_import,prefer_relative_imports
import 'package:dart_model/src/scopes.dart';
Expand Down
80 changes: 48 additions & 32 deletions tool/dart_model_generator/lib/generate_dart_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class GenerationContext {
}.nonNulls;
return ([
"import 'package:dart_model/src/json_buffer/json_buffer_builder.dart';",
"import 'package:dart_model/src/deep_cast_map.dart';",
"import 'package:dart_model/src/scopes.dart';",
for (final schema in schemas)
if (schema != currentSchema)
Expand Down Expand Up @@ -275,19 +276,8 @@ class Property {

/// Dart code for a getter for this property.
String get getterCode {
final nullAwareCast = nullable ? '?.cast()' : '.cast()';

if (type.isMap) {
final representationType = nullable ? 'Map?' : 'Map';
return _describe('${_type()} get $name => '
"(node['$name'] as $representationType)$nullAwareCast;");
} else if (type.isList) {
final representationType = nullable ? 'List?' : 'List';
return _describe('${_type()} get $name => '
"(node['$name'] as $representationType)$nullAwareCast;");
} else {
return _describe("${_type()} get $name => node['$name'] as ${_type()};");
}
return _describe('${_type()} get $name => '
'${type.castExpression("node['$name']", nullable: nullable)};');
}

/// Dart code for entry in `TypedMapSchema`.
Expand All @@ -301,13 +291,6 @@ class Property {
String _type({bool nullable = false}) {
return '${type.dartType}${(nullable || this.nullable) ? '?' : ''}';
}

/// The names of all types referenced by this property.
Set<String> get allTypeNames {
if (type.isMap) return {'Map', type.elementType!};
if (type.isList) return {'List', type.elementType!};
return {type.name};
}
}

/// A reference to a type.
Expand All @@ -319,13 +302,13 @@ class Property {
/// has one parameter because keys are always `String` in JSON.
class TypeReference {
static final RegExp _simpleRegexp = RegExp(r'^[A-Za-z]+$');
static final RegExp _mapRegexp = RegExp(r'^Map<([A-Za-z]+)>$');
static final RegExp _listRegexp = RegExp(r'^List<([A-Za-z]+)>$');
static final RegExp _mapRegexp = RegExp(r'^Map<([A-Za-z<>]+)>$');
static final RegExp _listRegexp = RegExp(r'^List<([A-Za-z<>]+)>$');

String name;
late final bool isMap;
late final bool isList;
late final String? elementType;
late final TypeReference? elementType;

TypeReference(this.name) {
if (_simpleRegexp.hasMatch(name)) {
Expand All @@ -335,19 +318,52 @@ class TypeReference {
} else if (_mapRegexp.hasMatch(name)) {
isMap = true;
isList = false;
elementType = _mapRegexp.firstMatch(name)!.group(1);
elementType = TypeReference(_mapRegexp.firstMatch(name)!.group(1)!);
} else if (_listRegexp.hasMatch(name)) {
isMap = false;
isList = true;
elementType = _listRegexp.firstMatch(name)!.group(1);
elementType = TypeReference(_listRegexp.firstMatch(name)!.group(1)!);
} else {
throw ArgumentError('Invalid type name: $name');
}
}

/// The names of all types referenced by this type.
Set<String> get allTypeNames {
return {name, ...?elementType?.allTypeNames};
}

/// Returns a piece of code which will cast an expression represented by
/// [value] to the type of `this`.
///
/// If [nullable] is true, it will handle the [value] expression being
/// nullable, otherwise it won't.
String castExpression(String value, {bool nullable = false}) {
final q = nullable ? '?' : '';
final representationName = isMap
? 'Map'
: isList
? 'List'
: name;
final rawCast = '$value as $representationName$q';
if (isMap) {
if (elementType!.elementType == null) {
return '($rawCast)$q.cast<String, ${elementType!.dartType}>()';
}
return '($rawCast)$q.deepCast<String, ${elementType!.dartType}>('
'(v) => ${elementType!.castExpression('v')})';
} else if (isList) {
if (elementType!.elementType == null) return '($rawCast).cast()';
throw UnsupportedError('Deep casting for lists isn\'t yet supported.');
} else {
return rawCast;
}
}

/// The Dart type name of this type.
String get dartType {
if (isMap) return 'Map<String, $elementType>';
if (isMap) return 'Map<String, ${elementType!.dartType}>';
if (isList) return 'List<${elementType!.dartType}>';
return name;
}

Expand Down Expand Up @@ -382,14 +398,13 @@ class TypeReference {
return {
'type': 'array',
if (description != null) 'description': description,
'items': TypeReference(elementType!).generateSchema(context),
'items': elementType!.generateSchema(context),
};
} else if (isMap) {
return {
'type': 'object',
if (description != null) 'description': description,
'additionalProperties':
TypeReference(elementType!).generateSchema(context),
'additionalProperties': elementType!.generateSchema(context),
};
} else if (name == 'String') {
return {
Expand Down Expand Up @@ -466,6 +481,7 @@ class ClassTypeDefinition implements Definition {
result.writeln(' $name() : ');
} else {
result.writeln(' $name({');
// TODO: Why are we excluding Map properties?
for (final property in propertiesExceptMap) {
result.writeln(property.parameterCode);
}
Expand All @@ -487,7 +503,7 @@ class ClassTypeDefinition implements Definition {
result.writeln('{');
for (final property in properties) {
if (property.type.isMap) {
result.writeln('${property.name}: {},');
result.writeln("'${property.name}': {},");
} else {
result.writeln(property.namedArgumentCode);
}
Expand All @@ -509,7 +525,7 @@ class ClassTypeDefinition implements Definition {

@override
Set<String> get allTypeNames =>
properties.expand((f) => f.allTypeNames).toSet();
properties.expand((f) => f.type.allTypeNames).toSet();

@override
String get representationTypeName => 'Map<String, Object?>';
Expand Down Expand Up @@ -669,7 +685,7 @@ class UnionTypeDefinition implements Definition {

@override
Set<String> get allTypeNames =>
{...types, ...properties.expand((f) => f.allTypeNames)};
{...types, ...properties.expand((f) => f.type.allTypeNames)};

@override
String get representationTypeName => 'Map<String, Object?>';
Expand Down

0 comments on commit b13df4b

Please sign in to comment.