Initial release of the generator package.

This commit is contained in:
Diego Cardenas
2021-10-08 21:39:40 -05:00
commit 2a66744761
10 changed files with 889 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# Files and directories created by pub
.dart_tool/
.packages
# Conventional directory for build outputs
build/
# Directory created by dartdoc
doc/api/

3
CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
## 0.1.0
- Initial release of the annotation package.

29
LICENSE Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, Diego Cardenas
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

221
README.md Normal file
View File

@@ -0,0 +1,221 @@
A generator to create config class from json files that support many environments.
# Motivation
If you use a json file to config your applications, perphaps you need to write a dart class that contain all variables you define with their data types. When you need to manage nested objects is more complicated. Even if you want to add or delete a value in your json, you need to modify your dart class.
If you want to manage environments you need to writte manually the file path for every enviroment you need.
[json_config_generator] wants to help you to have an easy process with the manage of config files and environments.
# Install
To use [json_config_generator], you will need [build_runner]/code-generator setup.\
First, install [build_runner] and [json_config_generator] by adding them to your `pubspec.yaml` file:
> pubspec.yaml
```yaml
dependencies:
json_config_annotation: ^0.1.0
dev_dependencies:
build_runner:
json_config_generator: ^0.1.0
```
- [build_runner], the tool to run code-generators
- [json_config_generator], the code generator
- [json_config_annotation], a package containing annotations for [json_config_generator]
# How to use
To use this generator need to create an empty config dart class that begins with `$` using the annotation `Configuration` defining the environments that you want to have. Each environment has `name` and `path`, if have more that one, the generator create an `Enum` to environments. Also you can specify the name of that `Enum`. By default it's name is `Environment`. Also, you need to add some imports.
> config.dart
```dart
import 'package:json_config_annotation/json_config_annotation.dart';
import 'dart:convert';
import 'package:flutter/services.dart';
part 'config.g.dart'; //{dart file name}.g.dart
@Configuration(
environmentEnumName: 'Env',
environments:[
Environment(name:'dev', path:'assets/config/dev.json'),
Environment(name:'prd', path:'assets/config/prd.json'),
],
)
class $Config{}
```
suposing that have the next json file
> dev.json
```json
{
"base_url": "https://example.com",
"custom_class": {
"value_1": "dfgdfgdfgwqrrqwrqwrqweqwe324523b252dghfdhd",
"value_2": "6Lez7aIaAAAAAN6qZG2343c252bv66b7yn5m8m6"
},
"int_value": 3,
"double_value": 3.5,
"boolean_value": true,
"string_list": ["hello", "world"],
"int_list": [1, 23, 5],
"bool_list": [false, true, true],
"custom_list": [
{
"value_1": "hello"
},
{
"value_1": "world"
}
]
}
```
the generator creates
> config.g.dart
```dart
enum Env { dev, prd }
class Config {
Config._();
static final instance = Config._();
late String baseUrl;
late _CustomClass customClass;
late int intValue;
late double doubleValue;
late bool booleanValue;
late List<String> stringList;
late List<int> intList;
late List<bool> boolList;
late List<_CustomList> customList;
Future<void> init(Env env) async {
String path = '';
switch (env) {
case Env.dev:
path = 'assets/config/dev.json';
break;
case Env.prd:
path = 'assets/config/prd.json';
break;
}
final jsonString = await rootBundle.loadString(path);
final config = json.decode(jsonString) as Map<String, dynamic>;
baseUrl = config['base_url'] as String;
customClass =
_CustomClass.fromJson(config['custom_class'] as Map<String, dynamic>);
intValue = config['int_value'] as int;
doubleValue = config['double_value'] as double;
booleanValue = config['boolean_value'] as bool;
stringList = (config['string_list'] as List).cast<String>();
intList = (config['int_list'] as List).cast<int>();
boolList = (config['bool_list'] as List).cast<bool>();
customList = _CustomList.listFromJson(config['custom_list'] as List);
}
}
class _CustomClass {
const _CustomClass({required this.value1, required this.value2});
factory _CustomClass.fromJson(Map<String, dynamic> customClass) =>
_CustomClass(
value1: customClass['value_1'] as String,
value2: customClass['value_2'] as String,
);
final String value1;
final String value2;
}
class _CustomList {
const _CustomList({required this.value1});
factory _CustomList.fromJson(Map<String, dynamic> customList) => _CustomList(
value1: customList['value_1'] as String,
);
final String value1;
static List<_CustomList> listFromJson(List data) =>
data.map((e) => _CustomList.fromJson(e as Map<String, dynamic>)).toList();
}
```
To use the generated configuration you need to call `init` method to initialize all values. Could be called in `main` defining the Environment that you want to use.
> main.dart
```dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Config.instance.init(Env.dev);
runApp(const MyApp());
}
```
Now can access to fields easy using the `instance`
```dart
Config.instance.baseUrl;
Config.instance.customClass.value1;
```
# Supported data types
```dart
- String
- int
- double
- bool
- CustomClass
- List<String>
- List<int>
- List<double>
- List<bool>
- List<CustomClass>
```
Also can create nested classes!
**Important**
- Do not support nullable values
- Do not support dynamic values
# Run the generator
To run the generator use one of the next commands:
- `flutter pub run build_runner build`
- `dart pub run build_runner build`
## Considerations
Can also create a config with only one `Environment`. If that so, the `Enum` do not be created and when call `init` method, do not need to pass any argument.
It is important to known that the generator takes the first value in lists to known the type of the list, so be caferull
[json_config_generator]: https://pub.dev/packages/json_config_generator
[json_config_annotation]: https://pub.dev/packages/json_config_annotation
[build_runner]: https://pub.dev/packages/build_runner

