update: updating files with detailed comments

This commit is contained in:
Patrick Stewart 2024-09-06 11:53:40 -07:00
parent 2cb685578b
commit 204b1b998e
6 changed files with 1005 additions and 30 deletions

View file

@ -13,7 +13,28 @@ import 'dart:mirrors';
import 'package:protevus_config/config.dart';
import 'package:protevus_runtime/runtime.dart';
/// A compiler class for configurations in the Protevus Platform.
///
/// This class extends the [Compiler] class and provides functionality to
/// compile configuration-related data and modify package files.
///
/// The [compile] method creates a map of configuration names to their
/// corresponding [ConfigurationRuntimeImpl] instances by scanning for
/// subclasses of [Configuration] in the given [MirrorContext].
///
/// The [deflectPackage] method modifies the "conduit_config.dart" file in the
/// destination directory by removing a specific export statement.
class ConfigurationCompiler extends Compiler {
/// Compiles configuration data from the given [MirrorContext].
///
/// This method scans the [context] for all subclasses of [Configuration]
/// and creates a map where:
/// - The keys are the names of these subclasses (as strings)
/// - The values are instances of [ConfigurationRuntimeImpl] created from
/// the corresponding subclass
///
/// Returns a [Map<String, Object>] where each entry represents a
/// configuration class and its runtime implementation.
@override
Map<String, Object> compile(MirrorContext context) {
return Map.fromEntries(
@ -26,15 +47,26 @@ class ConfigurationCompiler extends Compiler {
);
}
/// Modifies the package file by removing a specific export statement.
///
/// This method performs the following steps:
/// 1. Locates the "config.dart" file in the "lib/" directory of the [destinationDirectory].
/// 2. Reads the contents of the file.
/// 3. Removes the line "export 'package:protevus_config/src/compiler.dart';" from the file contents.
/// 4. Writes the modified contents back to the file.
///
/// This operation is typically used to adjust the exported modules in the compiled package.
///
/// [destinationDirectory] is the directory where the package files are located.
@override
void deflectPackage(Directory destinationDirectory) {
final libFile = File.fromUri(
destinationDirectory.uri.resolve("lib/").resolve("conduit_config.dart"),
destinationDirectory.uri.resolve("lib/").resolve("config.dart"),
);
final contents = libFile.readAsStringSync();
libFile.writeAsStringSync(
contents.replaceFirst(
"export 'package:conduit_config/src/compiler.dart';", ""),
"export 'package:protevus_config/src/compiler.dart';", ""),
);
}
}

View file

