add(conduit): refactoring conduit
This commit is contained in:
parent
bbc1e48740
commit
50322e71b2
8 changed files with 933 additions and 1 deletions
30
packages/config/lib/config.dart
Normal file
30
packages/config/lib/config.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* This file is part of the Protevus Platform.
|
||||
*
|
||||
* (C) Protevus <developers@protevus.com>
|
||||
*
|
||||
* 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';
|
40
packages/config/lib/src/compiler.dart
Normal file
40
packages/config/lib/src/compiler.dart
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* This file is part of the Protevus Platform.
|
||||
*
|
||||
* (C) Protevus <developers@protevus.com>
|
||||
*
|
||||
* 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<String, Object> 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';", ""),
|
||||
);
|
||||
}
|
||||
}
|
271
packages/config/lib/src/configuration.dart
Normal file
271
packages/config/lib/src/configuration.dart
Normal file
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* This file is part of the Protevus Platform.
|
||||
*
|
||||
* (C) Protevus <developers@protevus.com>
|
||||
*
|
||||
* 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<dynamic, dynamic> map) {
|
||||
decode(map.map<String, dynamic>((k, v) => MapEntry(k.toString(), v)));
|
||||
}
|
||||
|
||||
/// [contents] must be YAML.
|
||||
Configuration.fromString(String contents) {
|
||||
final yamlMap = loadYaml(contents) as Map<dynamic, dynamic>?;
|
||||
final map =
|
||||
yamlMap?.map<String, dynamic>((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<String> 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<dynamic> 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";
|
||||
}
|
||||
}
|
128
packages/config/lib/src/default_configurations.dart
Normal file
128
packages/config/lib/src/default_configurations.dart
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* This file is part of the Protevus Platform.
|
||||
*
|
||||
* (C) Protevus <developers@protevus.com>
|
||||
*
|
||||
* 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;
|
||||
}
|
16
packages/config/lib/src/intermediate_exception.dart
Normal file
16
packages/config/lib/src/intermediate_exception.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* This file is part of the Protevus Platform.
|
||||
*
|
||||
* (C) Protevus <developers@protevus.com>
|
||||
*
|
||||
* 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<dynamic> keyPath;
|
||||
}
|
249
packages/config/lib/src/mirror_property.dart
Normal file
249
packages/config/lib/src/mirror_property.dart
Normal file
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* This file is part of the Protevus Platform.
|
||||
*
|
||||
* (C) Protevus <developers@protevus.com>
|
||||
*
|
||||
* 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<dynamic, dynamic> _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 = <String, ${typeParam.expectedType}>{};
|
||||
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));
|
||||
}
|
||||
}
|
196
packages/config/lib/src/runtime.dart
Normal file
196
packages/config/lib/src/runtime.dart
Normal file
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* This file is part of the Protevus Platform.
|
||||
*
|
||||
* (C) Protevus <developers@protevus.com>
|
||||
*
|
||||
* 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<String, MirrorConfigurationProperty> 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<String, MirrorConfigurationProperty> _collectProperties() {
|
||||
final declarations = <VariableMirror>[];
|
||||
|
||||
var ptr = type;
|
||||
while (ptr.isSubclassOf(reflectClass(Configuration))) {
|
||||
declarations.addAll(
|
||||
ptr.declarations.values
|
||||
.whereType<VariableMirror>()
|
||||
.where((vm) => !vm.isStatic && !vm.isPrivate),
|
||||
);
|
||||
ptr = ptr.superclass!;
|
||||
}
|
||||
|
||||
final m = <String, MirrorConfigurationProperty>{};
|
||||
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 = <String>[];
|
||||
""";
|
||||
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<String> 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
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue