From d4386791fd2892a48282cfa4fb0db932abcdeb8c Mon Sep 17 00:00:00 2001 From: Alexandre Roux Date: Thu, 19 Sep 2024 11:22:46 +0200 Subject: [PATCH] feat: * add `keyPartsToString` and `keyPartsFromString` * add helper `CvModel.valueAtPath(path)` to get a model value at a given path. --- packages/cv/lib/cv.dart | 8 +- packages/cv/lib/src/cv_model.dart | 11 +-- packages/cv/lib/src/cv_model_list.dart | 4 +- packages/cv/lib/src/cv_model_mixin.dart | 112 +++++++++++++++++++----- packages/cv/lib/src/list_ext.dart | 18 ++-- packages/cv/lib/src/map_ext.dart | 69 +++++++++++---- packages/cv/test/cv_model_test.dart | 24 ++++- packages/cv/test/map_list_ext_test.dart | 11 +++ 8 files changed, 196 insertions(+), 61 deletions(-) diff --git a/packages/cv/lib/cv.dart b/packages/cv/lib/cv.dart index eb1e3aa..b03a308 100644 --- a/packages/cv/lib/cv.dart +++ b/packages/cv/lib/cv.dart @@ -45,8 +45,9 @@ export 'src/cv_model.dart' cvTypeNewModel, cvNewModel; export 'src/cv_model_list.dart' - show CvModelListExt, cvNewModelList, cvTypeNewModelList; -export 'src/cv_model_mixin.dart' show CvModelWriteExt, CvModelReadExt; + show CvModelReadListExt, cvNewModelList, cvTypeNewModelList; +export 'src/cv_model_mixin.dart' + show CvModelWriteExt, CvModelReadExt, CvModelCloneExt; export 'src/cv_tree_path.dart' show CvTreePath, @@ -57,7 +58,8 @@ export 'src/cv_tree_path.dart' CvTreePathModelListFieldExt, CvTreePathModelMapField; export 'src/list_ext.dart' show ModelRawListExt; -export 'src/map_ext.dart' show ModelRawMapExt; +export 'src/map_ext.dart' + show ModelRawMapExt, keyPartsToString, keyPartsFromString; export 'src/map_list_ext.dart' show ModelListExt; export 'src/object_ext.dart' show ModelRawObjectExt; export 'src/typedefs.dart' diff --git a/packages/cv/lib/src/cv_model.dart b/packages/cv/lib/src/cv_model.dart index 323a8a1..5379879 100644 --- a/packages/cv/lib/src/cv_model.dart +++ b/packages/cv/lib/src/cv_model.dart @@ -26,8 +26,8 @@ abstract class CvModelRead implements CvModelCore { Model toMap({List? columns, bool includeMissingValue = false}); } -/// Write helper -abstract class CvModelWrite implements CvModelCore { +/// Write helper (implies CvModelCore) +abstract class CvModelWrite implements CvModelRead { /// Map alias void fromMap(Map map, {List? columns}); @@ -42,9 +42,6 @@ abstract class CvModelCore { /// CvField access CvField? field(String name); - - /// Deep CvField access - CvField? fieldAtPath(List paths); } /// Modifiable map. @@ -59,8 +56,8 @@ abstract class CvMapModel implements CvModel, Model { } } -/// Model to access the data -abstract class CvModel implements CvModelRead, CvModelWrite, CvModelCore {} +/// Model to access the data (implies CvModelRead and CvModelCore) +abstract class CvModel implements CvModelWrite {} /// Empty model. class CvModelEmpty extends CvModelBase { diff --git a/packages/cv/lib/src/cv_model_list.dart b/packages/cv/lib/src/cv_model_list.dart index 28fdddb..cbe5705 100644 --- a/packages/cv/lib/src/cv_model_list.dart +++ b/packages/cv/lib/src/cv_model_list.dart @@ -3,6 +3,8 @@ import 'dart:collection'; import 'package:cv/cv.dart'; import 'package:cv/src/cv_model_mixin.dart'; +import 'cv_model.dart'; + /// Empty map. const cvEmptyMapList = []; @@ -15,7 +17,7 @@ List cvTypeNewModelList(Type type, {bool lazy = true}) => cvEmptyMapList.cvType(type, lazy: lazy); /// List convenient extensions. -extension CvModelListExt on List { +extension CvModelReadListExt on List { /// Convert to model list List toMapList( {List? columns, bool includeMissingValue = false}) { diff --git a/packages/cv/lib/src/cv_model_mixin.dart b/packages/cv/lib/src/cv_model_mixin.dart index 1bec2c1..1f975c2 100644 --- a/packages/cv/lib/src/cv_model_mixin.dart +++ b/packages/cv/lib/src/cv_model_mixin.dart @@ -12,21 +12,70 @@ var debugContent = false; // devWarning(true); /// Get raw value helper for map and list. CvField? rawGetFieldAtPath( - Object rawValue, List paths) { - if (paths.isEmpty) { + Object rawValue, List parts) { + if (parts.isEmpty) { if (rawValue is CvField) { return rawValue; } return null; } if (rawValue is CvModelField) { - return rawValue.v?.fieldAtPath(paths); + return rawValue.v?.fieldAtPath(parts); } else if (rawValue is CvModelListField) { - return rawValue.v?.fieldAtPath(paths); + return rawValue.v?.fieldAtPath(parts); } else if (rawValue is CvModel) { - return rawValue.fieldAtPath(paths); + return rawValue.fieldAtPath(parts); } else if (rawValue is List) { - return rawValue.fieldAtPath(paths); + return rawValue.fieldAtPath(parts); + } + return null; +} + +/// ['key1', 'key2', index3, 'key4] +T? _rawListGetValueAtPath( + List list, List parts) { + var path = parts.first; + if (path is int && path >= 0 && list.length > path) { + var rawValue = list[path]; + if (rawValue != null) { + return rawGetValueAtPath(rawValue, parts.sublist(1)); + } + } + return null; +} + +/// ['key1', 'key2', index3, 'key4] +T? _rawMapGetValueAtPath( + Map map, List parts) { + var path = parts.first; + var rawValue = map[path]; + if (rawValue != null) { + return rawGetValueAtPath(rawValue, parts.sublist(1)); + } + return null; +} + +/// Get a value at a given path - internal, handle CvField, CvModel (toMap), List (toMapList) +/// other types are returned as is for now (this might change in the future) +T? rawGetValueAtPath(Object rawValue, List parts) { + var value = (rawValue is CvField) ? rawValue.v : rawValue; + if (value == null) { + return null; + } else if (parts.isEmpty) { + if (value is CvModelRead) { + return value.toMap().anyAs(); + } else if (value is List) { + return value.toMapList().anyAs(); + } else if (value is CvModelField) { + return value.v?.toMap().anyAs(); + } + return value.anyAs(); + } else if (rawValue is List) { + return _rawListGetValueAtPath(rawValue, parts); + } else if (rawValue is Map) { + return _rawMapGetValueAtPath(rawValue, parts); + } else if (rawValue is CvModel) { + return rawValue.valueAtPath(parts); } return null; } @@ -58,19 +107,6 @@ mixin CvModelMixin implements CvModel { return _cvFieldMap![name]?.cast(); } - @override - CvField? fieldAtPath(List paths) { - var path = paths.first; - if (path is String) { - var rawField = field(path); - if (rawField?.isNotNull ?? false) { - return rawGetFieldAtPath(rawField!, paths.sublist(1)); - } - } - - return null; - } - @override int get hashCode => fields.first.hashCode; @@ -201,8 +237,10 @@ mixin CvModelMixin implements CvModel { void modelToMap(Model model, CvField field) { dynamic value = field.v; - if (value is List) { - value = value.map((e) => (e as CvModelRead).toMap()).toList(); + if (value is List) { + value = value + .map((e) => e.toMap(includeMissingValue: includeMissingValue)) + .toList(); } else if (value is CvModelRead) { value = value.toMap(includeMissingValue: includeMissingValue); } @@ -311,8 +349,8 @@ extension CvModelWriteExt on CvModelWrite { } } -/// Public extension on CvModelRead -extension CvModelReadExt on T { +/// Public extension on CvModelWrite +extension CvModelCloneExt on T { /// Copy content T clone() { var model = cvNewModel(); @@ -320,3 +358,31 @@ extension CvModelReadExt on T { return model; } } + +/// Public extension on CvModelCore +extension CvModelReadExt on CvModelRead { + /// Deep CvField access + CvField? fieldAtPath(List parts) { + var path = parts.first; + if (path is String) { + var rawField = field(path); + if (rawField?.isNotNull ?? false) { + return rawGetFieldAtPath(rawField!, parts.sublist(1)); + } + } + return null; + } + + /// Get a value at a given path + /// fields value is returned. CvModel/List are converted to map/mapList. + T? valueAtPath(List parts) { + var path = parts.first; + if (path is String) { + var rawValue = field(path)?.value; + if (rawValue != null) { + return rawGetValueAtPath(rawValue, parts.sublist(1)); + } + } + return null; + } +} diff --git a/packages/cv/lib/src/list_ext.dart b/packages/cv/lib/src/list_ext.dart index 4389185..0f23e22 100644 --- a/packages/cv/lib/src/list_ext.dart +++ b/packages/cv/lib/src/list_ext.dart @@ -7,25 +7,27 @@ import 'map_ext.dart'; /// Convenient extension on Model extension ModelRawListExt on List { /// ['key1', 'key2', index3, 'key4] - T? getKeyPathValue(List paths) { + T? getKeyPathValue(List parts) { Object? rawValue; - var path = paths.first; - if (path is int && length > path) { - rawValue = this[path]; - return rawGetKeyPathValue(rawValue, paths.sublist(1)); + var path = parts.first; + if (path is int && path >= 0 && path < length) { + rawValue = this[path] as Object?; + if (rawValue != null) { + return rawGetKeyPathValue(rawValue, parts.sublist(1)); + } } return null; } /// Handle [0, 'key2', 4, 'key4] first must be an int - CvField? fieldAtPath(List paths) { - var path = paths.first; + CvField? fieldAtPath(List parts) { + var path = parts.first; if (path is int && length > path) { var rawValue = this[path]; // i.e. not null. if (rawValue is Object) { - return rawGetFieldAtPath(rawValue, paths.sublist(1)); + return rawGetFieldAtPath(rawValue, parts.sublist(1)); } } return null; diff --git a/packages/cv/lib/src/map_ext.dart b/packages/cv/lib/src/map_ext.dart index aa77a69..7c150e2 100644 --- a/packages/cv/lib/src/map_ext.dart +++ b/packages/cv/lib/src/map_ext.dart @@ -2,17 +2,53 @@ import 'package:cv/cv.dart'; import 'package:cv/cv.dart' as cvimpl; import 'package:cv/src/typedefs.dart'; -/// Get raw value helper for map and list. -T? rawGetKeyPathValue(Object? rawValue, List paths) { - if (paths.isEmpty) { - if (rawValue is T) { - return rawValue; - } - return null; +String _keyPartToString(Object part) { + if (part is int) { + return part.toString(); + } + assert(part is String); + var partText = part as String; + + /// Look like an int + var intValue = int.tryParse(partText); + if (intValue != null) { + return '"$partText"'; + } + + return partText; +} + +Object _keyPartFromString(String partText) { + if (partText.startsWith('"') && partText.endsWith('"')) { + return partText.substring(1, partText.length - 1); + } + var intValue = int.tryParse(partText); + if (intValue is int) { + return intValue; + } + return partText; +} + +/// Convert ['key1', 'key2', index3, 'key4] to 'key1.key2.index3.key4' +/// string representing are double quoted (i.e. "1") +/// part with a dot are not supported yet... +String keyPartsToString(List parts) { + return parts.map(_keyPartToString).join('.'); +} + +/// Convert 'key1.key2.index3.key4' to ['key1', 'key2', index3, 'key4] +List keyPartsFromString(String key) { + return key.split('.').map(_keyPartFromString).toList(); +} + +/// Get raw value helper for map and list - internal +T? rawGetKeyPathValue(Object rawValue, List parts) { + if (parts.isEmpty) { + return rawValue.anyAs(); } else if (rawValue is Map) { - return rawValue.getKeyPathValue(paths); + return rawValue.getKeyPathValue(parts); } else if (rawValue is List) { - return rawValue.getKeyPathValue(paths); + return rawValue.getKeyPathValue(parts); } return null; } @@ -45,16 +81,13 @@ extension ModelRawMapExt on Map { } /// ['key1', 'key2', index3, 'key4] - T? getKeyPathValue(List paths) { - Object? rawValue; - var path = paths.first; - for (var entry in entries) { - if (entry.key == path) { - rawValue = entry.value; - return rawGetKeyPathValue(rawValue, paths.sublist(1)); - } + T? getKeyPathValue(List parts) { + var path = parts.first; + var rawValue = this[path] as Object?; + if (rawValue == null) { + return null; } - return null; + return rawGetKeyPathValue(rawValue, parts.sublist(1)); } /// Get a value expecting a given type diff --git a/packages/cv/test/cv_model_test.dart b/packages/cv/test/cv_model_test.dart index f549147..4a1c130 100644 --- a/packages/cv/test/cv_model_test.dart +++ b/packages/cv/test/cv_model_test.dart @@ -381,7 +381,7 @@ void main() { [IntContent()..value.v = 1]); }); - test('fillCvModel', () { + test('fillCvModel/fieldAtPath/valueAtPath', () { expect((IntContent()..fillModel(CvFillOptions(valueStart: 0))).toMap(), {'value': 1}); expect( @@ -425,10 +425,32 @@ void main() { }); expect( allTypes.fieldAtPath(['children', 0, 'child', 'sub'])?.v, 'text_6'); + expect(allTypes.valueAtPath(['children', 0, 'child', 'sub']), 'text_6'); + + expect(allTypes.fieldAtPath(['children', 0, 'child'])?.v, + isA()); + expect(allTypes.valueAtPath(['children', 0, 'child']), {'sub': 'text_6'}); + + expect(allTypes.fieldAtPath(['modelList']), isA()); + expect(allTypes.valueAtPath(['modelList']), [ + {'field_1': 14} + ]); + + expect(allTypes.fieldAtPath(['children']), isA()); + expect(allTypes.valueAtPath(['children']), [ + { + 'child': {'sub': 'text_6'} + } + ]); + expect(allTypes.fieldAtPath(['children', 0, 'child', 'sub'])?.v, isNull); + expect( + allTypes.valueAtPath(['children', 0, 'child', 'sub']), isNull); expect(allTypes.fieldAtPath(['children', 0, 'child', 'sub'])?.v, 'text_6'); + expect(allTypes.valueAtPath(['children', 0, 'child', 'sub']), + 'text_6'); expect( allTypes.fieldAtPath(['children', 0, 'child', 'sub_no'])?.v, isNull); diff --git a/packages/cv/test/map_list_ext_test.dart b/packages/cv/test/map_list_ext_test.dart index 17ce861..8a4490b 100644 --- a/packages/cv/test/map_list_ext_test.dart +++ b/packages/cv/test/map_list_ext_test.dart @@ -72,5 +72,16 @@ void main() { expect(newSub1, isNot(same(sub1))); expect(oldSub1, same(sub1)); }); + test('keyPartsToString', () { + expect(keyPartsToString(['test', 1, 'sub', 2]), 'test.1.sub.2'); + expect(keyPartsToString(['test', '1', 'sub', 2]), 'test."1".sub.2'); + }); + test('keyPartsFromString', () { + expect(keyPartsFromString('test.1.sub.2'), ['test', 1, 'sub', 2]); + expect( + keyPartsFromString('test."1".sub.2'), + ['test', '1', 'sub', 2], + ); + }); }); }