@ -14,17 +14,90 @@ 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.
/// A base class for configuration management in Dart applications.
///
/// [Configuration] provides a framework for reading and parsing YAML-based
/// configuration files or strings. It offers various constructors to create
/// configuration objects from different sources (maps, strings, or files),
/// and includes methods for decoding and validating configuration data.
///
/// Key features:
/// - Supports creating configurations from YAML strings, files, or maps
/// - Provides a runtime context for configuration-specific operations
/// - Includes a default decoding mechanism that can be overridden
/// - Offers a validation method to ensure all required fields are present
/// - Allows for environment variable substitution in configuration values
///
/// Subclasses of [Configuration] should implement specific configuration
/// structures by defining properties that correspond to expected YAML keys.
/// The [decode] and [validate] methods can be overridden to provide custom
/// behavior for complex configuration scenarios.
///
/// Example usage:
/// ```dart
/// class MyConfig extends Configuration {
/// late String apiKey;
/// int port = 8080;
///
/// @override
/// void validate() {
/// super.validate();
/// if (port < 1000 || port > 65535) {
/// throw ConfigurationException(this, "Invalid port number");
/// }
/// }
/// }
///
/// final config = MyConfig.fromFile(File('config.yaml'));
/// ```
abstract class Configuration {
/// Default constructor.
/// Default constructor for the Configuration class.
///
/// This constructor creates a new instance of the Configuration class
/// without any initial configuration data. It can be used as a starting
/// point for creating custom configurations, which can then be populated
/// using other methods or by setting properties directly.
Configuration();
/// Creates a [Configuration] instance from a given map.
///
/// This constructor takes a [Map] with dynamic keys and values, converts
/// all keys to strings, and then decodes the resulting map into the
/// configuration properties. This is useful when you have configuration
/// data already in a map format, possibly from a non-YAML source.
///
/// [map] The input map containing configuration data. Keys will be
/// converted to strings, while values remain as their original types.
///
/// Example:
/// ```dart
/// final configMap = {'key1': 'value1', 'key2': 42};
/// final config = MyConfiguration.fromMap(configMap);
/// ```
Configuration.fromMap(Map<dynamic, dynamic> map) {
decode(map.map<String, dynamic>((k, v) => MapEntry(k.toString(), v)));
}
/// [contents] must be YAML.
/// Creates a [Configuration] instance from a YAML string.
///
/// This constructor takes a [String] containing YAML content, parses it into
/// a map, and then decodes the resulting map into the configuration properties.
/// It's useful when you have configuration data as a YAML string, perhaps
/// loaded from a file or received from an API.
///
/// [contents] A string containing valid YAML data. This will be parsed and
/// used to populate the configuration properties.
///
/// Throws a [YamlException] if the YAML parsing fails.
///
/// Example:
/// ```dart
/// final yamlString = '''
/// api_key: abc123
/// port: 8080
/// ''';
/// final config = MyConfiguration.fromString(yamlString);
/// ```
Configuration.fromString(String contents) {
final yamlMap = loadYaml(contents) as Map<dynamic, dynamic>?;
final map =
@ -32,15 +105,29 @@ abstract class Configuration {
decode(map);
}
/// Opens a file and reads its string contents into this instance's properties.
/// Creates a [Configuration] instance from a YAML file.
///
/// [file] must contain valid YAML data.
Configuration.fromFile(File file) : this.fromString(file.readAsStringSync());
/// Returns the [ConfigurationRuntime] associated with the current instance's runtime type.
///
/// This getter retrieves the [ConfigurationRuntime] from the [RuntimeContext.current] map,
/// using the runtime type of the current instance as the key. The retrieved value
/// is then cast to [ConfigurationRuntime].
///
/// This is typically used internally to access runtime-specific configuration
/// operations and validations.
///
/// Returns:
/// The [ConfigurationRuntime] associated with this configuration's type.
///
/// Throws:
/// A runtime exception if the retrieved value cannot be cast to [ConfigurationRuntime].
ConfigurationRuntime get _runtime =>
RuntimeContext.current[runtimeType] as ConfigurationRuntime;
/// Ingests [value] into the properties of this type.
/// Decodes the given [value] and populates the properties of this configuration instance.
///
/// Override this method to provide decoding behavior other than the default behavior.
void decode(dynamic value) {
@ -58,7 +145,8 @@ abstract class Configuration {
/// Validates this configuration.
///
/// By default, ensures all required keys are non-null.
/// This method is called automatically after the configuration is decoded. It performs
/// validation checks on the configuration data to ensure its integrity and correctness.
///
/// Override this method to perform validations on input data. Throw [ConfigurationException]
/// for invalid data.
@ -67,6 +155,24 @@ abstract class Configuration {
_runtime.validate(this);
}
/// Retrieves an environment variable value or returns the original value.
///
/// This method checks if the given [value] is a string that starts with '$'.
/// If so, it interprets the rest of the string as an environment variable name
/// and attempts to retrieve its value from the system environment.
///
/// If the environment variable exists, its value is returned.
/// If the environment variable does not exist, null is returned.
/// If the [value] is not a string starting with '$', the original [value] is returned unchanged.
///
/// Parameters:
/// [value]: The value to check. Can be of any type.
///
/// Returns:
/// - The value of the environment variable if [value] is a string starting with '$'
/// and the corresponding environment variable exists.
/// - null if [value] is a string starting with '$' but the environment variable doesn't exist.
/// - The original [value] if it's not a string starting with '$'.
static dynamic getEnvironmentOrValue(dynamic value) {
if (value is String && value.startsWith(r"$")) {
final envKey = value.substring(1);
@ -80,10 +186,72 @@ abstract class Configuration {
}
}
/// An abstract class representing the runtime behavior for configuration objects.
///
/// This class provides methods for decoding and validating configuration objects,
/// as well as a utility method for handling exceptions during the decoding process.
///
/// Implementations of this class should provide concrete logic for decoding
/// configuration data from input maps and validating the resulting configuration objects.
///
/// The [tryDecode] method offers a standardized way to handle exceptions that may occur
/// during the decoding process, wrapping them in appropriate [ConfigurationException]s
/// with detailed key paths for easier debugging.
abstract class ConfigurationRuntime {
/// Decodes the input map and populates the given configuration object.
///
/// This method is responsible for parsing the input map and setting the
/// corresponding values in the configuration object. It should handle
/// type conversions, nested structures, and any specific logic required
/// for populating the configuration.
///
/// Parameters:
/// [configuration]: The Configuration object to be populated with decoded values.
/// [input]: A Map containing the raw configuration data to be decoded.
///
/// Implementations of this method should handle potential errors gracefully,
/// possibly by throwing ConfigurationException for invalid or missing data.
void decode(Configuration configuration, Map input);
/// Validates the given configuration object.
///
/// This method is responsible for performing validation checks on the
/// provided configuration object. It should ensure that all required
/// fields are present and that the values meet any specific criteria
/// or constraints defined for the configuration.
///
/// Parameters:
/// [configuration]: The Configuration object to be validated.
///
/// Implementations of this method should throw a [ConfigurationException]
/// if any validation errors are encountered, providing clear and specific
/// error messages to aid in debugging and resolution of configuration issues.
void validate(Configuration configuration);
/// Attempts to decode a configuration property and handles exceptions.
///
/// This method provides a standardized way to handle exceptions that may occur
/// during the decoding process of a configuration property. It wraps the decoding
/// logic in a try-catch block and transforms various exceptions into appropriate
/// [ConfigurationException]s with detailed key paths for easier debugging.
///
/// Parameters:
/// [configuration]: The Configuration object being decoded.
/// [name]: The name of the property being decoded.
/// [decode]: A function that performs the actual decoding logic.
///
/// Returns:
/// The result of the [decode] function if successful.
///
/// Throws:
/// [ConfigurationException]:
/// - If a [ConfigurationException] is caught, it's re-thrown with an updated key path.
/// - If an [IntermediateException] is caught, it's transformed into a [ConfigurationException]
/// with appropriate error details.
/// - For any other exception, a new [ConfigurationException] is thrown with the exception message.
///
/// This method is particularly useful for maintaining a consistent error handling
/// approach across different configuration properties and types.
dynamic tryDecode(
Configuration configuration,
String name,
@ -134,18 +302,40 @@ abstract class ConfigurationRuntime {
}
}
/// Possible options for a configuration item property's optionality.
/// Enumerates the possible options for a configuration item property's optionality.
///
/// This enum is used to specify whether a configuration property is required or optional
/// when parsing configuration data. It helps in determining how to handle missing keys
/// in the source YAML configuration.
enum ConfigurationItemAttributeType {
/// [Configuration] properties marked as [required] will throw an exception
/// if their source YAML doesn't contain a matching key.
/// Indicates that a configuration property is required.
///
/// When a configuration property is marked as [required], it means that
/// the corresponding key must be present in the source YAML configuration.
/// If the key is missing, an exception will be thrown during the parsing
/// or validation process.
///
/// This helps ensure that all necessary configuration values are provided
/// and reduces the risk of runtime errors due to missing configuration data.
required,
/// Indicates that a configuration property is optional.
///
/// When a configuration property is marked as [optional], it means that
/// the corresponding key can be omitted from the source YAML configuration
/// without causing an error. If the key is missing, the property will be
/// silently ignored during the parsing process.
///
/// This allows for more flexible configuration structures where some
/// properties are not mandatory and can be omitted without affecting
/// the overall functionality of the configuration.
///
/// [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.
/// Represents an attribute for configuration item properties.
///
/// **NOTICE**: This will be removed in version 2.0.0.
/// To signify required or optional config you could do:
@ -204,14 +394,83 @@ const ConfigurationItemAttribute requiredConfiguration =
const ConfigurationItemAttribute optionalConfiguration =
ConfigurationItemAttribute._(ConfigurationItemAttributeType.optional);
/// Thrown when reading data into a [Configuration] fails.
/// Represents an exception thrown when reading data into a [Configuration] fails.
///
/// This exception provides detailed information about the configuration error,
/// including the configuration object where the error occurred, the error message,
/// and optionally, the key path to the problematic configuration item.
///
/// The class offers two constructors:
/// 1. A general constructor for creating exceptions with custom messages.
/// 2. A specialized constructor [ConfigurationException.missingKeys] for creating
/// exceptions specifically related to missing required keys.
///
/// The [toString] method provides a formatted error message that includes the
/// configuration type, the key path (if available), and the error message.
///
/// Usage:
/// ```dart
/// throw ConfigurationException(
/// myConfig,
/// "Invalid value",
/// keyPath: ['server', 'port'],
/// );
/// ```
///
/// Or for missing keys:
/// ```dart
/// throw ConfigurationException.missingKeys(
/// myConfig,
/// ['apiKey', 'secret'],
/// );
/// ```
class ConfigurationException {
/// Creates a new [ConfigurationException] instance.
///
/// This constructor is used to create an exception that provides information
/// about a configuration error.
///
/// Parameters:
/// - [configuration]: The [Configuration] object where the error occurred.
/// - [message]: A string describing the error.
/// - [keyPath]: An optional list of keys or indices that specify the path to
/// the problematic configuration item. Defaults to an empty list.
///
/// Example:
/// ```dart
/// throw ConfigurationException(
/// myConfig,
/// "Invalid port number",
/// keyPath: ['server', 'port'],
/// );
/// ```
ConfigurationException(
this.configuration,
this.message, {
this.keyPath = const [],
});
/// Creates a [ConfigurationException] for missing required keys.
///
/// This constructor is specifically used to create an exception when one or more
/// required keys are missing from the configuration.
///
/// Parameters:
/// - [configuration]: The [Configuration] object where the missing keys were detected.
/// - [missingKeys]: A list of strings representing the names of the missing required keys.
/// - [keyPath]: An optional list of keys or indices that specify the path to the
/// configuration item where the missing keys were expected. Defaults to an empty list.
///
/// The [message] is automatically generated to list all the missing keys.
///
/// Example:
/// ```dart
/// throw ConfigurationException.missingKeys(
/// myConfig,
/// ['apiKey', 'secret'],
/// keyPath: ['server', 'authentication'],
/// );
/// ```
ConfigurationException.missingKeys(
this.configuration,
List<String> missingKeys, {
@ -219,17 +478,52 @@ class ConfigurationException {
}) : message =
"missing required key(s): ${missingKeys.map((s) => "'$s'").join(", ")}";
/// The [Configuration] in which this exception occurred.
/// The [Configuration] instance in which this exception occurred.
///
/// This field stores a reference to the [Configuration] object that was being
/// processed when the exception was thrown. It provides context about which
/// specific configuration was involved in the error, allowing for more
/// detailed error reporting and easier debugging.
///
/// The stored configuration can be used to access additional information
/// about the configuration state at the time of the error, which can be
/// helpful in diagnosing and resolving configuration-related issues.
final Configuration configuration;
/// The reason for the exception.
///
/// This field contains a string describing the specific error or reason
/// why the [ConfigurationException] was thrown. It provides detailed
/// information about what went wrong during the configuration process.
///
/// The message can be used for logging, debugging, or displaying error
/// information to users or developers to help diagnose and fix
/// configuration-related issues.
final String message;
/// The key of the object being evaluated.
/// The key path of the object being evaluated.
///
/// Either a string (adds '.name') or an int (adds '\[value\]').
final List<dynamic> keyPath;
/// Provides a string representation of the [ConfigurationException].
///
/// This method generates a formatted error message that includes:
/// - The type of the configuration where the error occurred
/// - The key path to the problematic configuration item (if available)
/// - The specific error message
///
/// The key path is constructed by joining the elements in [keyPath]:
/// - String elements are joined with dots (e.g., 'server.port')
/// - Integer elements are enclosed in square brackets (e.g., '[0]')
///
/// If [keyPath] is empty, a general error message for the configuration is returned.
///
/// Returns:
/// A string containing the formatted error message.
///
/// Throws:
/// [StateError] if an element in [keyPath] is neither a String nor an int.
@override
String toString() {
if (keyPath.isEmpty) {
@ -254,16 +548,78 @@ class ConfigurationException {
}
}
/// Thrown when [Configuration] subclass is invalid and requires a change in code.
/// Represents an error that occurs when a [Configuration] subclass is invalid and requires a change in code.
///
/// This exception is thrown when there's a structural or logical issue with a [Configuration] subclass
/// that cannot be resolved at runtime and requires modifications to the code itself.
///
/// The [ConfigurationError] provides information about the specific [Configuration] type that caused the error
/// and a descriptive message explaining the nature of the invalidity.
///
/// Properties:
/// - [type]: The Type of the [Configuration] subclass where the error occurred.
/// - [message]: A String describing the specific error or invalidity.
///
/// Usage:
/// ```dart
/// throw ConfigurationError(MyConfig, "Missing required property 'apiKey'");
/// ```
///
/// The [toString] method provides a formatted error message combining the invalid type and the error description.
class ConfigurationError {
/// Creates a new [ConfigurationError] instance.
///
/// This constructor is used to create an error that indicates an invalid [Configuration] subclass
/// which requires changes to the code itself to resolve.
///
/// Parameters:
/// - [type]: The [Type] of the [Configuration] subclass where the error occurred.
/// - [message]: A string describing the specific error or invalidity.
///
/// This error is typically thrown when there's a structural or logical issue with a [Configuration]
/// subclass that cannot be resolved at runtime and requires modifications to the code.
///
/// Example:
/// ```dart
/// throw ConfigurationError(MyConfig, "Missing required property 'apiKey'");
/// ```
ConfigurationError(this.type, this.message);
/// The type of [Configuration] in which this error appears in.
/// The type of [Configuration] in which this error appears.
///
/// This property stores the [Type] of the [Configuration] subclass that is
/// considered invalid or problematic. It provides context about which specific
/// configuration class triggered the error, allowing for more precise error
/// reporting and easier debugging.
///
/// The stored type can be used to identify the exact [Configuration] subclass
/// that needs to be modified or corrected to resolve the error.
final Type type;
/// The reason for the error.
///
/// This field contains a string describing the specific error or reason
/// why the [ConfigurationError] was thrown. It provides detailed
/// information about what makes the [Configuration] subclass invalid
/// or problematic.
///
/// The message can be used for logging, debugging, or displaying error
/// information to developers to help diagnose and fix issues related
/// to the structure or implementation of the [Configuration] subclass.
String message;
/// Returns a string representation of the [ConfigurationError].
///
/// This method generates a formatted error message that includes:
/// - The type of the invalid [Configuration] subclass
/// - The specific error message describing the invalidity
///
/// The resulting string is useful for logging, debugging, or displaying
/// error information to developers to help identify and fix issues with
/// the [Configuration] subclass implementation.
///
/// Returns:
/// A string containing the formatted error message.
@override
String toString() {
return "Invalid configuration type '$type'. $message";

View file

@ -10,17 +10,73 @@
import 'package:protevus_config/config.dart';
/// A [Configuration] to represent a database connection configuration.
///
/// This class extends [Configuration] and provides properties and methods
/// for managing database connection settings. It includes properties for
/// host, port, database name, username, password, and a flag for temporary
/// databases. The class supports initialization from various sources
/// (file, string, map) and provides a custom decoder for parsing connection
/// strings.
///
/// Properties:
/// - [host]: The host of the database to connect to (required).
/// - [port]: The port of the database to connect to (required).
/// - [databaseName]: The name of the database to connect to (required).
/// - [username]: A username for authenticating to the database (optional).
/// - [password]: A password for authenticating to the database (optional).
/// - [isTemporary]: A flag to represent permanence, used for test suites (optional).
///
/// The [decode] method allows parsing of connection strings or maps to
/// populate the configuration properties.
class DatabaseConfiguration extends Configuration {
/// Default constructor.
/// Default constructor for DatabaseConfiguration.
///
/// Creates a new instance of DatabaseConfiguration without initializing any properties.
/// Properties can be set manually or through the decode method after instantiation.
DatabaseConfiguration();
/// Creates a [DatabaseConfiguration] instance from a file.
///
/// This named constructor initializes the configuration by reading from a file.
/// The file path is passed to the superclass constructor [Configuration.fromFile].
///
/// Parameters:
/// [file]: The path to the configuration file.
DatabaseConfiguration.fromFile(super.file) : super.fromFile();
/// Creates a [DatabaseConfiguration] instance from a YAML string.
///
/// This named constructor initializes the configuration by parsing a YAML string.
/// The YAML string is passed to the superclass constructor [Configuration.fromString].
///
/// Parameters:
/// [yaml]: A string containing YAML-formatted configuration data.
DatabaseConfiguration.fromString(super.yaml) : super.fromString();
/// Creates a [DatabaseConfiguration] instance from a Map.
///
/// This named constructor initializes the configuration using a Map of key-value pairs.
/// The Map is passed to the superclass constructor [Configuration.fromMap].
///
/// Parameters:
/// [yaml]: A Map containing configuration data.
DatabaseConfiguration.fromMap(super.yaml) : super.fromMap();
/// A named constructor that contains all of the properties of this instance.
/// Creates a [DatabaseConfiguration] instance with all connection information provided.
///
/// This named constructor allows for the direct initialization of all database connection
/// properties in a single call. It sets both required and optional properties.
///
/// Parameters:
/// [username]: The username for database authentication (optional).
/// [password]: The password for database authentication (optional).
/// [host]: The host address of the database server (required).
/// [port]: The port number on which the database server is listening (required).
/// [databaseName]: The name of the specific database to connect to (required).
/// [isTemporary]: A flag indicating if this is a temporary database connection (optional, defaults to false).
///
/// This constructor provides a convenient way to create a fully configured
/// [DatabaseConfiguration] object when all connection details are known in advance.
DatabaseConfiguration.withConnectionInfo(
this.username,
this.password,
@ -32,36 +88,102 @@ class DatabaseConfiguration extends Configuration {
/// The host of the database to connect to.
///
/// This property is required.
/// This property represents the hostname or IP address of the database server
/// that this configuration will connect to. It is a required field and must be
/// set before attempting to establish a database connection.
///
/// The value should be a valid hostname (e.g., 'localhost', 'db.example.com')
/// or an IP address (e.g., '192.168.1.100').
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late String host;
/// The port of the database to connect to.
///
/// This property is required.
/// This property represents the network port number on which the database server
/// is listening for connections. It is a required field and must be set before
/// attempting to establish a database connection.
///
/// The value should be a valid port number, typically an integer between 0 and 65535.
/// Common database port numbers include 5432 for PostgreSQL, 3306 for MySQL,
/// and 1433 for SQL Server, but the actual port may vary depending on the specific
/// database configuration.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late int port;
/// The name of the database to connect to.
///
/// This property is required.
/// This property represents the specific database name within the database server
/// that this configuration will target. It is a required field and must be set
/// before attempting to establish a database connection.
///
/// The value should be a valid database name as defined in your database server.
/// For example, it could be 'myapp_database', 'users_db', or 'production_data'.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late String databaseName;
/// A username for authenticating to the database.
///
/// This property is optional.
/// This property represents the username used for authentication when connecting
/// to the database. It is an optional field, meaning it can be null if authentication
/// is not required or if other authentication methods are used.
///
/// The value should be a string containing the username as configured in the
/// database server for this particular connection. For example, it could be
/// 'db_user', 'admin', or 'app_service_account'.
///
/// If this property is set, it is typically used in conjunction with the [password]
/// property to form a complete set of credentials for database authentication.
String? username;
/// A password for authenticating to the database.
///
/// This property is optional.
/// This property represents the password used for authentication when connecting
/// to the database. It is an optional field, meaning it can be null if authentication
/// is not required or if other authentication methods are used.
///
/// The value should be a string containing the password that corresponds to the
/// [username] for this database connection. For security reasons, it's important
/// to handle this value carefully and avoid exposing it in logs or user interfaces.
///
/// If this property is set, it is typically used in conjunction with the [username]
/// property to form a complete set of credentials for database authentication.
///
/// Note: In production environments, it's recommended to use secure methods of
/// storing and retrieving passwords, such as environment variables or secure
/// secret management systems, rather than hardcoding them in the configuration.
String? password;
/// A flag to represent permanence.
/// A flag to represent permanence of the database.
///
/// 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;
/// Decodes and populates the configuration from a given value.
///
/// This method can handle two types of input:
/// 1. A Map: In this case, it delegates to the superclass's decode method.
/// 2. A String: It parses the string as a URI to extract database connection details.
///
/// For string input, it extracts:
/// - Host and port from the URI
/// - Database name from the path (if present)
/// - Username and password from the userInfo part of the URI (if present)
///
/// After parsing, it calls the validate method to ensure all required fields are set.
///
/// Parameters:
/// [value]: The input to decode. Can be a Map or a String.
///
/// Throws:
/// [ConfigurationException]: If the input is neither a Map nor a String.
@override
void decode(dynamic value) {
if (value is Map) {
@ -101,27 +223,80 @@ class DatabaseConfiguration extends Configuration {
}
/// A [Configuration] to represent an external HTTP API.
///
/// This class extends [Configuration] and provides properties for managing
/// external API connection settings. It includes properties for the base URL,
/// client ID, and client secret.
///
/// The class supports initialization from various sources (file, string, map)
/// through its constructors.
///
/// Properties:
/// - [baseURL]: The base URL of the described API (required).
/// - [clientID]: The client ID for API authentication (optional).
/// - [clientSecret]: The client secret for API authentication (optional).
///
/// Constructors:
/// - Default constructor: Creates an empty instance.
/// - [fromFile]: Initializes from a configuration file.
/// - [fromString]: Initializes from a YAML string.
/// - [fromMap]: Initializes from a Map.
class APIConfiguration extends Configuration {
/// Default constructor for APIConfiguration.
///
/// Creates a new instance of APIConfiguration without initializing any properties.
/// Properties can be set manually or through the decode method after instantiation.
APIConfiguration();
/// Creates an [APIConfiguration] instance from a file.
///
/// This named constructor initializes the configuration by reading from a file.
/// The file path is passed to the superclass constructor [Configuration.fromFile].
///
/// Parameters:
/// [file]: The path to the configuration file.
APIConfiguration.fromFile(super.file) : super.fromFile();
/// Creates an [APIConfiguration] instance from a YAML string.
///
/// This named constructor initializes the configuration by parsing a YAML string.
/// The YAML string is passed to the superclass constructor [Configuration.fromString].
///
/// Parameters:
/// [yaml]: A string containing YAML-formatted configuration data.
APIConfiguration.fromString(super.yaml) : super.fromString();
/// Creates an [APIConfiguration] instance from a Map.
///
/// This named constructor initializes the configuration using a Map of key-value pairs.
/// The Map is passed to the superclass constructor [Configuration.fromMap].
///
/// Parameters:
/// [yaml]: A Map containing configuration data.
APIConfiguration.fromMap(super.yaml) : super.fromMap();
/// The base URL of the described API.
///
/// This property represents the root URL for the external API that this configuration
/// is describing. It is a required field and must be set before using the API configuration.
///
/// The value should be a complete URL, including the protocol (http or https),
/// domain name, and optionally the port and base path. It serves as the foundation
/// for constructing full URLs to specific API endpoints.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
///
/// This property is required.
/// Example: https://external.api.com:80/resources
late String baseURL;
/// The client ID.
/// The client ID for API authentication.
///
/// This property is optional.
String? clientID;
/// The client secret.
/// The client secret for API authentication.
///
/// This property is optional.
String? clientSecret;

View file

@ -7,10 +7,31 @@
* file that was distributed with this source code.
*/
/// An exception class used for intermediate error handling.
///
/// This class encapsulates an underlying exception and a key path,
/// allowing for more detailed error reporting in nested structures.
///
/// [underlying] is the original exception that was caught.
/// [keyPath] is a list representing the path to the error in a nested structure.
class IntermediateException implements Exception {
/// Creates an [IntermediateException] with the given [underlying] exception and [keyPath].
///
/// [underlying] is the original exception that was caught.
/// [keyPath] is a list representing the path to the error in a nested structure.
IntermediateException(this.underlying, this.keyPath);
/// The original exception that was caught.
///
/// This field stores the underlying exception that triggered the creation
/// of this [IntermediateException]. It can be of any type, hence the
/// [dynamic] type annotation.
final dynamic underlying;
/// A list representing the path to the error in a nested structure.
///
/// This field stores the key path as a list of dynamic elements. Each element
/// in the list represents a key or index in the nested structure, helping to
/// pinpoint the exact location of the error.
final List<dynamic> keyPath;
}

View file

@ -8,10 +8,47 @@
*/
import 'dart:mirrors';
import 'package:protevus_config/config.dart';
/// A codec for decoding and encoding values based on their type using reflection.
///
/// This class uses the dart:mirrors library to introspect types and provide
/// appropriate decoding and encoding logic for various data types including
/// int, bool, Configuration subclasses, List, and Map.
///
/// The class supports:
/// - Decoding values from various input formats to their corresponding Dart types.
/// - Generating source code strings for decoding operations.
/// - Validating Configuration subclasses to ensure they have a default constructor.
///
/// Usage:
/// ```dart
/// final codec = MirrorTypeCodec(reflectType(SomeType));
/// final decodedValue = codec._decodeValue(inputValue);
/// final sourceCode = codec.source;
/// ```
///
/// Note: This class relies heavily on reflection, which may have performance
/// implications and is not supported in all Dart runtime environments.
class MirrorTypeCodec {
/// Constructor for MirrorTypeCodec.
///
/// This constructor takes a [TypeMirror] as its parameter and initializes the codec.
/// It performs a validation check for Configuration subclasses to ensure they have
/// a default (unnamed) constructor with all optional parameters.
///
/// Parameters:
/// [type]: The TypeMirror representing the type for which this codec is being created.
///
/// Throws:
/// [StateError]: If the type is a subclass of Configuration and doesn't have
/// an unnamed constructor with all optional parameters.
///
/// The constructor specifically:
/// 1. Checks if the type is a subclass of Configuration.
/// 2. If so, it verifies the presence of a default constructor.
/// 3. Throws a StateError if the required constructor is missing, providing
/// a detailed error message to guide the developer.
MirrorTypeCodec(this.type) {
if (type.isSubtypeOf(reflectType(Configuration))) {
final klass = type as ClassMirror;
@ -32,8 +69,34 @@ class MirrorTypeCodec {
}
}
/// The [TypeMirror] representing the type for which this codec is created.
///
/// This field stores the reflection information about the type that this
/// [MirrorTypeCodec] instance is designed to handle. It is used throughout
/// the class to determine how to decode and encode values of this type.
final TypeMirror type;
/// Decodes a value based on its type using reflection.
///
/// This method takes a [dynamic] input value and decodes it according to the
/// type specified by this codec's [type] property. It supports decoding for:
/// - Integers
/// - Booleans
/// - Configuration subclasses
/// - Lists
/// - Maps
///
/// If the input type doesn't match any of these, the original value is returned.
///
/// Parameters:
/// [value]: The input value to be decoded.
///
/// Returns:
/// The decoded value, with its type corresponding to the codec's [type].
///
/// Throws:
/// May throw exceptions if decoding fails, particularly for nested structures
/// like Lists and Maps.
dynamic _decodeValue(dynamic value) {
if (type.isSubtypeOf(reflectType(int))) {
return _decodeInt(value);
@ -50,6 +113,21 @@ class MirrorTypeCodec {
return value;
}
/// Decodes a boolean value from various input types.
///
/// This method handles the conversion of input values to boolean:
/// - If the input is a String, it returns true if the string is "true" (case-sensitive),
/// and false otherwise.
/// - For non-String inputs, it attempts to cast the value directly to a bool.
///
/// Parameters:
/// [value]: The input value to be decoded into a boolean.
///
/// Returns:
/// A boolean representation of the input value.
///
/// Throws:
/// TypeError: If the input cannot be cast to a bool (for non-String inputs).
dynamic _decodeBool(dynamic value) {
if (value is String) {
return value == "true";
@ -58,6 +136,21 @@ class MirrorTypeCodec {
return value as bool;
}
/// Decodes an integer value from various input types.
///
/// This method handles the conversion of input values to integers:
/// - If the input is a String, it attempts to parse it as an integer.
/// - For non-String inputs, it attempts to cast the value directly to an int.
///
/// Parameters:
/// [value]: The input value to be decoded into an integer.
///
/// Returns:
/// An integer representation of the input value.
///
/// Throws:
/// FormatException: If the input String cannot be parsed as an integer.
/// TypeError: If the input cannot be cast to an int (for non-String inputs).
dynamic _decodeInt(dynamic value) {
if (value is String) {
return int.parse(value);
@ -66,6 +159,21 @@ class MirrorTypeCodec {
return value as int;
}
/// Decodes a Configuration object from the given input.
///
/// This method creates a new instance of the Configuration subclass
/// represented by this codec's type, and then decodes the input object
/// into it.
///
/// Parameters:
/// [object]: The input object to be decoded into a Configuration instance.
///
/// Returns:
/// A new instance of the Configuration subclass, populated with the decoded data.
///
/// Throws:
/// May throw exceptions if the instantiation fails or if the decode
/// method of the Configuration subclass throws an exception.
Configuration _decodeConfig(dynamic object) {
final item = (type as ClassMirror).newInstance(Symbol.empty, []).reflectee
as Configuration;
@ -75,6 +183,26 @@ class MirrorTypeCodec {
return item;
}
/// Decodes a List value based on the codec's type parameters.
///
/// This method creates a new List instance and populates it with decoded elements
/// from the input List. It uses an inner decoder to process each element according
/// to the type specified in the codec's type arguments.
///
/// Parameters:
/// [value]: The input List to be decoded.
///
/// Returns:
/// A new List containing the decoded elements.
///
/// Throws:
/// IntermediateException: If an error occurs during the decoding of any element.
/// The exception includes the index of the problematic element in its keyPath.
///
/// Note:
/// - The method creates a growable List.
/// - It uses reflection to create the new List instance.
/// - Each element is decoded using an inner decoder based on the first type argument.
List _decodeList(List value) {
final out = (type as ClassMirror).newInstance(const Symbol('empty'), [], {
const Symbol('growable'): true,
@ -94,6 +222,27 @@ class MirrorTypeCodec {
return out;
}
/// Decodes a Map value based on the codec's type parameters.
///
/// This method creates a new Map instance and populates it with decoded key-value pairs
/// from the input Map. It uses an inner decoder to process each value according
/// to the type specified in the codec's type arguments.
///
/// Parameters:
/// [value]: The input Map to be decoded.
///
/// Returns:
/// A new Map containing the decoded key-value pairs.
///
/// Throws:
/// StateError: If any key in the input Map is not a String.
/// IntermediateException: If an error occurs during the decoding of any value.
/// The exception includes the key of the problematic value in its keyPath.
///
/// Note:
/// - The method creates a new Map instance using reflection.
/// - It enforces that all keys must be Strings.
/// - Each value is decoded using an inner decoder based on the last type argument.
Map<dynamic, dynamic> _decodeMap(Map value) {
final map =
(type as ClassMirror).newInstance(Symbol.empty, []).reflectee as Map;
@ -117,10 +266,36 @@ class MirrorTypeCodec {
return map;
}
/// Returns a string representation of the expected type for this codec.
///
/// This getter uses the [reflectedType] property of the [type] field
/// to obtain a string representation of the type that this codec is
/// expecting to handle. This is useful for generating type-specific
/// decoding logic or for debugging purposes.
///
/// Returns:
/// A [String] representing the name of the expected type.
String get expectedType {
return type.reflectedType.toString();
}
/// Returns the source code for decoding a value based on its type.
///
/// This getter generates and returns a string containing Dart code that can be used
/// to decode a value of the type represented by this codec. The returned code varies
/// depending on the type:
///
/// - For [int], it returns code to parse integers from strings or cast to int.
/// - For [bool], it returns code to convert strings to booleans or cast to bool.
/// - For [Configuration] subclasses, it returns code to create and decode a new instance.
/// - For [List], it returns code to decode each element of the list.
/// - For [Map], it returns code to decode each value in the map.
/// - For any other type, it returns code that simply returns the input value unchanged.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding the value.
String get source {
if (type.isSubtypeOf(reflectType(int))) {
return _decodeIntSource;
@ -137,6 +312,20 @@ class MirrorTypeCodec {
return "return v;";
}
/// Generates source code for decoding a List value.
///
/// This getter creates a string containing Dart code that decodes a List
/// based on the codec's type parameters. The generated code:
/// - Creates a new List to store decoded elements.
/// - Defines an inner decoder function for processing each element.
/// - Iterates through the input List, decoding each element.
/// - Handles exceptions, wrapping them in IntermediateException with the index.
///
/// The decoder function uses the source code from the inner codec,
/// which is based on the first type argument of the List.
///
/// Returns:
/// A String containing the Dart code for List decoding.
String get _decodeListSource {
final typeParam = MirrorTypeCodec(type.typeArguments.first);
return """
@ -159,6 +348,21 @@ return out;
""";
}
/// Generates source code for decoding a Map value.
///
/// This getter creates a string containing Dart code that decodes a Map
/// based on the codec's type parameters. The generated code:
/// - Creates a new Map to store decoded key-value pairs.
/// - Defines an inner decoder function for processing each value.
/// - Iterates through the input Map, ensuring all keys are Strings.
/// - Decodes each value using the inner decoder function.
/// - Handles exceptions, wrapping them in IntermediateException with the key.
///
/// The decoder function uses the source code from the inner codec,
/// which is based on the last type argument of the Map.
///
/// Returns:
/// A String containing the Dart code for Map decoding.
String get _decodeMapSource {
final typeParam = MirrorTypeCodec(type.typeArguments.last);
return """
@ -185,6 +389,17 @@ return map;
""";
}
/// Generates source code for decoding a Configuration object.
///
/// This getter returns a string containing Dart code that:
/// 1. Creates a new instance of the Configuration subclass represented by [expectedType].
/// 2. Calls the `decode` method on this new instance, passing in the input value 'v'.
/// 3. Returns the decoded Configuration object.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding a Configuration object.
String get _decodeConfigSource {
return """
final item = $expectedType();
@ -195,6 +410,17 @@ return map;
""";
}
/// Generates source code for decoding an integer value.
///
/// This getter returns a string containing Dart code that:
/// 1. Checks if the input value 'v' is a String.
/// 2. If it is a String, parses it to an integer using `int.parse()`.
/// 3. If it's not a String, casts the value directly to an int.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding an integer value.
String get _decodeIntSource {
return """
if (v is String) {
@ -205,6 +431,17 @@ return map;
""";
}
/// Generates source code for decoding a boolean value.
///
/// This getter returns a string containing Dart code that:
/// 1. Checks if the input value 'v' is a String.
/// 2. If it is a String, returns true if it equals "true", false otherwise.
/// 3. If it's not a String, casts the value directly to a bool.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding a boolean value.
String get _decodeBoolSource {
return """
if (v is String) {
@ -216,6 +453,24 @@ return map;
}
}
/// Represents a property of a Configuration class, providing metadata and decoding capabilities.
///
/// This class encapsulates information about a single property within a Configuration
/// subclass, including its name, whether it's required, and how to decode its value.
///
/// It uses the [MirrorTypeCodec] to handle the decoding of values based on the property's type.
///
/// Key features:
/// - Extracts the property name from the [VariableMirror].
/// - Determines if the property is required based on its metadata.
/// - Provides access to the decoding logic through the [codec] field.
/// - Offers a method to decode input values, taking into account environment variables.
///
/// Usage:
/// ```dart
/// final property = MirrorConfigurationProperty(someVariableMirror);
/// final decodedValue = property.decode(inputValue);
/// ```
class MirrorConfigurationProperty {
MirrorConfigurationProperty(this.property)
: codec = MirrorTypeCodec(property.type);

View file

@ -8,21 +8,73 @@
*/
import 'dart:mirrors';
import 'package:protevus_config/config.dart';
import 'package:protevus_runtime/runtime.dart';
/// ConfigurationRuntimeImpl is a class that extends ConfigurationRuntime and implements SourceCompiler.
///
/// This class is responsible for handling the runtime configuration of the application. It uses
/// Dart's mirror system to introspect and manipulate configuration objects at runtime.
///
/// Key features:
/// - Decodes configuration input from a Map into a strongly-typed Configuration object
/// - Validates the configuration to ensure all required fields are present
/// - Generates implementation code for decoding and validating configurations
/// - Collects and manages configuration properties
///
/// The class provides methods for decoding input, validating configurations, and compiling
/// source code for runtime configuration handling. It also includes utility methods for
/// collecting properties and generating implementation strings for decode and validate operations.
class ConfigurationRuntimeImpl extends ConfigurationRuntime
implements SourceCompiler {
/// Constructs a ConfigurationRuntimeImpl instance for the given type.
///
/// The constructor initializes the type and properties of the configuration runtime.
/// It collects properties using the `_collectProperties` method.
///
/// Parameters:
/// - type: The ClassMirror representing the type of the configuration object.
ConfigurationRuntimeImpl(this.type) {
// Should be done in the constructor so a type check could be run.
properties = _collectProperties();
}
/// The ClassMirror representing the type of the configuration object.
///
/// This field stores the reflection information for the configuration class,
/// allowing for runtime introspection and manipulation of the configuration object.
final ClassMirror type;
/// A map of property names to MirrorConfigurationProperty objects.
///
/// This late-initialized field stores the configuration properties of the class.
/// Each key is a string representing the property name, and the corresponding value
/// is a MirrorConfigurationProperty object containing metadata about that property.
///
/// The properties are collected during the initialization of the ConfigurationRuntimeImpl
/// instance and are used for decoding, validating, and generating implementation code
/// for the configuration.
late final Map<String, MirrorConfigurationProperty> properties;
/// Decodes the input map into the given configuration object.
///
/// This method takes a [Configuration] object and a [Map] input, and populates
/// the configuration object with the decoded values from the input map.
///
/// The method performs the following steps:
/// 1. Creates a copy of the input map.
/// 2. Iterates through each property in the configuration.
/// 3. For each property, it attempts to decode the corresponding value from the input.
/// 4. If the decoded value is not null and of the correct type, it sets the value on the configuration object.
/// 5. After processing all properties, it checks if there are any unexpected keys left in the input map.
///
/// Throws a [ConfigurationException] if:
/// - A decoded value is of the wrong type.
/// - There are unexpected keys in the input map after processing all known properties.
///
/// Parameters:
/// - [configuration]: The Configuration object to be populated with decoded values.
/// - [input]: A Map containing the input values to be decoded.
@override
void decode(Configuration configuration, Map input) {
final values = Map.from(input);
@ -61,6 +113,24 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
}
}
/// Generates the implementation string for the decode method.
///
/// This getter creates a String representation of the decode method implementation.
/// The generated code does the following:
/// 1. Creates a copy of the input map.
/// 2. Iterates through each property in the configuration.
/// 3. For each property:
/// - Retrieves the value from the input, considering environment variables.
/// - If a value exists, it attempts to decode it.
/// - Checks if the decoded value is of the expected type.
/// - If valid, assigns the decoded value to the configuration object.
/// 4. After processing all properties, it checks for any unexpected keys in the input.
///
/// The generated code includes proper error handling, throwing ConfigurationExceptions
/// for type mismatches or unexpected input keys.
///
/// Returns:
/// A String containing the implementation code for the decode method.
String get decodeImpl {
final buf = StringBuffer();
@ -98,6 +168,22 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
return buf.toString();
}
/// Validates the given configuration object to ensure all required properties are present.
///
/// This method performs the following steps:
/// 1. Creates a mirror of the configuration object for reflection.
/// 2. Iterates through all properties of the configuration.
/// 3. For each property, it checks if:
/// - The property is required.
/// - The property value is null or cannot be accessed.
/// 4. Collects a list of all required properties that are missing or null.
/// 5. If any required properties are missing, it throws a ConfigurationException.
///
/// Parameters:
/// - configuration: The Configuration object to be validated.
///
/// Throws:
/// - ConfigurationException: If any required properties are missing or null.
@override
void validate(Configuration configuration) {
final configMirror = reflect(configuration);
@ -121,6 +207,23 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
}
}
/// Collects and returns a map of configuration properties for the current type.
///
/// This method traverses the class hierarchy, starting from the current type
/// up to (but not including) the Configuration class, collecting all non-static
/// and non-private variable declarations. It then creates a map where:
///
/// - Keys are the string names of the properties
/// - Values are MirrorConfigurationProperty objects created from the VariableMirrors
///
/// The method performs the following steps:
/// 1. Initializes an empty list to store VariableMirror objects.
/// 2. Traverses the class hierarchy, collecting relevant VariableMirrors.
/// 3. Creates a map from the collected VariableMirrors.
/// 4. Returns the resulting map of property names to MirrorConfigurationProperty objects.
///
/// Returns:
/// A Map<String, MirrorConfigurationProperty> representing the configuration properties.
Map<String, MirrorConfigurationProperty> _collectProperties() {
final declarations = <VariableMirror>[];
@ -142,6 +245,22 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
return m;
}
/// Generates the implementation string for the validate method.
///
/// This getter creates a String representation of the validate method implementation.
/// The generated code does the following:
/// 1. Initializes a list to store missing keys.
/// 2. Iterates through each property in the configuration.
/// 3. For each property:
/// - Attempts to retrieve the property value from the configuration object.
/// - Checks if the property is required and its value is null.
/// - If required and null, or if an error occurs during retrieval, adds the property name to the missing keys list.
/// 4. After checking all properties, throws a ConfigurationException if any keys are missing.
///
/// The generated code includes error handling to catch any issues during property access.
///
/// Returns:
/// A String containing the implementation code for the validate method.
String get validateImpl {
final buf = StringBuffer();
@ -170,6 +289,23 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
return buf.toString();
}
/// Compiles the configuration runtime implementation into a string representation.
///
/// This method generates the source code for a ConfigurationRuntimeImpl class
/// that extends ConfigurationRuntime. The generated class includes implementations
/// for the 'decode' and 'validate' methods.
///
/// The method performs the following steps:
/// 1. Retrieves import directives for the current type and adds them to the generated code.
/// 2. Adds an import for the intermediate_exception.dart file.
/// 3. Creates an instance of ConfigurationRuntimeImpl.
/// 4. Generates the class definition with implementations of decode and validate methods.
///
/// Parameters:
/// - ctx: A BuildContext object used to retrieve import directives.
///
/// Returns:
/// A Future<String> containing the generated source code for the ConfigurationRuntimeImpl class.
@override
Future<String> compile(BuildContext ctx) async {
final directives = await ctx.getImportDirectives(