add(conduit): refactoring conduit

This commit is contained in:
Patrick Stewart 2024-09-03 13:15:39 -07:00
parent bbc1e48740
commit 50322e71b2
8 changed files with 933 additions and 1 deletions

View 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';

View 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';", ""),
);
}
}

View 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";
}
}

View 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;
}

View 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;
}

View 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));
}
}

View 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
}
}
""";
}
}

View file

@ -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