9
build.yaml Normal file
View File

@@ -0,0 +1,9 @@
builders:
json_config_generator:
target: ":json_config_annotations"
import: "package:json_config_generator/json_config_generator.dart"
builder_factories: ["configBuilder"]
build_extensions: { ".dart": [".json_config_generator.g.part"] }
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]

View File

@@ -0,0 +1,8 @@
import 'package:build/build.dart' show Builder, BuilderOptions;
import 'package:json_config_generator/src/generator.dart';
import 'package:source_gen/source_gen.dart';
Builder configBuilder(BuilderOptions options) {
return SharedPartBuilder(
const [JsonConfigGenerator()], 'json_config_generator');
}

296
lib/src/generator.dart Normal file
View File

@@ -0,0 +1,296 @@
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:built_collection/built_collection.dart';
import 'package:json_config_generator/src/utils.dart';
import 'package:source_gen/source_gen.dart';
import 'package:json_config_annotation/json_config_annotation.dart';
import 'dart:io';
import 'dart:convert';
import 'package:recase/recase.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
class JsonConfigGenerator extends GeneratorForAnnotation<Configuration> {
const JsonConfigGenerator();
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
throwIf(
!element.displayName.startsWith('\$'), 'class name must start with \$',
element: element);
final className = element.displayName.replaceAll('\$', '');
final environmentEnumName =
annotation.read('environmentEnumName').stringValue;
final environments = annotation.read('environments').listValue;
throwIf(environments.isEmpty, 'environments could not be empty',
element: element);
final environmentMap = generateEnvironmentMap(environments, element);
final path = environmentMap.values.first;
final Map<String, dynamic> config = await getConfigMap(path);
if (environments.length == 1) {
return mainClassGenerator(className, config, configFile: path);
} else {
throwIf(environmentEnumName.isEmpty,
'environemntEnumName could not be empty');
final environmentEnum =
environmentEnumGenerator(environmentEnumName, environmentMap);
return stringConverter(environmentEnum) +
mainClassGenerator(className, config,
environmentName: environmentEnumName,
environmentMap: environmentMap);
}
}
Future<Map<String, dynamic>> getConfigMap(String path) async {
final jsonString = await File(path).readAsString();
throwIf(jsonString.isEmpty, 'file in path "$path" could not be empty');
Map<String, dynamic> map = await json.decode(jsonString);
throwIf(map.isEmpty, 'file in path "$path" do not have fields');
return map;
}
Map<String, String> generateEnvironmentMap(
List<DartObject> environments, Element element) {
final map = Map<String, String>();
environments.forEach((env) {
final name = env.getField('name')?.toStringValue();
throwIf(name == null || name.trim().isEmpty,
'name could not be empty in $env',
element: element);
final path = env.getField('path')?.toStringValue();
throwIf(path == null || path.trim().isEmpty,
'path could not be empty in $env',
element: element);
throwIf(
map.containsKey(name),
'every environment need to have distinct name. '
'$name was already used',
element: element);
final fileName = path!.split('/').last;
throwIf(!fileName.contains('.json'),
'environment file "$fileName" must have extension ".json"',
element: element);
map[name!] = path;
});
return map;
}
Enum environmentEnumGenerator(String enumName, Map<String, String> map) {
final builder = EnumBuilder()
..name = enumName
..values =
ListBuilder(map.keys.map((e) => EnumValue((v) => v..name = e)));
return builder.build();
}
String mainClassGenerator(String name, Map<String, dynamic> config,
{String? environmentName,
Map<String, String>? environmentMap,
String? configFile}) {
String initMethodBody = '';
if (configFile != null) {
initMethodBody +=
'''
final jsonString = await rootBundle.loadString('$configFile');
final ${name.camelCase} = json.decode(jsonString) as Map<String, dynamic>;
''';
} else {
initMethodBody =
'''
String path = '';
switch(${environmentName!.camelCase}){
''';
environmentMap!.entries.forEach((entry) => initMethodBody +=
'''
case ${environmentName}.${entry.key}:
path = '${entry.value}';
break;
''');
initMethodBody +=
'''
}
final jsonString = await rootBundle.loadString(path);
final ${name.camelCase} = json.decode(jsonString) as Map<String, dynamic>;
''';
}
String subClasses = '';
config.forEach((key, value) {
final type = getType(key, value);
if (value is Map) {
subClasses += subClassesGenerator(key, value);
initMethodBody +=
'${key.camelCase}= _${key.pascalCase}.fromJson(${name.camelCase}[\'${key.snakeCase}\'] as Map<String,dynamic>);';
} else if (type == 'List<_${key.pascalCase}>') {
subClasses += subClassesGenerator(key, value);
initMethodBody +=
'${key.camelCase}= _${key.pascalCase}.listFromJson(${name.camelCase}[\'${key.snakeCase}\'] as List);';
} else if (type.contains('List')) {
final castType = type.replaceAll('List', '');
initMethodBody +=
'${key.camelCase}= (${name.camelCase}[\'${key.snakeCase}\'] as List).cast$castType();';
} else {
initMethodBody +=
'${key.camelCase}= ${name.camelCase}[\'${key.snakeCase}\'] as $type;';
}
});
final mainClass = Class(
(c) => c
..name = name
..constructors.addAll([Constructor((c) => c..name = '_')])
..fields.addAll([
Field((f) => f
..name = 'instance'
..static = true
..modifier = FieldModifier.final$
..assignment = Code('$name._()')),
...config
.map((key, value) {
return MapEntry(
key,
Field((f) => f
..name = key.camelCase
..late = true
..type = refer(getType(key, value))),
);
})
.values
.toList()
])
..methods.addAll(
[
Method((m) => m
..name = 'init'
..modifier = MethodModifier.async
..requiredParameters = ListBuilder([
if (environmentName != null && environmentMap != null)
Parameter((p) => p
..name = environmentName.camelCase
..type = refer(environmentName))
])
..body = Code(initMethodBody)
..returns = refer('Future<void>'))
],
),
);
return stringConverter(mainClass) + subClasses;
}
String subClassesGenerator(String name, dynamic values) {
Map<String, dynamic> data;
String subClasses = '';
bool isList = false;
if (getType(name, values) == 'List<_${name.pascalCase}>') {
data = (values as List).first;
isList = true;
} else if (values is Map<String, dynamic>) {
data = values;
} else {
return '';
}
String factoryCode = '';
factoryCode += '_${name.pascalCase}(';
data.forEach((key, value) {
final type = getType(key, value);
if (value is Map) {
subClasses += subClassesGenerator(key, value);
factoryCode +=
'${key.camelCase}: _${key.pascalCase}.fromJson(${name.camelCase}[\'${key.snakeCase}\'] as Map<String,dynamic>),';
} else if (type == 'List<_${key.pascalCase}>') {
subClasses += subClassesGenerator(key, value);
factoryCode +=
'${key.camelCase}: _${key.pascalCase}.listFromJson(${name.camelCase}[\'${key.snakeCase}\'] as List),';
} else if (type.contains('List')) {
final castType = type.replaceAll('List', '');
factoryCode +=
'${key.camelCase}= (${name.camelCase}[\'${key.snakeCase}\'] as List).cast$castType();';
} else {
factoryCode +=
'${key.camelCase}: ${name.camelCase}[\'${key.snakeCase}\'] as $type,';
}
});
factoryCode += ')';
final subclass = Class(
(c) => c
..name = '_${name.pascalCase}'
..fields.addAll(data
.map(
(key, value) => MapEntry(
key,
Field((f) => f
..name = key.camelCase
..type = refer(getType(key, value))
..modifier = FieldModifier.final$)),
)
.values
.toList())
..constructors.addAll(
[
Constructor((e) => e
..constant = true
..optionalParameters.addAll(data
.map(
(key, value) => MapEntry(
key,
Parameter((f) => f
..name = key.camelCase
..toThis = true
..named = true
..required = true),
),
)
.values
.toList())),
Constructor((e) => e
..factory = true
..name = 'fromJson'
..lambda = true
..requiredParameters.add(Parameter(
(p) => p
..type = refer('Map<String, dynamic>')
..name = name.camelCase,
))
..body = Code(factoryCode)),
],
)
..methods.addAll([
if (isList)
Method((m) => m
..static = true
..returns = refer('List<_${name.pascalCase}>')
..name = 'listFromJson'
..requiredParameters.add(
Parameter((p) => p
..name = 'data'
..type = refer('List')),
)
..lambda = true
..body = Code(
'data.map((e) => _${name.pascalCase}.fromJson(e as Map<String,dynamic>)).toList()'))
]),
);
return stringConverter(subclass) + subClasses;
}
String getType(String key, dynamic value) {
if (value is String)
return 'String';
else if (value is bool)
return 'bool';
else if (value is int)
return 'int';
else if (value is double)
return 'double';
else if (value is List)
return 'List<${getType(key, value.first)}>';
else
return '_${key.pascalCase}';
}
String stringConverter(Spec obj) {
final emitter = DartEmitter(useNullSafetySyntax: true);
return DartFormatter().format(obj.accept(emitter).toString());
}
}

