From 50322e71b26a03a801f5f58ffedd5a5e4a005121 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Tue, 3 Sep 2024 13:15:39 -0700 Subject: [PATCH] add(conduit): refactoring conduit --- packages/config/lib/config.dart | 30 ++ packages/config/lib/src/compiler.dart | 40 +++ packages/config/lib/src/configuration.dart | 271 ++++++++++++++++++ .../lib/src/default_configurations.dart | 128 +++++++++ .../lib/src/intermediate_exception.dart | 16 ++ packages/config/lib/src/mirror_property.dart | 249 ++++++++++++++++ packages/config/lib/src/runtime.dart | 196 +++++++++++++ packages/config/pubspec.yaml | 4 +- 8 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 packages/config/lib/config.dart create mode 100644 packages/config/lib/src/compiler.dart create mode 100644 packages/config/lib/src/configuration.dart create mode 100644 packages/config/lib/src/default_configurations.dart create mode 100644 packages/config/lib/src/intermediate_exception.dart create mode 100644 packages/config/lib/src/mirror_property.dart create mode 100644 packages/config/lib/src/runtime.dart diff --git a/packages/config/lib/config.dart b/packages/config/lib/config.dart new file mode 100644 index 0000000..d1e6982 --- /dev/null +++ b/packages/config/lib/config.dart @@ -0,0 +1,30 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// Configuration library for the Protevus Platform. +/// +/// This library exports various components related to configuration management, +/// including compiler, runtime, and default configurations. It also includes +/// utilities for handling intermediate exceptions and mirror properties. +/// +/// The exported modules are: +/// - compiler: Handles compilation of configuration files. +/// - configuration: Defines the core configuration structure. +/// - default_configurations: Provides pre-defined default configurations. +/// - intermediate_exception: Manages exceptions during configuration processing. +/// - mirror_property: Utilities for reflection-based property handling. +/// - runtime: Manages runtime configuration aspects. +library config; + +export 'package:protevus_config/src/compiler.dart'; +export 'package:protevus_config/src/configuration.dart'; +export 'package:protevus_config/src/default_configurations.dart'; +export 'package:protevus_config/src/intermediate_exception.dart'; +export 'package:protevus_config/src/mirror_property.dart'; +export 'package:protevus_config/src/runtime.dart'; diff --git a/packages/config/lib/src/compiler.dart b/packages/config/lib/src/compiler.dart new file mode 100644 index 0000000..9f538fa --- /dev/null +++ b/packages/config/lib/src/compiler.dart @@ -0,0 +1,40 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:protevus_config/config.dart'; +import 'package:protevus_runtime/runtime.dart'; + +class ConfigurationCompiler extends Compiler { + @override + Map compile(MirrorContext context) { + return Map.fromEntries( + context.getSubclassesOf(Configuration).map((c) { + return MapEntry( + MirrorSystem.getName(c.simpleName), + ConfigurationRuntimeImpl(c), + ); + }), + ); + } + + @override + void deflectPackage(Directory destinationDirectory) { + final libFile = File.fromUri( + destinationDirectory.uri.resolve("lib/").resolve("conduit_config.dart"), + ); + final contents = libFile.readAsStringSync(); + libFile.writeAsStringSync( + contents.replaceFirst( + "export 'package:conduit_config/src/compiler.dart';", ""), + ); + } +} diff --git a/packages/config/lib/src/configuration.dart b/packages/config/lib/src/configuration.dart new file mode 100644 index 0000000..60b2053 --- /dev/null +++ b/packages/config/lib/src/configuration.dart @@ -0,0 +1,271 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:io'; + +import 'package:protevus_config/config.dart'; +import 'package:protevus_runtime/runtime.dart'; +import 'package:meta/meta.dart'; +import 'package:yaml/yaml.dart'; + +/// Subclasses of [Configuration] read YAML strings and files, assigning values from the YAML document to properties +/// of an instance of this type. +abstract class Configuration { + /// Default constructor. + Configuration(); + + Configuration.fromMap(Map map) { + decode(map.map((k, v) => MapEntry(k.toString(), v))); + } + + /// [contents] must be YAML. + Configuration.fromString(String contents) { + final yamlMap = loadYaml(contents) as Map?; + final map = + yamlMap?.map((k, v) => MapEntry(k.toString(), v)); + decode(map); + } + + /// Opens a file and reads its string contents into this instance's properties. + /// + /// [file] must contain valid YAML data. + Configuration.fromFile(File file) : this.fromString(file.readAsStringSync()); + + ConfigurationRuntime get _runtime => + RuntimeContext.current[runtimeType] as ConfigurationRuntime; + + /// Ingests [value] into the properties of this type. + /// + /// Override this method to provide decoding behavior other than the default behavior. + void decode(dynamic value) { + if (value is! Map) { + throw ConfigurationException( + this, + "input is not an object (is a '${value.runtimeType}')", + ); + } + + _runtime.decode(this, value); + + validate(); + } + + /// Validates this configuration. + /// + /// By default, ensures all required keys are non-null. + /// + /// Override this method to perform validations on input data. Throw [ConfigurationException] + /// for invalid data. + @mustCallSuper + void validate() { + _runtime.validate(this); + } + + static dynamic getEnvironmentOrValue(dynamic value) { + if (value is String && value.startsWith(r"$")) { + final envKey = value.substring(1); + if (!Platform.environment.containsKey(envKey)) { + return null; + } + + return Platform.environment[envKey]; + } + return value; + } +} + +abstract class ConfigurationRuntime { + void decode(Configuration configuration, Map input); + void validate(Configuration configuration); + + dynamic tryDecode( + Configuration configuration, + String name, + dynamic Function() decode, + ) { + try { + return decode(); + } on ConfigurationException catch (e) { + throw ConfigurationException( + configuration, + e.message, + keyPath: [name, ...e.keyPath], + ); + } on IntermediateException catch (e) { + final underlying = e.underlying; + if (underlying is ConfigurationException) { + final keyPaths = [ + [name], + e.keyPath, + underlying.keyPath, + ].expand((i) => i).toList(); + + throw ConfigurationException( + configuration, + underlying.message, + keyPath: keyPaths, + ); + } else if (underlying is TypeError) { + throw ConfigurationException( + configuration, + "input is wrong type", + keyPath: [name, ...e.keyPath], + ); + } + + throw ConfigurationException( + configuration, + underlying.toString(), + keyPath: [name, ...e.keyPath], + ); + } catch (e) { + throw ConfigurationException( + configuration, + e.toString(), + keyPath: [name], + ); + } + } +} + +/// Possible options for a configuration item property's optionality. +enum ConfigurationItemAttributeType { + /// [Configuration] properties marked as [required] will throw an exception + /// if their source YAML doesn't contain a matching key. + required, + + /// [Configuration] properties marked as [optional] will be silently ignored + /// if their source YAML doesn't contain a matching key. + optional +} + +/// [Configuration] properties may be attributed with these. +/// +/// **NOTICE**: This will be removed in version 2.0.0. +/// To signify required or optional config you could do: +/// Example: +/// ```dart +/// class MyConfig extends Config { +/// late String required; +/// String? optional; +/// String optionalWithDefult = 'default'; +/// late String optionalWithComputedDefault = _default(); +/// +/// String _default() => 'computed'; +/// } +/// ``` +class ConfigurationItemAttribute { + const ConfigurationItemAttribute._(this.type); + + final ConfigurationItemAttributeType type; +} + +/// A [ConfigurationItemAttribute] for required properties. +/// +/// **NOTICE**: This will be removed in version 2.0.0. +/// To signify required or optional config you could do: +/// Example: +/// ```dart +/// class MyConfig extends Config { +/// late String required; +/// String? optional; +/// String optionalWithDefult = 'default'; +/// late String optionalWithComputedDefault = _default(); +/// +/// String _default() => 'computed'; +/// } +/// ``` +@Deprecated("Use `late` property") +const ConfigurationItemAttribute requiredConfiguration = + ConfigurationItemAttribute._(ConfigurationItemAttributeType.required); + +/// A [ConfigurationItemAttribute] for optional properties. +/// +/// **NOTICE**: This will be removed in version 2.0.0. +/// To signify required or optional config you could do: +/// Example: +/// ```dart +/// class MyConfig extends Config { +/// late String required; +/// String? optional; +/// String optionalWithDefult = 'default'; +/// late String optionalWithComputedDefault = _default(); +/// +/// String _default() => 'computed'; +/// } +/// ``` +@Deprecated("Use `nullable` property") +const ConfigurationItemAttribute optionalConfiguration = + ConfigurationItemAttribute._(ConfigurationItemAttributeType.optional); + +/// Thrown when reading data into a [Configuration] fails. +class ConfigurationException { + ConfigurationException( + this.configuration, + this.message, { + this.keyPath = const [], + }); + + ConfigurationException.missingKeys( + this.configuration, + List missingKeys, { + this.keyPath = const [], + }) : message = + "missing required key(s): ${missingKeys.map((s) => "'$s'").join(", ")}"; + + /// The [Configuration] in which this exception occurred. + final Configuration configuration; + + /// The reason for the exception. + final String message; + + /// The key of the object being evaluated. + /// + /// Either a string (adds '.name') or an int (adds '\[value\]'). + final List keyPath; + + @override + String toString() { + if (keyPath.isEmpty) { + return "Failed to read '${configuration.runtimeType}'\n\t-> $message"; + } + final joinedKeyPath = StringBuffer(); + for (var i = 0; i < keyPath.length; i++) { + final thisKey = keyPath[i]; + if (thisKey is String) { + if (i != 0) { + joinedKeyPath.write("."); + } + joinedKeyPath.write(thisKey); + } else if (thisKey is int) { + joinedKeyPath.write("[$thisKey]"); + } else { + throw StateError("not an int or String"); + } + } + + return "Failed to read key '$joinedKeyPath' for '${configuration.runtimeType}'\n\t-> $message"; + } +} + +/// Thrown when [Configuration] subclass is invalid and requires a change in code. +class ConfigurationError { + ConfigurationError(this.type, this.message); + + /// The type of [Configuration] in which this error appears in. + final Type type; + + /// The reason for the error. + String message; + + @override + String toString() { + return "Invalid configuration type '$type'. $message"; + } +} diff --git a/packages/config/lib/src/default_configurations.dart b/packages/config/lib/src/default_configurations.dart new file mode 100644 index 0000000..6838e7d --- /dev/null +++ b/packages/config/lib/src/default_configurations.dart @@ -0,0 +1,128 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'package:protevus_config/config.dart'; + +/// A [Configuration] to represent a database connection configuration. +class DatabaseConfiguration extends Configuration { + /// Default constructor. + DatabaseConfiguration(); + + DatabaseConfiguration.fromFile(super.file) : super.fromFile(); + + DatabaseConfiguration.fromString(super.yaml) : super.fromString(); + + DatabaseConfiguration.fromMap(super.yaml) : super.fromMap(); + + /// A named constructor that contains all of the properties of this instance. + DatabaseConfiguration.withConnectionInfo( + this.username, + this.password, + this.host, + this.port, + this.databaseName, { + this.isTemporary = false, + }); + + /// The host of the database to connect to. + /// + /// This property is required. + late String host; + + /// The port of the database to connect to. + /// + /// This property is required. + late int port; + + /// The name of the database to connect to. + /// + /// This property is required. + late String databaseName; + + /// A username for authenticating to the database. + /// + /// This property is optional. + String? username; + + /// A password for authenticating to the database. + /// + /// This property is optional. + String? password; + + /// A flag to represent permanence. + /// + /// This flag is used for test suites that use a temporary database to run tests against, + /// dropping it after the tests are complete. + /// This property is optional. + bool isTemporary = false; + + @override + void decode(dynamic value) { + if (value is Map) { + super.decode(value); + return; + } + + if (value is! String) { + throw ConfigurationException( + this, + "'${value.runtimeType}' is not assignable; must be a object or string", + ); + } + + final uri = Uri.parse(value); + host = uri.host; + port = uri.port; + if (uri.pathSegments.length == 1) { + databaseName = uri.pathSegments.first; + } + + if (uri.userInfo == '') { + validate(); + return; + } + + final authority = uri.userInfo.split(":"); + if (authority.isNotEmpty) { + username = Uri.decodeComponent(authority.first); + } + if (authority.length > 1) { + password = Uri.decodeComponent(authority.last); + } + + validate(); + } +} + +/// A [Configuration] to represent an external HTTP API. +class APIConfiguration extends Configuration { + APIConfiguration(); + + APIConfiguration.fromFile(super.file) : super.fromFile(); + + APIConfiguration.fromString(super.yaml) : super.fromString(); + + APIConfiguration.fromMap(super.yaml) : super.fromMap(); + + /// The base URL of the described API. + /// + /// This property is required. + /// Example: https://external.api.com:80/resources + late String baseURL; + + /// The client ID. + /// + /// This property is optional. + String? clientID; + + /// The client secret. + /// + /// This property is optional. + String? clientSecret; +} diff --git a/packages/config/lib/src/intermediate_exception.dart b/packages/config/lib/src/intermediate_exception.dart new file mode 100644 index 0000000..e457766 --- /dev/null +++ b/packages/config/lib/src/intermediate_exception.dart @@ -0,0 +1,16 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class IntermediateException implements Exception { + IntermediateException(this.underlying, this.keyPath); + + final dynamic underlying; + + final List keyPath; +} diff --git a/packages/config/lib/src/mirror_property.dart b/packages/config/lib/src/mirror_property.dart new file mode 100644 index 0000000..0318a7d --- /dev/null +++ b/packages/config/lib/src/mirror_property.dart @@ -0,0 +1,249 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:mirrors'; + +import 'package:protevus_config/config.dart'; + +class MirrorTypeCodec { + MirrorTypeCodec(this.type) { + if (type.isSubtypeOf(reflectType(Configuration))) { + final klass = type as ClassMirror; + final classHasDefaultConstructor = klass.declarations.values.any((dm) { + return dm is MethodMirror && + dm.isConstructor && + dm.constructorName == Symbol.empty && + dm.parameters.every((p) => p.isOptional == true); + }); + + if (!classHasDefaultConstructor) { + throw StateError( + "Failed to compile '${type.reflectedType}'\n\t-> " + "'Configuration' subclasses MUST declare an unnammed constructor " + "(i.e. '${type.reflectedType}();') if they are nested.", + ); + } + } + } + + final TypeMirror type; + + dynamic _decodeValue(dynamic value) { + if (type.isSubtypeOf(reflectType(int))) { + return _decodeInt(value); + } else if (type.isSubtypeOf(reflectType(bool))) { + return _decodeBool(value); + } else if (type.isSubtypeOf(reflectType(Configuration))) { + return _decodeConfig(value); + } else if (type.isSubtypeOf(reflectType(List))) { + return _decodeList(value as List); + } else if (type.isSubtypeOf(reflectType(Map))) { + return _decodeMap(value as Map); + } + + return value; + } + + dynamic _decodeBool(dynamic value) { + if (value is String) { + return value == "true"; + } + + return value as bool; + } + + dynamic _decodeInt(dynamic value) { + if (value is String) { + return int.parse(value); + } + + return value as int; + } + + Configuration _decodeConfig(dynamic object) { + final item = (type as ClassMirror).newInstance(Symbol.empty, []).reflectee + as Configuration; + + item.decode(object); + + return item; + } + + List _decodeList(List value) { + final out = (type as ClassMirror).newInstance(const Symbol('empty'), [], { + const Symbol('growable'): true, + }).reflectee as List; + final innerDecoder = MirrorTypeCodec(type.typeArguments.first); + for (var i = 0; i < value.length; i++) { + try { + final v = innerDecoder._decodeValue(value[i]); + out.add(v); + } on IntermediateException catch (e) { + e.keyPath.add(i); + rethrow; + } catch (e) { + throw IntermediateException(e, [i]); + } + } + return out; + } + + Map _decodeMap(Map value) { + final map = + (type as ClassMirror).newInstance(Symbol.empty, []).reflectee as Map; + + final innerDecoder = MirrorTypeCodec(type.typeArguments.last); + value.forEach((key, val) { + if (key is! String) { + throw StateError('cannot have non-String key'); + } + + try { + map[key] = innerDecoder._decodeValue(val); + } on IntermediateException catch (e) { + e.keyPath.add(key); + rethrow; + } catch (e) { + throw IntermediateException(e, [key]); + } + }); + + return map; + } + + String get expectedType { + return type.reflectedType.toString(); + } + + String get source { + if (type.isSubtypeOf(reflectType(int))) { + return _decodeIntSource; + } else if (type.isSubtypeOf(reflectType(bool))) { + return _decodeBoolSource; + } else if (type.isSubtypeOf(reflectType(Configuration))) { + return _decodeConfigSource; + } else if (type.isSubtypeOf(reflectType(List))) { + return _decodeListSource; + } else if (type.isSubtypeOf(reflectType(Map))) { + return _decodeMapSource; + } + + return "return v;"; + } + + String get _decodeListSource { + final typeParam = MirrorTypeCodec(type.typeArguments.first); + return """ +final out = <${typeParam.expectedType}>[]; +final decoder = (v) { + ${typeParam.source} +}; +for (var i = 0; i < (v as List).length; i++) { + try { + final innerValue = decoder(v[i]); + out.add(innerValue); + } on IntermediateException catch (e) { + e.keyPath.add(i); + rethrow; + } catch (e) { + throw IntermediateException(e, [i]); + } +} +return out; + """; + } + + String get _decodeMapSource { + final typeParam = MirrorTypeCodec(type.typeArguments.last); + return """ +final map = {}; +final decoder = (v) { + ${typeParam.source} +}; +v.forEach((key, val) { + if (key is! String) { + throw StateError('cannot have non-String key'); + } + + try { + map[key] = decoder(val); + } on IntermediateException catch (e) { + e.keyPath.add(key); + rethrow; + } catch (e) { + throw IntermediateException(e, [key]); + } +}); + +return map; + """; + } + + String get _decodeConfigSource { + return """ + final item = $expectedType(); + + item.decode(v); + + return item; + """; + } + + String get _decodeIntSource { + return """ + if (v is String) { + return int.parse(v); + } + + return v as int; +"""; + } + + String get _decodeBoolSource { + return """ + if (v is String) { + return v == "true"; + } + + return v as bool; + """; + } +} + +class MirrorConfigurationProperty { + MirrorConfigurationProperty(this.property) + : codec = MirrorTypeCodec(property.type); + + final VariableMirror property; + final MirrorTypeCodec codec; + + String get key => MirrorSystem.getName(property.simpleName); + bool get isRequired => _isVariableRequired(property); + + String get source => codec.source; + + static bool _isVariableRequired(VariableMirror m) { + try { + final attribute = m.metadata + .firstWhere( + (im) => + im.type.isSubtypeOf(reflectType(ConfigurationItemAttribute)), + ) + .reflectee as ConfigurationItemAttribute; + + return attribute.type == ConfigurationItemAttributeType.required; + } catch (_) { + return false; + } + } + + dynamic decode(dynamic input) { + return codec._decodeValue(Configuration.getEnvironmentOrValue(input)); + } +} diff --git a/packages/config/lib/src/runtime.dart b/packages/config/lib/src/runtime.dart new file mode 100644 index 0000000..9353593 --- /dev/null +++ b/packages/config/lib/src/runtime.dart @@ -0,0 +1,196 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:mirrors'; + +import 'package:protevus_config/config.dart'; +import 'package:protevus_runtime/runtime.dart'; + +class ConfigurationRuntimeImpl extends ConfigurationRuntime + implements SourceCompiler { + ConfigurationRuntimeImpl(this.type) { + // Should be done in the constructor so a type check could be run. + properties = _collectProperties(); + } + + final ClassMirror type; + + late final Map properties; + + @override + void decode(Configuration configuration, Map input) { + final values = Map.from(input); + properties.forEach((name, property) { + final takingValue = values.remove(name); + if (takingValue == null) { + return; + } + + final decodedValue = tryDecode( + configuration, + name, + () => property.decode(takingValue), + ); + if (decodedValue == null) { + return; + } + + if (!reflect(decodedValue).type.isAssignableTo(property.property.type)) { + throw ConfigurationException( + configuration, + "input is wrong type", + keyPath: [name], + ); + } + + final mirror = reflect(configuration); + mirror.setField(property.property.simpleName, decodedValue); + }); + + if (values.isNotEmpty) { + throw ConfigurationException( + configuration, + "unexpected keys found: ${values.keys.map((s) => "'$s'").join(", ")}.", + ); + } + } + + String get decodeImpl { + final buf = StringBuffer(); + + buf.writeln("final valuesCopy = Map.from(input);"); + properties.forEach((k, v) { + buf.writeln("{"); + buf.writeln( + "final v = Configuration.getEnvironmentOrValue(valuesCopy.remove('$k'));", + ); + buf.writeln("if (v != null) {"); + buf.writeln( + " final decodedValue = tryDecode(configuration, '$k', () { ${v.source} });", + ); + buf.writeln(" if (decodedValue is! ${v.codec.expectedType}) {"); + buf.writeln( + " throw ConfigurationException(configuration, 'input is wrong type', keyPath: ['$k']);", + ); + buf.writeln(" }"); + buf.writeln( + " (configuration as ${type.reflectedType}).$k = decodedValue as ${v.codec.expectedType};", + ); + buf.writeln("}"); + buf.writeln("}"); + }); + + buf.writeln( + """ + if (valuesCopy.isNotEmpty) { + throw ConfigurationException(configuration, + "unexpected keys found: \${valuesCopy.keys.map((s) => "'\$s'").join(", ")}."); + } + """, + ); + + return buf.toString(); + } + + @override + void validate(Configuration configuration) { + final configMirror = reflect(configuration); + final requiredValuesThatAreMissing = properties.values + .where((v) { + try { + final value = configMirror.getField(Symbol(v.key)).reflectee; + return v.isRequired && value == null; + } catch (e) { + return true; + } + }) + .map((v) => v.key) + .toList(); + + if (requiredValuesThatAreMissing.isNotEmpty) { + throw ConfigurationException.missingKeys( + configuration, + requiredValuesThatAreMissing, + ); + } + } + + Map _collectProperties() { + final declarations = []; + + var ptr = type; + while (ptr.isSubclassOf(reflectClass(Configuration))) { + declarations.addAll( + ptr.declarations.values + .whereType() + .where((vm) => !vm.isStatic && !vm.isPrivate), + ); + ptr = ptr.superclass!; + } + + final m = {}; + for (final vm in declarations) { + final name = MirrorSystem.getName(vm.simpleName); + m[name] = MirrorConfigurationProperty(vm); + } + return m; + } + + String get validateImpl { + final buf = StringBuffer(); + + const startValidation = """ + final missingKeys = []; +"""; + buf.writeln(startValidation); + properties.forEach((name, property) { + final propCheck = """ + try { + final $name = (configuration as ${type.reflectedType}).$name; + if (${property.isRequired} && $name == null) { + missingKeys.add('$name'); + } + } on Error catch (e) { + missingKeys.add('$name'); + }"""; + buf.writeln(propCheck); + }); + const throwIfErrors = """ + if (missingKeys.isNotEmpty) { + throw ConfigurationException.missingKeys(configuration, missingKeys); + }"""; + buf.writeln(throwIfErrors); + + return buf.toString(); + } + + @override + Future compile(BuildContext ctx) async { + final directives = await ctx.getImportDirectives( + uri: type.originalDeclaration.location!.sourceUri, + alsoImportOriginalFile: true, + ) + ..add("import 'package:conduit_config/src/intermediate_exception.dart';"); + return """ + ${directives.join("\n")} + final instance = ConfigurationRuntimeImpl(); + class ConfigurationRuntimeImpl extends ConfigurationRuntime { + @override + void decode(Configuration configuration, Map input) { + $decodeImpl + } + + @override + void validate(Configuration configuration) { + $validateImpl + } + } + """; + } +} diff --git a/packages/config/pubspec.yaml b/packages/config/pubspec.yaml index 4d939f1..c2953d9 100644 --- a/packages/config/pubspec.yaml +++ b/packages/config/pubspec.yaml @@ -10,7 +10,9 @@ environment: # Add regular dependencies here. dependencies: - # path: ^1.8.0 + protevus_runtime: ^0.0.1 + meta: ^1.3.0 + yaml: ^3.1.2 dev_dependencies: lints: ^3.0.0