diff --git a/packages/config/.gitignore b/packages/config/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/config/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/packages/config/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/config/LICENSE.md b/packages/config/LICENSE.md new file mode 100644 index 0000000..0fd0d03 --- /dev/null +++ b/packages/config/LICENSE.md @@ -0,0 +1,10 @@ +The MIT License (MIT) + +The Laravel Framework is Copyright (c) Taylor Otwell +The Fabric Framework is Copyright (c) Vieo, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/config/README.md b/packages/config/README.md new file mode 100644 index 0000000..6236d0b --- /dev/null +++ b/packages/config/README.md @@ -0,0 +1,130 @@ +# Platform Config + +A Dart implementation of Laravel-inspired configuration management for the Protevus platform. + +## Features + +- Flexible configuration storage and retrieval +- Support for nested configuration keys +- Type-safe retrieval methods (string, integer, float, boolean, array) +- Implementation of Dart's `Map` interface for familiar usage +- Macro system for extending functionality at runtime + +## Installation + +Add this package to your `pubspec.yaml`: + +```yaml +dependencies: + platform_config: ^1.0.0 +``` + +Then run: + +``` +dart pub get +``` + +## Usage + +Here's a basic example of how to use the `Repository` class: + +```dart +import 'package:platform_config/platform_config.dart'; + +void main() { + final config = Repository({ + 'app': { + 'name': 'My App', + 'debug': true, + }, + 'database': { + 'default': 'mysql', + 'connections': { + 'mysql': { + 'host': 'localhost', + 'port': 3306, + }, + }, + }, + }); + + // Get a value + print(config.get('app.name')); // Output: My App + + // Get a typed value + final isDebug = config.boolean('app.debug'); + print(isDebug); // Output: true + + // Get a nested value + final dbPort = config.integer('database.connections.mysql.port'); + print(dbPort); // Output: 3306 + + // Set a value + config.set('app.version', '1.0.0'); + + // Check if a key exists + print(config.has('app.version')); // Output: true + + // Get multiple values + final values = config.getMany(['app.name', 'app.debug']); + print(values); // Output: {app.name: My App, app.debug: true} +} +``` + +### Available Methods + +- `get(String key, [T? defaultValue])`: Get a value by key, optionally specifying a default value. +- `set(dynamic key, dynamic value)`: Set a value for a key. +- `has(String key)`: Check if a key exists in the configuration. +- `string(String key, [String? defaultValue])`: Get a string value. +- `integer(String key, [int? defaultValue])`: Get an integer value. +- `float(String key, [double? defaultValue])`: Get a float value. +- `boolean(String key, [bool? defaultValue])`: Get a boolean value. +- `array(String key, [List? defaultValue])`: Get an array value. +- `getMany(List keys)`: Get multiple values at once. +- `all()`: Get all configuration items. +- `prepend(String key, dynamic value)`: Prepend a value to an array. +- `push(String key, dynamic value)`: Append a value to an array. + +The `Repository` class also implements Dart's `Map` interface, so you can use it like a regular map: + +```dart +config['new.key'] = 'new value'; +print(config['new.key']); // Output: new value +``` + +## Error Handling + +The type-safe methods (`string()`, `integer()`, `float()`, `boolean()`, `array()`) will throw an `ArgumentError` if the value at the specified key is not of the expected type. + +## Extending Functionality + +You can extend the `Repository` class with custom methods using the macro system: + +```dart +Repository.macro('getConnectionUrl', (Repository repo, String connection) { + final conn = repo.get('database.connections.$connection'); + return 'mysql://${conn['username']}:${conn['password']}@${conn['host']}:${conn['port']}/${conn['database']}'; +}); + +final config = Repository(/* ... */); +final mysqlUrl = config.callMacro('getConnectionUrl', ['mysql']); +print(mysqlUrl); // Output: mysql://user:password@localhost:3306/dbname +``` + +## Testing + +To run the tests for this package, use the following command: + +``` +dart test +``` + +## Contributing + +Contributions are welcome! Please read our contributing guidelines before submitting pull requests. + +## License + +This project is licensed under the MIT License. diff --git a/packages/config/analysis_options.yaml b/packages/config/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/packages/config/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/config/example/config_example.dart b/packages/config/example/config_example.dart new file mode 100644 index 0000000..4b7d5de --- /dev/null +++ b/packages/config/example/config_example.dart @@ -0,0 +1,104 @@ +import 'package:platform_config/platform_config.dart'; + +void main() { + // Create a new Repository instance with some initial configuration + final config = Repository({ + 'app': { + 'name': 'Protevus Demo App', + 'version': '1.0.0', + 'debug': true, + }, + 'database': { + 'default': 'mysql', + 'connections': { + 'mysql': { + 'host': 'localhost', + 'port': 3306, + 'database': 'protevus_demo', + 'username': 'demo_user', + 'password': 'secret', + }, + 'redis': { + 'host': 'localhost', + 'port': 6379, + }, + }, + }, + 'cache': { + 'default': 'redis', + 'stores': { + 'redis': { + 'driver': 'redis', + 'connection': 'default', + }, + 'file': { + 'driver': 'file', + 'path': '/tmp/cache', + }, + }, + }, + 'logging': { + 'channels': ['file', 'console'], + 'level': 'info', + }, + }); + + // Demonstrate usage of various methods + print('Application Name: ${config.string('app.name')}'); + print('Debug Mode: ${config.boolean('app.debug') ? 'Enabled' : 'Disabled'}'); + + // Using get with a default value + print('API Version: ${config.get('app.api_version', 'v1')}'); + + // Accessing nested configuration + final dbConfig = config.get('database.connections.mysql'); + print('Database Configuration:'); + print(' Host: ${dbConfig['host']}'); + print(' Port: ${dbConfig['port']}'); + print(' Database: ${dbConfig['database']}'); + + // Using type-specific getters + final redisPort = config.integer('database.connections.redis.port'); + print('Redis Port: $redisPort'); + + // Checking for existence of a key + if (config.has('cache.stores.memcached')) { + print('Memcached configuration exists'); + } else { + print('Memcached configuration does not exist'); + } + + // Setting a new value + config.set('app.timezone', 'UTC'); + print('Timezone: ${config.string('app.timezone')}'); + + // Getting multiple values at once + final loggingConfig = config.getMany(['logging.channels', 'logging.level']); + print('Logging Configuration:'); + print(' Channels: ${loggingConfig['logging.channels']}'); + print(' Level: ${loggingConfig['logging.level']}'); + + // Using array method + final logChannels = config.array('logging.channels'); + print('Log Channels: $logChannels'); + + // Demonstrating error handling + try { + config.integer('app.name'); + } catch (e) { + print('Error: $e'); + } + + // Using the Repository as a Map + config['new.feature.enabled'] = true; + print('New Feature Enabled: ${config['new.feature.enabled']}'); + + // Demonstrating Macroable functionality + Repository.macro('getConnectionUrl', (Repository repo, String connection) { + final conn = repo.get('database.connections.$connection'); + return 'mysql://${conn['username']}:${conn['password']}@${conn['host']}:${conn['port']}/${conn['database']}'; + }); + + final mysqlUrl = config.callMacro('getConnectionUrl', ['mysql']); + print('MySQL Connection URL: $mysqlUrl'); +} diff --git a/packages/config/lib/platform_config.dart b/packages/config/lib/platform_config.dart new file mode 100644 index 0000000..92a8aeb --- /dev/null +++ b/packages/config/lib/platform_config.dart @@ -0,0 +1,3 @@ +library platform_config; + +export 'src/repository.dart'; diff --git a/packages/config/lib/src/repository.dart b/packages/config/lib/src/repository.dart new file mode 100644 index 0000000..c48c250 --- /dev/null +++ b/packages/config/lib/src/repository.dart @@ -0,0 +1,206 @@ +import 'package:platform_contracts/contracts.dart'; +import 'package:platform_collections/collections.dart'; + +class Repository implements ConfigContract, Map { + static final Map _macros = {}; + + final Map _items; + + Repository([Map items = const {}]) + : _items = Map.from(items); + + static void macro(String name, Function macro) { + _macros[name] = macro; + } + + @override + bool has(String key) { + return Arr.has(_items, key); + } + + @override + T? get(String key, [T? defaultValue]) { + final value = Arr.get(_items, key); + if (value is T) { + return value; + } + return defaultValue; + } + + Map getMany(List keys) { + return Map.fromEntries(keys.map((key) => MapEntry(key, get(key)))); + } + + String string(String key, [String? defaultValue]) { + final value = get(key); + if (value is String) { + return value; + } + if (value == null && defaultValue != null) { + return defaultValue; + } + throw ArgumentError( + 'Configuration value for key [$key] must be a string, ${value.runtimeType} given.'); + } + + int integer(String key, [int? defaultValue]) { + final value = get(key); + if (value is int) { + return value; + } + if (value == null && defaultValue != null) { + return defaultValue; + } + throw ArgumentError( + 'Configuration value for key [$key] must be an integer, ${value.runtimeType} given.'); + } + + double float(String key, [double? defaultValue]) { + final value = get(key); + if (value is double) { + return value; + } + if (value == null && defaultValue != null) { + return defaultValue; + } + throw ArgumentError( + 'Configuration value for key [$key] must be a double, ${value.runtimeType} given.'); + } + + bool boolean(String key, [bool? defaultValue]) { + final value = get(key); + if (value is bool) { + return value; + } + if (value == null && defaultValue != null) { + return defaultValue; + } + throw ArgumentError( + 'Configuration value for key [$key] must be a boolean, ${value.runtimeType} given.'); + } + + List array(String key, [List? defaultValue]) { + final value = get(key); + if (value is List) { + return value; + } + if (value == null && defaultValue != null) { + return defaultValue; + } + throw ArgumentError( + 'Configuration value for key [$key] must be a List, ${value.runtimeType} given.'); + } + + @override + void set(dynamic key, dynamic value) { + Arr.set(_items, key, value); + } + + @override + void prepend(String key, dynamic value) { + final list = array(key, []); + list.insert(0, value); + set(key, list); + } + + @override + void push(String key, dynamic value) { + final list = array(key, []); + list.add(value); + set(key, list); + } + + @override + Map all() => Map.from(_items); + + // Implement Map interface + @override + dynamic operator [](Object? key) => get(key as String); + + @override + void operator []=(String key, dynamic value) => set(key, value); + + @override + void clear() => _items.clear(); + + @override + Iterable get keys => _items.keys; + + @override + dynamic remove(Object? key) => _items.remove(key); + + // Other Map interface methods... + @override + void addAll(Map other) => other.forEach(set); + + @override + void addEntries(Iterable> newEntries) { + for (final entry in newEntries) { + set(entry.key, entry.value); + } + } + + @override + Map cast() => _items.cast(); + + @override + bool containsKey(Object? key) => has(key as String); + + @override + bool containsValue(Object? value) => _items.containsValue(value); + + @override + Iterable> get entries => _items.entries; + + @override + void forEach(void Function(String key, dynamic value) action) { + _items.forEach(action); + } + + @override + bool get isEmpty => _items.isEmpty; + + @override + bool get isNotEmpty => _items.isNotEmpty; + + @override + int get length => _items.length; + + @override + Map map( + MapEntry Function(String key, dynamic value) convert) { + return _items.map(convert); + } + + @override + dynamic putIfAbsent(String key, dynamic Function() ifAbsent) { + return _items.putIfAbsent(key, ifAbsent); + } + + @override + void removeWhere(bool Function(String key, dynamic value) test) { + _items.removeWhere(test); + } + + @override + dynamic update(String key, dynamic Function(dynamic value) update, + {dynamic Function()? ifAbsent}) { + return _items.update(key, update, ifAbsent: ifAbsent); + } + + @override + void updateAll(dynamic Function(String key, dynamic value) update) { + _items.updateAll(update); + } + + @override + Iterable get values => _items.values; + + dynamic callMacro(String name, List arguments) { + if (_macros.containsKey(name)) { + return Function.apply(_macros[name]!, [this, ...arguments]); + } + throw NoSuchMethodError.withInvocation( + this, Invocation.method(Symbol(name), arguments)); + } +} diff --git a/packages/config/pubspec.yaml b/packages/config/pubspec.yaml new file mode 100644 index 0000000..05c4583 --- /dev/null +++ b/packages/config/pubspec.yaml @@ -0,0 +1,18 @@ +name: platform_config +description: A Dart implementation of Laravel's Config package for the Protevus platform. +version: 1.0.0 +homepage: https://protevus.com +documentation: https://docs.protevus.com +repository: https://github.com/protevus/platformo + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + platform_contracts: ^1.0.0 + platform_collections: ^1.0.0 + platform_macroable: ^1.0.0 + +dev_dependencies: + test: ^1.16.0 + lints: ^2.0.0 diff --git a/packages/config/test/repository_test.dart b/packages/config/test/repository_test.dart new file mode 100644 index 0000000..16ed863 --- /dev/null +++ b/packages/config/test/repository_test.dart @@ -0,0 +1,110 @@ +import 'package:platform_config/src/repository.dart'; +import 'package:test/test.dart'; + +void main() { + late Repository config; + + setUp(() { + config = Repository({ + 'app': { + 'name': 'My App', + 'debug': true, + }, + 'database': { + 'default': 'mysql', + 'connections': { + 'mysql': { + 'host': 'localhost', + 'port': 3306, + }, + }, + }, + 'numbers': [1, 2, 3, 4, 5], + }); + }); + + group('Repository', () { + test('has() returns correct boolean for existing and non-existing keys', + () { + expect(config.has('app.name'), isTrue); + expect(config.has('app.non_existent'), isFalse); + }); + + test('get() returns correct values for existing keys', () { + expect(config.get('app.name'), equals('My App')); + expect(config.get('database.connections.mysql.port'), equals(3306)); + }); + + test('get() returns default value for non-existing keys', () { + expect(config.get('non_existent', 'default'), equals('default')); + }); + + test('string() returns correct string value', () { + expect(config.string('app.name'), equals('My App')); + }); + + test('string() throws ArgumentError for non-string values', () { + expect(() => config.string('app.debug'), throwsArgumentError); + }); + + test('integer() returns correct integer value', () { + expect(config.integer('database.connections.mysql.port'), equals(3306)); + }); + + test('integer() throws ArgumentError for non-integer values', () { + expect(() => config.integer('app.name'), throwsArgumentError); + }); + + test('boolean() returns correct boolean value', () { + expect(config.boolean('app.debug'), isTrue); + }); + + test('boolean() throws ArgumentError for non-boolean values', () { + expect(() => config.boolean('app.name'), throwsArgumentError); + }); + + test('array() returns correct list value', () { + expect(config.array('numbers'), equals([1, 2, 3, 4, 5])); + }); + + test('array() throws ArgumentError for non-list values', () { + expect(() => config.array('app.name'), throwsArgumentError); + }); + + test('set() correctly sets a new value', () { + config.set('new.key', 'new value'); + expect(config.get('new.key'), equals('new value')); + }); + + test('prepend() correctly prepends a value to an array', () { + config.prepend('numbers', 0); + expect(config.array('numbers'), equals([0, 1, 2, 3, 4, 5])); + }); + + test('push() correctly appends a value to an array', () { + config.push('numbers', 6); + expect(config.array('numbers'), equals([1, 2, 3, 4, 5, 6])); + }); + + test('all() returns all config items', () { + expect( + config.all(), + equals({ + 'app': { + 'name': 'My App', + 'debug': true, + }, + 'database': { + 'default': 'mysql', + 'connections': { + 'mysql': { + 'host': 'localhost', + 'port': 3306, + }, + }, + }, + 'numbers': [1, 2, 3, 4, 5], + })); + }); + }); +} diff --git a/packages/contracts/lib/src/config/repository.dart b/packages/contracts/lib/src/config/repository.dart index 67e214b..b1dfe33 100644 --- a/packages/contracts/lib/src/config/repository.dart +++ b/packages/contracts/lib/src/config/repository.dart @@ -3,7 +3,7 @@ /// This contract defines the standard way to interact with configuration values /// in the application. It provides methods to get, set, and manipulate /// configuration values in a consistent manner. -abstract class Repository { +abstract class ConfigContract { /// Determine if the given configuration value exists. /// /// Example: