add: adding config package

This commit is contained in:
Patrick Stewart 2024-12-15 22:51:33 -07:00
parent 5d53594f08
commit 40168bfe8d
11 changed files with 622 additions and 1 deletions

7
packages/config/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

View file

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

130
packages/config/README.md Normal file
View file

@ -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<T>(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<dynamic>? defaultValue])`: Get an array value.
- `getMany(List<String> 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.

View file

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

View file

@ -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');
}

View file

@ -0,0 +1,3 @@
library platform_config;
export 'src/repository.dart';

View file

@ -0,0 +1,206 @@
import 'package:platform_contracts/contracts.dart';
import 'package:platform_collections/collections.dart';
class Repository implements ConfigContract, Map<String, dynamic> {
static final Map<String, Function> _macros = {};
final Map<String, dynamic> _items;
Repository([Map<String, dynamic> 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<T>(String key, [T? defaultValue]) {
final value = Arr.get(_items, key);
if (value is T) {
return value;
}
return defaultValue;
}
Map<String, dynamic> getMany(List<String> keys) {
return Map.fromEntries(keys.map((key) => MapEntry(key, get(key))));
}
String string(String key, [String? defaultValue]) {
final value = get<dynamic>(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<dynamic>(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<dynamic>(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<dynamic>(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<dynamic> array(String key, [List<dynamic>? defaultValue]) {
final value = get<dynamic>(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<String, dynamic> 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<String> get keys => _items.keys;
@override
dynamic remove(Object? key) => _items.remove(key);
// Other Map interface methods...
@override
void addAll(Map<String, dynamic> other) => other.forEach(set);
@override
void addEntries(Iterable<MapEntry<String, dynamic>> newEntries) {
for (final entry in newEntries) {
set(entry.key, entry.value);
}
}
@override
Map<RK, RV> cast<RK, RV>() => _items.cast<RK, RV>();
@override
bool containsKey(Object? key) => has(key as String);
@override
bool containsValue(Object? value) => _items.containsValue(value);
@override
Iterable<MapEntry<String, dynamic>> 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<K2, V2> map<K2, V2>(
MapEntry<K2, V2> 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<dynamic> get values => _items.values;
dynamic callMacro(String name, List<dynamic> arguments) {
if (_macros.containsKey(name)) {
return Function.apply(_macros[name]!, [this, ...arguments]);
}
throw NoSuchMethodError.withInvocation(
this, Invocation.method(Symbol(name), arguments));
}
}

View file

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

View file

@ -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],
}));
});
});
}

View file

@ -3,7 +3,7 @@
/// This contract defines the standard way to interact with configuration values /// This contract defines the standard way to interact with configuration values
/// in the application. It provides methods to get, set, and manipulate /// in the application. It provides methods to get, set, and manipulate
/// configuration values in a consistent manner. /// configuration values in a consistent manner.
abstract class Repository { abstract class ConfigContract {
/// Determine if the given configuration value exists. /// Determine if the given configuration value exists.
/// ///
/// Example: /// Example: