diff --git a/packages/config/lib/src/compiler.dart b/packages/config/lib/src/compiler.dart index 9f538fa..a594aaf 100644 --- a/packages/config/lib/src/compiler.dart +++ b/packages/config/lib/src/compiler.dart @@ -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] where each entry represents a + /// configuration class and its runtime implementation. @override Map 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';", ""), ); } } diff --git a/packages/config/lib/src/configuration.dart b/packages/config/lib/src/configuration.dart index 60b2053..9ffcd6b 100644 --- a/packages/config/lib/src/configuration.dart +++ b/packages/config/lib/src/configuration.dart @@ -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 map) { decode(map.map((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?; 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 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 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"; diff --git a/packages/config/lib/src/default_configurations.dart b/packages/config/lib/src/default_configurations.dart index 6838e7d..f46b9b5 100644 --- a/packages/config/lib/src/default_configurations.dart +++ b/packages/config/lib/src/default_configurations.dart @@ -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; diff --git a/packages/config/lib/src/intermediate_exception.dart b/packages/config/lib/src/intermediate_exception.dart index e457766..193ede1 100644 --- a/packages/config/lib/src/intermediate_exception.dart +++ b/packages/config/lib/src/intermediate_exception.dart @@ -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 keyPath; } diff --git a/packages/config/lib/src/mirror_property.dart b/packages/config/lib/src/mirror_property.dart index 0318a7d..3230821 100644 --- a/packages/config/lib/src/mirror_property.dart +++ b/packages/config/lib/src/mirror_property.dart @@ -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 _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); diff --git a/packages/config/lib/src/runtime.dart b/packages/config/lib/src/runtime.dart index 9353593..deda901 100644 --- a/packages/config/lib/src/runtime.dart +++ b/packages/config/lib/src/runtime.dart @@ -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 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 representing the configuration properties. Map _collectProperties() { final declarations = []; @@ -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 containing the generated source code for the ConfigurationRuntimeImpl class. @override Future compile(BuildContext ctx) async { final directives = await ctx.getImportDirectives(