├── .gitignore ├── .idea ├── .gitignore ├── fson.iml ├── libraries │ ├── Dart_Packages.xml │ └── Dart_SDK.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── dson_adapter.dart └── src │ ├── dson.dart │ ├── errors │ └── dson_exception.dart │ ├── extensions │ └── iterable_extension.dart │ └── param.dart ├── pubspec.yaml └── test └── src ├── dson_base_test.dart └── extensions └── iterable_extension_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build outputs. 6 | build/ 7 | 8 | # Omit committing pubspec.lock for library packages; see 9 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 10 | pubspec.lock 11 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/fson.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_Packages.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "fson", 9 | "request": "launch", 10 | "type": "dart" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.1 - 2023-004-11 2 | 3 | - Add new error types : 4 | [ParamUnknown, ParamNullNotAllowed, ParamInvalidType] 5 | - Turn public the [FunctionParam] 6 | - Updates for new sdk version 7 | 8 | ## 1.2.0+2 - 2023-004-11 9 | 10 | - Added Aliases propertie; 11 | - Fixed String List 12 | - Added Dart Docs 13 | - Fix Lints 14 | 15 | ## 1.0.0 16 | 17 | - Initial version. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | License Copyright: Flutterando. 3 | License License: Flutterando. 4 | License Contact: Flutterando. 5 | SPDX short identifier: MIT 6 | Further resources... 7 |   8 | Begin license text. 9 | Copyright 2023 Flutterando 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | End license text. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DSON 2 | 3 | Convert JSON to Dart Class withless code generate(build_runner). 4 | 5 | ## A simple Object 6 | 7 | ```dart 8 | class Person { 9 | final int id; 10 | final String name; 11 | final int age; 12 | 13 | Person({ 14 | required this.id, 15 | required this.name, 16 | required this.age, 17 | }); 18 | } 19 | ``` 20 | 21 | Convert json to Object: 22 | 23 | ```dart 24 | main(){ 25 | final jsonMap = { 26 | 'id': 1, 27 | 'name': 'Joshua Clak', 28 | 'age': 3, 29 | }; 30 | 31 | Person person = dson.fromJson(jsonMap, Person.new); 32 | 33 | print(person.id); 34 | print(person.name); 35 | print(person.age); 36 | } 37 | 38 | 39 | ``` 40 | 41 | ## A complex object: 42 | 43 | For complex objects it is necessary to declare the constructor in the `inner` property; 44 | 45 | ```dart 46 | main(){ 47 | final jsonMap = { 48 | 'id': 1, 49 | 'name': 'MyHome', 50 | 'owner': { 51 | 'id': 1, 52 | 'name': 'Joshua Clak', 53 | 'age': 3, 54 | }, 55 | }; 56 | 57 | Person person = dson.fromJson( 58 | jsonMap, 59 | Person.new, 60 | inner: { 61 | 'owner': Person.new, 62 | } 63 | ); 64 | 65 | print(person); 66 | } 67 | 68 | ``` 69 | 70 | ## A complex object with List: 71 | 72 | For work with a list, it is necessary to declare the constructor in the `inner` property and declare 73 | the list resolver in the `resolvers` property. 74 | 75 | ```dart 76 | main(){ 77 | final jsonMap = { 78 | 'id': 1, 79 | 'name': 'MyHome', 80 | 'owner': { 81 | 'id': 1, 82 | 'name': 'Joshua Clak', 83 | 'age': 3, 84 | }, 85 | 'parents': [ 86 | { 87 | 'id': 2, 88 | 'name': 'Kepper Vidal', 89 | 'age': 25, 90 | }, 91 | { 92 | 'id': 3, 93 | 'name': 'Douglas Bisserra', 94 | 'age': 23, 95 | }, 96 | ], 97 | }; 98 | 99 | Home home = dson.fromJson( 100 | // json Map or List 101 | jsonMap, 102 | // Main constructor 103 | Home.new, 104 | // external types 105 | inner: { 106 | 'owner': Person.new, 107 | 'parents': ListParam(Person.new), 108 | }, 109 | ); 110 | 111 | print(home); 112 | } 113 | 114 | ``` 115 | 116 | DSON Have `ListParam` and `SetParam` for collection. 117 | 118 | ## When API replace Param Name (Set aliases): 119 | 120 | You need to declare within the aliases map the object type that has changed in the key, and in the value, a map with the old key as the key and the new key as the value. 121 | 122 | ```dart 123 | main(){ 124 | final jsonMap = { 125 | 'id': 1, 126 | 'name': 'MyHome', 127 | 'master': { 128 | 'key': 1, 129 | 'name': 'Joshua Clak', 130 | 'age': 3, 131 | }, 132 | 'parents': [ 133 | { 134 | 'key': 2, 135 | 'name': 'Kepper Vidal', 136 | 'age': 25, 137 | }, 138 | { 139 | 'key': 3, 140 | 'name': 'Douglas Bisserra', 141 | 'age': 23, 142 | }, 143 | ], 144 | }; 145 | 146 | Home home = dson.fromJson( 147 | // json Map or List 148 | jsonMap, 149 | // Main constructor 150 | Home.new, 151 | // external types 152 | inner: { 153 | 'owner': Person.new, 154 | 'parents': ListParam(Person.new), 155 | }, 156 | // Param names Object <-> Param name in API 157 | aliases: { 158 | Home: {'owner': 'master'}, 159 | Person: {'id': 'key'} 160 | } 161 | ); 162 | 163 | print(home); 164 | } 165 | 166 | ``` 167 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:flutterando_analysis/dart_package.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:dson_adapter/dson_adapter.dart'; 4 | 5 | void main() { 6 | final jsondata = { 7 | 'id': 1, 8 | 'name': 'Jacob', 9 | 'age': 1, 10 | }; 11 | 12 | final person = const DSON().fromJson(jsondata, Person.new); 13 | print(person.age); 14 | } 15 | 16 | class Person { 17 | final int id; 18 | final String? name; 19 | final int age; 20 | 21 | Person({ 22 | required this.id, 23 | this.name, 24 | this.age = 20, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /lib/dson_adapter.dart: -------------------------------------------------------------------------------- 1 | library dson_adapter; 2 | 3 | export 'src/dson.dart'; 4 | export 'src/errors/dson_exception.dart'; 5 | export 'src/param.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/dson.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_catching_errors 2 | 3 | import '../dson_adapter.dart'; 4 | 5 | /// Function to transform the value of an object based on its key 6 | typedef ResolverCallback = Object Function(String key, dynamic value); 7 | 8 | /// Convert JSON to Dart Class withless code generate(build_runner) 9 | class DSON { 10 | /// Convert JSON to Dart Class withless code generate(build_runner) 11 | const DSON(); 12 | 13 | /// 14 | /// For complex objects it is necessary to declare the constructor in 15 | /// the [inner] property and declare the list resolver in the [resolvers] 16 | /// property. 17 | /// 18 | /// The [aliases] parameter can be used to create alias to specify the name 19 | /// of a field when it is deserialized. 20 | /// 21 | /// For example: 22 | /// ```dart 23 | /// Home home = dson.fromJson( 24 | /// // json Map or List 25 | /// jsonMap, 26 | /// // Main constructor 27 | /// Home.new, 28 | /// // external types 29 | /// inner: { 30 | /// 'owner': Person.new, 31 | /// 'parents': ListParam(Person.new), 32 | /// }, 33 | /// // Param names Object <-> Param name in API 34 | /// aliases: { 35 | /// Home: {'owner': 'master'}, 36 | /// Person: {'id': 'key'} 37 | /// } 38 | /// ); 39 | /// ``` 40 | 41 | /// 42 | /// For more information, see the 43 | /// [documentation](https://pub.dev/documentation/dson_adapter/latest/). 44 | T fromJson( 45 | dynamic map, 46 | Function mainConstructor, { 47 | Map inner = const {}, 48 | List resolvers = const [], 49 | Map> aliases = const {}, 50 | }) { 51 | final mainConstructorNamed = mainConstructor.runtimeType.toString(); 52 | final aliasesWithTypeInString = 53 | aliases.map((key, value) => MapEntry(key.toString(), value)); 54 | final hasOnlyNamedParams = 55 | RegExp(r'\(\{(.+)\}\)').firstMatch(mainConstructorNamed); 56 | final parentClass = mainConstructorNamed.split(' => ').last; 57 | if (hasOnlyNamedParams == null) { 58 | throw ParamsNotAllowed('$parentClass must have named params only!'); 59 | } 60 | 61 | final regExp = _namedParamsRegExMatch(parentClass, mainConstructorNamed); 62 | final functionParams = 63 | _parseFunctionParams(regExp, aliasesWithTypeInString[parentClass]); 64 | 65 | try { 66 | final mapEntryParams = functionParams 67 | .map( 68 | (functionParam) { 69 | dynamic value; 70 | 71 | final hasSubscriptOperator = 72 | map is Map || map is List || map is Set; 73 | 74 | if (!hasSubscriptOperator) { 75 | throw ParamInvalidType.notIterable( 76 | functionParam: functionParam, 77 | receivedType: map.runtimeType.toString(), 78 | parentClass: parentClass, 79 | stackTrace: StackTrace.current, 80 | ); 81 | } 82 | 83 | final workflow = map[functionParam.aliasOrName]; 84 | 85 | if (workflow is Map || workflow is List || workflow is Set) { 86 | final innerParam = inner[functionParam.name]; 87 | 88 | if (innerParam is IParam) { 89 | value = innerParam.call( 90 | this, 91 | workflow, 92 | inner, 93 | resolvers, 94 | aliases, 95 | ); 96 | } else if (innerParam is Function) { 97 | value = fromJson( 98 | workflow, 99 | innerParam, 100 | resolvers: resolvers, 101 | aliases: aliases, 102 | ); 103 | } else { 104 | value = workflow; 105 | } 106 | } else { 107 | value = workflow; 108 | } 109 | 110 | value = resolvers.fold( 111 | value, 112 | (previousValue, element) => 113 | element(functionParam.name, previousValue), 114 | ); 115 | 116 | if (value == null) { 117 | if (!functionParam.isRequired) return null; 118 | if (!functionParam.isNullable) { 119 | throw ParamNullNotAllowed( 120 | functionParam: functionParam, 121 | parentClass: parentClass, 122 | stackTrace: StackTrace.current, 123 | ); 124 | } 125 | 126 | final entry = MapEntry(Symbol(functionParam.name), null); 127 | return entry; 128 | } 129 | 130 | final entry = MapEntry(Symbol(functionParam.name), value); 131 | return entry; 132 | }, 133 | ) 134 | .where((entry) => entry != null) 135 | .cast>() 136 | .toList(); 137 | 138 | final namedParams = {}..addEntries(mapEntryParams); 139 | 140 | return Function.apply(mainConstructor, [], namedParams); 141 | } on TypeError catch (error, stackTrace) { 142 | throw ParamInvalidType.typeError( 143 | error: error, 144 | stackTrace: stackTrace, 145 | functionParams: functionParams, 146 | parentClass: parentClass, 147 | ); 148 | } 149 | } 150 | 151 | RegExpMatch _namedParamsRegExMatch( 152 | String parentClass, 153 | String mainConstructorNamed, 154 | ) { 155 | final result = RegExp(r'\(\{(.+)\}\)').firstMatch(mainConstructorNamed); 156 | 157 | if (result == null) { 158 | throw ParamsNotAllowed('$parentClass must have named params only!'); 159 | } 160 | 161 | return result; 162 | } 163 | 164 | Iterable _parseFunctionParams( 165 | RegExpMatch regExp, 166 | Map? aliases, 167 | ) { 168 | return regExp.group(1)!.split(',').map((e) => e.trim()).map( 169 | (element) => FunctionParam.fromString(element) 170 | .copyWith(alias: aliases?[element.split(' ').last]), 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/src/errors/dson_exception.dart: -------------------------------------------------------------------------------- 1 | import '../../dson_adapter.dart'; 2 | import '../extensions/iterable_extension.dart'; 3 | 4 | /// Exception from DSON 5 | class DSONException implements Exception { 6 | /// Message for exception 7 | final String message; 8 | 9 | /// stackTrace for exception 10 | final StackTrace? stackTrace; 11 | 12 | /// Exception from DSON 13 | DSONException(this.message, [this.stackTrace]); 14 | 15 | String get _className => '[$DSONException]'; 16 | 17 | @override 18 | String toString() { 19 | var message = '$_className: ${this.message}'; 20 | if (stackTrace != null) { 21 | message = '$message\n\n$stackTrace'; 22 | } 23 | 24 | return message; 25 | } 26 | } 27 | 28 | /// Called when params is not allowed 29 | class ParamsNotAllowed extends DSONException { 30 | /// Called when params is not allowed 31 | ParamsNotAllowed(super.message, [super.stackTrace]); 32 | 33 | @override 34 | String get _className => '[$ParamsNotAllowed]'; 35 | } 36 | 37 | /// Called when param is unknown and the library is not able to handle it 38 | class ParamUnknown extends DSONException { 39 | /// The name of the class that contains the param 40 | final String? parentClass; 41 | 42 | /// Param name 43 | final String? paramName; 44 | 45 | /// Called when param is unknown and the library is not able to handle it 46 | ParamUnknown({ 47 | this.parentClass, 48 | this.paramName, 49 | StackTrace? stackTrace, 50 | }) : super( 51 | "Unknown error while trying parse parameter '$paramName' on class" 52 | " '$parentClass'", 53 | stackTrace, 54 | ); 55 | 56 | @override 57 | String get _className => '[$ParamUnknown]'; 58 | } 59 | 60 | /// Called when value is null, but params is required and non-nullable 61 | class ParamNullNotAllowed extends DSONException { 62 | /// the representation of the param 63 | final FunctionParam functionParam; 64 | 65 | /// The name of the class that contains the param 66 | final String parentClass; 67 | 68 | /// Called when value is null, but params is required and non-nullable 69 | ParamNullNotAllowed({ 70 | required this.functionParam, 71 | StackTrace? stackTrace, 72 | required this.parentClass, 73 | }) : super( 74 | "Param '${functionParam.name}' from $parentClass" 75 | '({required $functionParam})' 76 | "${functionParam.alias != null ? " with alias" 77 | " '${functionParam.alias}'," : ''}" 78 | ' is required and non-nullable, but the value is null or some alias' 79 | ' is missing.', 80 | stackTrace, 81 | ); 82 | 83 | @override 84 | String get _className => '[$ParamNullNotAllowed]'; 85 | } 86 | 87 | /// Called when params is not the correct type 88 | class ParamInvalidType extends DSONException { 89 | /// the representation of the param 90 | final FunctionParam functionParam; 91 | 92 | /// The type of param received in json 93 | final String receivedType; 94 | 95 | /// The name of the class that contains the param 96 | final String parentClass; 97 | 98 | /// Called when params is not the correct type 99 | ParamInvalidType( 100 | super.message, 101 | super.stackTrace, { 102 | required this.receivedType, 103 | required this.functionParam, 104 | required this.parentClass, 105 | }); 106 | 107 | @override 108 | String get _className => '[$ParamInvalidType]'; 109 | 110 | /// Called when params is not the correct type 111 | factory ParamInvalidType.typeError({ 112 | required Error error, 113 | required String parentClass, 114 | required Iterable functionParams, 115 | StackTrace? stackTrace, 116 | }) { 117 | final typeErrorAsString = error.toString(); 118 | 119 | final errorSplitted = typeErrorAsString.split("'"); 120 | final receivedType = errorSplitted[1]; 121 | final paramName = errorSplitted[5]; 122 | 123 | final functionParam = functionParams.firstWhereOrNull( 124 | (element) => element.name == paramName, 125 | ); 126 | 127 | if (functionParam == null) { 128 | throw ParamUnknown( 129 | stackTrace: stackTrace, 130 | parentClass: parentClass, 131 | paramName: paramName, 132 | ); 133 | } 134 | 135 | return ParamInvalidType( 136 | "Type '$receivedType' is not a subtype of type '${functionParam.type}' of" 137 | " '$parentClass({${functionParam.isRequired ? 'required ' : ''}" 138 | "$functionParam})'${functionParam.alias != null ? " with alias '" 139 | "${functionParam.alias}'." : '.'}", 140 | stackTrace, 141 | receivedType: receivedType, 142 | functionParam: functionParam, 143 | parentClass: parentClass, 144 | ); 145 | } 146 | 147 | /// This is called when the value expected should have a 148 | /// subscritor operator ([]), but the incoming value in json 149 | /// is not iterable (e.g.: not a List, Set or Map) 150 | factory ParamInvalidType.notIterable({ 151 | required String receivedType, 152 | required FunctionParam functionParam, 153 | required String parentClass, 154 | required StackTrace? stackTrace, 155 | }) { 156 | return ParamInvalidType( 157 | "Type not iterable '$receivedType' is not a subtype of type" 158 | " '$parentClass'${functionParam.alias != null ? " with alias '" 159 | "${functionParam.alias}'." : '.'}", 160 | stackTrace, 161 | receivedType: receivedType, 162 | functionParam: functionParam, 163 | parentClass: parentClass, 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/src/extensions/iterable_extension.dart: -------------------------------------------------------------------------------- 1 | /// Extensions that apply to all iterables. 2 | extension IterableExtension on Iterable { 3 | /// The first element satisfying [test], or `null` if there are none. 4 | T? firstWhereOrNull(bool Function(T element) test) { 5 | for (final element in this) { 6 | if (test(element)) return element; 7 | } 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/param.dart: -------------------------------------------------------------------------------- 1 | import '../dson_adapter.dart'; 2 | 3 | /// Used in "inner" propetier.
4 | /// IParam represents complex transform, for example: ListParam, SetParam 5 | abstract class IParam { 6 | /// execute transform 7 | T call( 8 | DSON dson, 9 | dynamic map, 10 | Map inner, 11 | List resolvers, 12 | Map> aliases, 13 | ); 14 | } 15 | 16 | /// Used in "inner" propetier.
17 | /// Represent a List with no primitive value; 18 | class ListParam implements IParam> { 19 | final Function _constructor; 20 | 21 | /// Used in "inner" propetier.
22 | /// Represent a List with no primitive value; 23 | ListParam(this._constructor); 24 | 25 | @override 26 | List call( 27 | DSON dson, 28 | covariant List map, 29 | Map inner, 30 | List resolvers, 31 | Map> aliases, 32 | ) { 33 | final typedList = map 34 | .map((e) { 35 | return dson.fromJson( 36 | e, 37 | _constructor, 38 | inner: inner, 39 | resolvers: resolvers, 40 | aliases: aliases, 41 | ); 42 | }) 43 | .toList() 44 | .cast(); 45 | 46 | return typedList; 47 | } 48 | } 49 | 50 | /// Used in "inner" propetier.
51 | /// Represent a Set with no primitive value; 52 | class SetParam implements IParam> { 53 | final Function _constructor; 54 | 55 | /// Used in "inner" propetier.
56 | /// Represent a Set with no primitive value; 57 | SetParam(this._constructor); 58 | 59 | @override 60 | Set call( 61 | DSON dson, 62 | covariant List map, 63 | Map inner, 64 | List resolvers, 65 | Map> aliases, 66 | ) { 67 | final typedList = map 68 | .map((e) { 69 | return dson.fromJson( 70 | e, 71 | _constructor, 72 | inner: inner, 73 | resolvers: resolvers, 74 | aliases: aliases, 75 | ); 76 | }) 77 | .toSet() 78 | .cast(); 79 | 80 | return typedList; 81 | } 82 | } 83 | 84 | /// Used to represent a parameter 85 | class FunctionParam { 86 | /// Type of parameter 87 | final String type; 88 | 89 | /// Name of parameter 90 | final String name; 91 | 92 | /// If parameter is required 93 | final bool isRequired; 94 | 95 | /// If parameter is nullable 96 | final bool isNullable; 97 | 98 | /// Alias of parameter 99 | final String? alias; 100 | 101 | /// Used to represent a parameter 102 | FunctionParam({ 103 | required this.type, 104 | required this.name, 105 | required this.isRequired, 106 | required this.isNullable, 107 | this.alias, 108 | }); 109 | 110 | /// Return [String] using alias or name 111 | String get aliasOrName => alias ?? name; 112 | 113 | /// Create a [FunctionParam] from [String] 114 | factory FunctionParam.fromString(String paramText) { 115 | final elements = paramText.split(' '); 116 | 117 | final name = elements.last; 118 | elements.removeLast(); 119 | 120 | var type = elements.last; 121 | 122 | final lastMarkQuestionIndex = type.lastIndexOf('?'); 123 | final isNullable = lastMarkQuestionIndex == type.length - 1; 124 | 125 | if (isNullable) { 126 | type = type.replaceFirst('?', '', lastMarkQuestionIndex); 127 | } 128 | 129 | final isRequired = elements.contains('required'); 130 | 131 | return FunctionParam( 132 | name: name, 133 | type: type, 134 | isRequired: isRequired, 135 | isNullable: isNullable, 136 | ); 137 | } 138 | 139 | @override 140 | String toString() => '$type $name'; 141 | 142 | /// Copy this instance with new values 143 | FunctionParam copyWith({ 144 | String? type, 145 | String? name, 146 | bool? isRequired, 147 | bool? isNullable, 148 | String? alias, 149 | }) { 150 | return FunctionParam( 151 | type: type ?? this.type, 152 | name: name ?? this.name, 153 | isRequired: isRequired ?? this.isRequired, 154 | isNullable: isNullable ?? this.isNullable, 155 | alias: alias ?? this.alias, 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dson_adapter 2 | description: Convert JSON to Dart Class withless code generate(build_runner) 3 | version: 1.2.1 4 | repository: https://github.com/Flutterando/dson_adapter 5 | 6 | environment: 7 | sdk: ">=2.18.0 <4.0.0" 8 | 9 | # dependencies: 10 | # path: ^1.8.0 11 | 12 | dev_dependencies: 13 | flutterando_analysis: ^0.0.2 14 | test: ^1.16.0 15 | -------------------------------------------------------------------------------- /test/src/dson_base_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dson_adapter/dson_adapter.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | late DSON dson; 6 | setUp(() { 7 | dson = const DSON(); 8 | }); 9 | 10 | test('fromJson convert map in Person', () { 11 | final jsonMap = { 12 | 'id': 1, 13 | 'name': 'Joshua Clak', 14 | 'age': 3, 15 | 'nickname': 'Josh', 16 | }; 17 | 18 | final person = dson.fromJson(jsonMap, Person.new); 19 | expect(person.id, 1); 20 | expect(person.name, 'Joshua Clak'); 21 | expect(person.age, 3); 22 | expect(person.nickname, 'Josh'); 23 | }); 24 | 25 | test('fromJson convert map in Person withless name', () { 26 | final jsonMap = { 27 | 'id': 1, 28 | 'age': 3, 29 | }; 30 | 31 | final person = dson.fromJson(jsonMap, Person.new); 32 | expect(person.id, 1); 33 | expect(person.name, null); 34 | expect(person.age, 3); 35 | }); 36 | 37 | test('fromJson convert map in Person withless age', () { 38 | final jsonMap = { 39 | 'id': 1, 40 | 'name': 'Joshua Clak', 41 | }; 42 | 43 | final person = dson.fromJson(jsonMap, Person.new); 44 | expect(person.id, 1); 45 | expect(person.name, 'Joshua Clak'); 46 | expect(person.age, 20); 47 | }); 48 | 49 | test('fromJson convert map in Home (inner object)', () { 50 | final jsonMap = { 51 | 'id': 1, 52 | 'name': 'MyHome', 53 | 'owner': { 54 | 'id': 1, 55 | 'name': 'Joshua Clak', 56 | 'age': 3, 57 | }, 58 | 'parents': [ 59 | { 60 | 'id': 2, 61 | 'name': 'Kepper Vidal', 62 | 'age': 25, 63 | }, 64 | { 65 | 'id': 3, 66 | 'name': 'Douglas Bisserra', 67 | 'age': 23, 68 | }, 69 | ], 70 | }; 71 | 72 | final home = dson.fromJson( 73 | // json Map or List 74 | jsonMap, 75 | // Main constructor 76 | Home.new, 77 | // external types 78 | inner: { 79 | 'owner': Person.new, 80 | 'parents': ListParam(Person.new), 81 | }, 82 | ); 83 | 84 | expect(home.id, 1); 85 | expect(home.name, 'MyHome'); 86 | expect(home.owner, isA()); 87 | expect(home.owner.id, 1); 88 | expect(home.owner.name, 'Joshua Clak'); 89 | expect(home.owner.age, 3); 90 | 91 | expect(home.parents[0].id, 2); 92 | expect(home.parents[0].name, 'Kepper Vidal'); 93 | expect(home.parents[0].age, 25); 94 | 95 | expect(home.parents[1].id, 3); 96 | expect(home.parents[1].name, 'Douglas Bisserra'); 97 | expect(home.parents[1].age, 23); 98 | }); 99 | 100 | test('fromJson works only named params constructor', () { 101 | expect( 102 | () => dson.fromJson({}, (String name) {}, aliases: {}), 103 | throwsA(isA()), 104 | ); 105 | }); 106 | 107 | test('throws error if dont has non-nullable required param [id]', () { 108 | final jsonMap = { 109 | 'name': 'Joshua Clak', 110 | 'age': 3, 111 | }; 112 | 113 | expect( 114 | () => dson.fromJson(jsonMap, Person.new), 115 | throwsA(isA()), 116 | ); 117 | }); 118 | 119 | test('fromJson convert map in Person with id alias to key', () { 120 | final jsonMap = { 121 | 'key': 1, 122 | 'name': 'Joshua Clak', 123 | 'age': 3, 124 | }; 125 | final person = dson.fromJson( 126 | jsonMap, 127 | Person.new, 128 | aliases: { 129 | Person: {'id': 'key'} 130 | }, 131 | ); 132 | expect(person.id, 1); 133 | expect(person.name, 'Joshua Clak'); 134 | expect(person.age, 3); 135 | }); 136 | 137 | test( 138 | 'fromJson convert map in Person withless name when name ' 139 | 'has alias but alias no exist in map', () { 140 | final jsonMap = { 141 | 'id': 1, 142 | 'name': 'Joshua Clak', 143 | 'age': 3, 144 | }; 145 | 146 | final person = dson.fromJson( 147 | jsonMap, 148 | Person.new, 149 | aliases: { 150 | Person: {'name': 'othername'} 151 | }, 152 | ); 153 | expect(person.id, 1); 154 | expect(person.name, null); 155 | expect(person.age, 3); 156 | }); 157 | 158 | test('fromJson convert map in Home (inner object) when API modify most keys', 159 | () { 160 | final jsonMap = { 161 | 'id': 1, 162 | 'name': 'MyHome', 163 | 'master': { 164 | 'key': 1, 165 | 'name': 'Joshua Clak', 166 | 'age': 3, 167 | }, 168 | 'parents': [ 169 | { 170 | 'key': 2, 171 | 'name': 'Kepper Vidal', 172 | 'age': 25, 173 | }, 174 | { 175 | 'key': 3, 176 | 'name': 'Douglas Bisserra', 177 | 'age': 23, 178 | }, 179 | ], 180 | }; 181 | 182 | final home = dson.fromJson( 183 | // json Map or List 184 | jsonMap, 185 | // Main constructor 186 | Home.new, 187 | // external types 188 | inner: { 189 | 'owner': Person.new, 190 | 'parents': ListParam(Person.new), 191 | }, 192 | // Param names Object <-> Param name in API 193 | aliases: { 194 | Home: {'owner': 'master'}, 195 | Person: {'id': 'key'} 196 | }, 197 | ); 198 | 199 | expect(home.id, 1); 200 | expect(home.name, 'MyHome'); 201 | expect(home.owner, isA()); 202 | expect(home.owner.id, 1); 203 | expect(home.owner.name, 'Joshua Clak'); 204 | expect(home.owner.age, 3); 205 | 206 | expect(home.parents[0].id, 2); 207 | expect(home.parents[0].name, 'Kepper Vidal'); 208 | expect(home.parents[0].age, 25); 209 | 210 | expect(home.parents[1].id, 3); 211 | expect(home.parents[1].name, 'Douglas Bisserra'); 212 | expect(home.parents[1].age, 23); 213 | }); 214 | 215 | test( 216 | 'throws error if dont has required param when ' 217 | 'use aliases in required param', () { 218 | final jsonMap = { 219 | 'id': 2, 220 | 'name': 'Kepper Vidal', 221 | 'age': 25, 222 | }; 223 | 224 | expect( 225 | () => dson.fromJson( 226 | jsonMap, 227 | Person.new, 228 | aliases: { 229 | Person: {'id': 'key'} 230 | }, 231 | ), 232 | throwsA(isA()), 233 | ); 234 | }); 235 | 236 | test('Convert List with primitive type', () { 237 | final list = ['Philadelphia, PA, USA']; 238 | 239 | final json = { 240 | 'destination_addresses': list, 241 | }; 242 | 243 | final primitive = const DSON().fromJson( 244 | json, 245 | PrimitiveList.new, 246 | aliases: { 247 | PrimitiveList: {'destinationAddresses': 'destination_addresses'}, 248 | }, 249 | ); 250 | 251 | expect(primitive.destinationAddresses, list); 252 | }); 253 | 254 | test( 255 | 'fromJson should allow ' 256 | 'to have required and nullable parameters simultaneously', () { 257 | // arrange 258 | final jsonMap = { 259 | 'id': 1, 260 | 'age': 3, 261 | }; 262 | 263 | // act 264 | final person = dson.fromJson(jsonMap, Person.new); 265 | 266 | // assert 267 | expect(person.age, 3); // not required and non-nullable 268 | expect(person.name, null); // not required and nullable 269 | expect(person.id, 1); // required and non-nullable 270 | expect(person.nickname, null); // required and nullable 271 | }); 272 | 273 | test( 274 | 'fromJson convert map in Person ' 275 | 'with required and nullable param nickname equal null', () { 276 | // arrange 277 | final jsonMap = { 278 | 'id': 1, 279 | 'age': 3, 280 | 'name': 'Joshua Clak', 281 | }; 282 | 283 | // act 284 | final person = dson.fromJson(jsonMap, Person.new); 285 | 286 | // assert 287 | expect(person.id, 1); 288 | expect(person.age, 3); 289 | expect(person.name, 'Joshua Clak'); 290 | expect(person.nickname, 'Joshua Clak'); 291 | }); 292 | 293 | test( 294 | 'Given a list [List] is not nullable, ' 295 | 'When the converted json for this list is null, ' 296 | 'Then it should throw an exception of type [DSONException] ' 297 | 'instead of [TypeError]', () { 298 | // arrange 299 | final errorsSnapshotJson = {'errors': null}; 300 | 301 | // act, assert 302 | expect( 303 | () => dson.fromJson( 304 | errorsSnapshotJson, 305 | ErrorsSnapshot.new, 306 | ), 307 | throwsA( 308 | predicate( 309 | (e) => e is DSONException && e is! TypeError, 310 | ), 311 | ), 312 | ); 313 | }); 314 | 315 | test( 316 | 'Given id is a parameter of type int, ' 317 | 'When the json contains a value of type String, ' 318 | 'Then it should throw an exception of type [ParamInvalidType]', () { 319 | final jsonMap = { 320 | 'id': '1', 321 | 'name': 'Joshua Clak', 322 | 'age': 3, 323 | 'nickname': 'Josh', 324 | }; 325 | 326 | expect( 327 | () => dson.fromJson(jsonMap, Person.new), 328 | throwsA( 329 | predicate( 330 | (e) => 331 | e is ParamInvalidType && 332 | e.functionParam.type == 'int' && 333 | e.receivedType == 'String', 334 | ), 335 | ), 336 | ); 337 | }); 338 | 339 | test( 340 | 'Given [key] is an alias of the parameter [id], ' 341 | 'And [id] is a parameter of type [int], ' 342 | 'When the json contains a value of type [String], ' 343 | 'Then it should throw an exception of type [ParamInvalidType]', () { 344 | final jsonMap = { 345 | 'key': '1', 346 | 'name': 'Joshua Clak', 347 | 'age': 3, 348 | 'nickname': 'Josh', 349 | }; 350 | 351 | expect( 352 | () => dson.fromJson( 353 | jsonMap, 354 | Person.new, 355 | aliases: { 356 | Person: {'id': 'key'} 357 | }, 358 | ), 359 | throwsA( 360 | predicate( 361 | (e) => 362 | e is ParamInvalidType && 363 | e.functionParam.type == 'int' && 364 | e.receivedType == 'String' && 365 | e.functionParam.alias == 'key', 366 | ), 367 | ), 368 | ); 369 | }); 370 | 371 | test( 372 | 'Given [key] is an alias of the parameter [id], ' 373 | 'And [key] was not specified in the [aliases] property, ' 374 | 'And [key] is INCORRECTLY typed, ' 375 | 'When [dson.fromJson] is called, ' 376 | 'Then it should throw an exception of type [ParamNullNotAllowed]', () { 377 | final jsonMap = { 378 | 'key': '1', 379 | 'name': 'Joshua Clak', 380 | 'age': 3, 381 | 'nickname': 'Josh', 382 | }; 383 | 384 | expect( 385 | () => dson.fromJson( 386 | jsonMap, 387 | Person.new, 388 | ), 389 | throwsA(isA()), 390 | ); 391 | }); 392 | 393 | test( 394 | 'Given [key] is an alias of the parameter [id], ' 395 | 'And [key] was not specified in the [aliases] property, ' 396 | 'And [key] is typed CORRECT, ' 397 | 'When [dson.fromJson] is called, ' 398 | 'Then it should throw an exception of type [ParamNullNotAllowed]', () { 399 | final jsonMap = { 400 | 'key': 1, 401 | 'name': 'Joshua Clak', 402 | 'age': 3, 403 | 'nickname': 'Josh', 404 | }; 405 | 406 | expect( 407 | () => dson.fromJson( 408 | jsonMap, 409 | Person.new, 410 | ), 411 | throwsA(isA()), 412 | ); 413 | }); 414 | 415 | test( 416 | 'Given [key] is an alias of the parameter [id], ' 417 | 'And [key] is specified in the [aliases] property, ' 418 | 'And [key] value is null, ' 419 | 'When [dson.fromJson] is called, ' 420 | 'Then it should throw an exception of type [ParamNullNotAllowed]', () { 421 | final jsonMap = { 422 | 'key': null, 423 | 'name': 'Joshua Clak', 424 | 'age': 3, 425 | 'nickname': 'Josh', 426 | }; 427 | 428 | expect( 429 | () => dson.fromJson( 430 | jsonMap, 431 | Person.new, 432 | aliases: { 433 | Person: { 434 | 'id': 'key', 435 | } 436 | }, 437 | ), 438 | throwsA(isA()), 439 | ); 440 | }); 441 | 442 | test( 443 | 'Given [key] is an alias of the parameter [id], ' 444 | 'And [key] is specified in the [aliases] property, ' 445 | 'And [key] is not present in the json, ' 446 | 'When [dson.fromJson] is called, ' 447 | 'Then it should throw an exception of type [ParamNullNotAllowed]', () { 448 | final jsonMap = { 449 | 'name': 'Joshua Clak', 450 | 'age': 3, 451 | 'nickname': 'Josh', 452 | }; 453 | 454 | expect( 455 | () => dson.fromJson( 456 | jsonMap, 457 | Person.new, 458 | aliases: { 459 | Person: { 460 | 'id': 'key', 461 | } 462 | }, 463 | ), 464 | throwsA(isA()), 465 | ); 466 | }); 467 | 468 | test( 469 | 'Since [parents] is a list of [Pearson], ' 470 | 'When the value of [parents] is a list of [List], ' 471 | 'Then it should throw an error [ParamUnknown] ', () { 472 | final jsonMap = { 473 | 'id': 1, 474 | 'name': 'MyHome', 475 | 'owner': { 476 | 'id': 1, 477 | 'name': 'Joshua Clak', 478 | 'age': 3, 479 | }, 480 | 'parents': [[]] 481 | }; 482 | 483 | expect( 484 | () => dson.fromJson( 485 | jsonMap, 486 | Home.new, 487 | inner: { 488 | 'owner': Person.new, 489 | 'parents': ListParam(Person.new), 490 | }, 491 | ), 492 | throwsA( 493 | predicate((e) => e is ParamUnknown && e.parentClass == 'Person'), 494 | ), 495 | ); 496 | }); 497 | 498 | test( 499 | 'Since [parents] is a list of [Pearson], ' 500 | 'When the value of [parents] is a list of [String] ' 501 | '(not iterable object), Then it should throw a [ParamInvalidType] error ', 502 | () { 503 | final jsonMap = { 504 | 'id': 1, 505 | 'name': 'MyHome', 506 | 'owner': { 507 | 'id': 1, 508 | 'name': 'Joshua Clak', 509 | 'age': 3, 510 | }, 511 | 'parents': [''] 512 | }; 513 | 514 | expect( 515 | () => dson.fromJson( 516 | jsonMap, 517 | Home.new, 518 | inner: { 519 | 'owner': Person.new, 520 | 'parents': ListParam(Person.new), 521 | }, 522 | ), 523 | throwsA( 524 | predicate( 525 | (e) => 526 | e is ParamInvalidType && 527 | e.parentClass == 'Person' && 528 | e.receivedType == 'String' && 529 | e.message == 530 | "Type not iterable 'String' is not a subtype of type " 531 | "'Person'.", 532 | ), 533 | ), 534 | ); 535 | }); 536 | 537 | test( 538 | 'Since [owner] is of type [Person], ' 539 | 'And has the alias [owner_alias] ' 540 | 'When the value of [owner] is a [String], ' 541 | 'Then it should throw a [ParamInvalidType] error', () { 542 | final jsonMap = { 543 | 'id': 1, 544 | 'name': 'MyHome', 545 | 'owner_alias': '', 546 | 'parents': [], 547 | }; 548 | 549 | expect( 550 | () => dson.fromJson( 551 | jsonMap, 552 | Home.new, 553 | inner: { 554 | 'owner': Person.new, 555 | 'parents': ListParam(Person.new), 556 | }, 557 | aliases: { 558 | Home: { 559 | 'owner': 'owner_alias', 560 | }, 561 | }, 562 | ), 563 | throwsA( 564 | predicate( 565 | (e) => 566 | e is ParamInvalidType && 567 | e.parentClass == 'Home' && 568 | e.receivedType == 'String' && 569 | e.functionParam.type == 'Person' && 570 | e.functionParam.alias == 'owner_alias' && 571 | e.functionParam.name == 'owner', 572 | ), 573 | ), 574 | ); 575 | }); 576 | 577 | test( 578 | 'Since home [name] is of type [String], ' 579 | 'When the value of [name] is a [_Map], ' 580 | 'Then it should throw a [ParamInvalidType] error', () { 581 | final jsonMap = { 582 | 'id': 1, 583 | 'name': { 584 | 'id': 1, 585 | 'name': 'Joshua Clak', 586 | 'age': 3, 587 | }, 588 | 'owner': { 589 | 'id': 2, 590 | 'name': 'Father', 591 | 'age': 4, 592 | }, 593 | 'parents': [], 594 | }; 595 | 596 | expect( 597 | () => dson.fromJson( 598 | jsonMap, 599 | Home.new, 600 | inner: { 601 | 'owner': Person.new, 602 | 'parents': ListParam(Person.new), 603 | }, 604 | ), 605 | throwsA( 606 | predicate( 607 | (e) => 608 | e is ParamInvalidType && 609 | e.parentClass == 'Home' && 610 | (e.receivedType == '_Map' || 611 | e.receivedType == '_InternalLinkedHashMap') && 612 | e.functionParam.type == 'String' && 613 | e.functionParam.name == 'name', 614 | ), 615 | ), 616 | ); 617 | }); 618 | 619 | test( 620 | 'Since home [name] is of type [String], ' 621 | 'When the value of [name] is a [List>], ' 622 | 'And [name] has the alias [name_alias], ' 623 | 'Then it should throw a [ParamInvalidType] error', () { 624 | final jsonMap = { 625 | 'id': 1, 626 | 'name_alias': [ 627 | { 628 | 'id': 2, 629 | 'name': 'Father', 630 | 'age': 4, 631 | } 632 | ], 633 | 'owner': { 634 | 'id': 2, 635 | 'name': 'Father', 636 | 'age': 4, 637 | }, 638 | 'parents': [], 639 | }; 640 | 641 | expect( 642 | () => dson.fromJson( 643 | jsonMap, 644 | Home.new, 645 | inner: { 646 | 'owner': Person.new, 647 | 'parents': ListParam(Person.new), 648 | }, 649 | aliases: { 650 | Home: { 651 | 'name': 'name_alias', 652 | }, 653 | }, 654 | ), 655 | throwsA( 656 | predicate( 657 | (e) => 658 | e is ParamInvalidType && 659 | e.parentClass == 'Home' && 660 | e.receivedType == 'List>' && 661 | e.functionParam.type == 'String' && 662 | e.functionParam.name == 'name' && 663 | e.functionParam.alias == 'name_alias', 664 | ), 665 | ), 666 | ); 667 | }); 668 | } 669 | 670 | class Person { 671 | final int id; 672 | final String? name; 673 | final int age; 674 | final String? nickname; 675 | 676 | Person({ 677 | required this.id, 678 | this.name, 679 | this.age = 20, 680 | required String? nickname, 681 | }) : nickname = nickname ?? name; 682 | 683 | @override 684 | String toString() => 'PersonModel(id: $id, name: $name, age: $age)'; 685 | } 686 | 687 | class Home { 688 | final int id; 689 | final String name; 690 | final Person owner; 691 | final List parents; 692 | 693 | Home({ 694 | required this.id, 695 | required this.name, 696 | required this.owner, 697 | required this.parents, 698 | }); 699 | } 700 | 701 | class PrimitiveList { 702 | final List destinationAddresses; 703 | PrimitiveList({ 704 | required this.destinationAddresses, 705 | }); 706 | } 707 | 708 | class ErrorsSnapshot { 709 | final List errors; 710 | ErrorsSnapshot({required this.errors}); 711 | } 712 | -------------------------------------------------------------------------------- /test/src/extensions/iterable_extension_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dson_adapter/src/extensions/iterable_extension.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test( 6 | 'Given [Iterable] has a searched element, ' 7 | 'When [Iterable.firstWhereOrNull] is called, ' 8 | 'Then it should return the element', () async { 9 | const iterable = [1, 2, 3]; 10 | const searchedElement = 3; 11 | 12 | final result = 13 | iterable.firstWhereOrNull((element) => element == searchedElement); 14 | 15 | expect(result, 3); 16 | }); 17 | 18 | test( 19 | 'Given [Iterable] does not have a searched element, ' 20 | 'When [Iterable.firstWhereOrNull] is called, ' 21 | 'Then it should return [null]', () async { 22 | const iterable = [1, 2, 3]; 23 | const searchedElement = 4; 24 | 25 | final result = 26 | iterable.firstWhereOrNull((element) => element == searchedElement); 27 | 28 | expect(result, null); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------