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.
|
# Add regular dependencies here.
|
||||||
dependencies:
|
dependencies:
|
||||||
# path: ^1.8.0
|
protevus_runtime: ^0.0.1
|
||||||
|
meta: ^1.3.0
|
||||||
|
yaml: ^3.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^3.0.0
|
lints: ^3.0.0
|
||||||
|
|
Loading…
Reference in a new issue