23
lib/src/utils.dart Normal file
View File

@@ -0,0 +1,23 @@
import 'package:analyzer/dart/element/element.dart';
void throwIf(bool condition, String message, {Element? element}) {
if (condition) throwError(message, element: element);
}
void throwError(String message, {Element? element}) {
throw InvalidGenerationSourceError(
message,
element: element,
);
}
class InvalidGenerationSourceError implements Exception {
final String message;
final Element? element;
const InvalidGenerationSourceError(this.message, {this.element});
@override
String toString() =>
'[ERROR] $message ${element != null ? 'in $element' : ''}';
}

271
pubspec.lock Normal file
View File

@@ -0,0 +1,271 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "27.0.0"
analyzer:
dependency: "direct main"
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0"
build:
dependency: "direct main"
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
build_config:
dependency: "direct main"
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
built_collection:
dependency: "direct main"
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.5"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
code_builder:
dependency: "direct main"
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
dart_style:
dependency: "direct main"
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
json_config_annotation:
dependency: "direct main"
description:
name: json_config_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
recase:
dependency: "direct main"
description:
name: recase
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
source_gen:
dependency: "direct main"
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.14.0 <3.0.0"

20
pubspec.yaml Normal file
View File

@@ -0,0 +1,20 @@
name: json_config_generator
description: Configuration generator from json files.
version: 0.1.0
homepage: https://dev.azure.com/fcorp-infraestructura-servicios-ti/Packages/_git/Flutter.ConfigGenerator
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
analyzer: ^2.4.0
build: ^2.1.1
dart_style: ^2.2.0
source_gen: ^1.1.1
code_builder: ^4.1.0
build_config: ^1.0.0
built_collection: ^5.1.1
json_config_annotation: ^0.1.0
recase: ^4.0.0
dev_dependencies: