diff --git a/README.md b/README.md index 93a7116e..9e8b1a10 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Angel Configuration -![version 1.0.3](https://img.shields.io/badge/version-1.0.3-brightgreen.svg) -![build status](https://travis-ci.org/angel-dart/configuration.svg) +[![Pub](https://img.shields.io/pub/v/angel_configuration.svg)](https://pub.dartlang.org/packages/angel_configuration) +[![build status](https://travis-ci.org/angel-dart/configuration.svg)](https://travis-ci.org/angel-dart/configuration.svg) -Isomorphic YAML configuration loader for Angel. +Automatic YAML configuration loader for Angel. # About Any web app needs different configuration for development and production. This plugin will search -for a `config/default.yaml` file. If it is found, configuration from it is loaded into `angel.properties`. +for a `config/default.yaml` file. If it is found, configuration from it is loaded into `app.properties`. Then, it will look for a `config/$ANGEL_ENV` file. (i.e. config/development.yaml). If this found, all of its configuration be loaded, and will override anything loaded from the `default.yaml` file. This allows for your app to work under different conditions without you re-coding anything. :) @@ -22,7 +22,37 @@ dependencies: # Usage +**Example Configuration** +```yaml +# Define normal YAML objects +some_key: foo +this_is_a_map: + a_string: "string" + another_string: "string" + +``` + +You can also load configuration from the environment: +```yaml +# Loaded from the environment +system_path: $PATH +``` + +If a `.env` file is present in your configuration directory, then it will be loaded before +applying YAML configuration. + **Server-side** +Call `loadConfigurationFile()`. The loaded properties will be available in your application's +`properties` map, which means you can access them like normal instance members. + +```dart +main() { + print(app.foo == app.properties['foo']); // true +} +``` + +An instance of `Configuration` will also be injected to your application, and it works +the same way: ```dart import 'dart:io'; @@ -41,7 +71,7 @@ main() async { `loadConfigurationFile` also accepts a `sourceDirectory` or `overrideEnvironmentName` parameter. The former will allow you to search in a directory other than `config`, and the latter lets you -override `$ANGEL_ENV` by specifying a specific configuration name to look for (i.e. 'production'). +override `$ANGEL_ENV` by specifying a specific configuration name to look for (i.e. `production`). **In the Browser** diff --git a/lib/angel_configuration.dart b/lib/angel_configuration.dart index b3dcc319..6e451095 100644 --- a/lib/angel_configuration.dart +++ b/lib/angel_configuration.dart @@ -2,14 +2,16 @@ library angel_configuration; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_route/src/extensible.dart'; +import 'package:dotenv/dotenv.dart' as dotenv; import 'package:yaml/yaml.dart'; final RegExp _equ = new RegExp(r'=$'); final RegExp _sym = new RegExp(r'Symbol\("([^"]+)"\)'); +/// A proxy object that encapsulates a server's configuration. @proxy class Configuration { + /// The [Angel] instance that loaded this configuration. final Angel app; Configuration(this.app); @@ -21,8 +23,8 @@ class Configuration { String name = _sym.firstMatch(invocation.memberName.toString()).group(1); if (invocation.isMethod) { - return Function.apply(app.properties[name], invocation.positionalArguments, - invocation.namedArguments); + return Function.apply(app.properties[name], + invocation.positionalArguments, invocation.namedArguments); } else if (invocation.isGetter) { return app.properties[name]; } @@ -32,34 +34,74 @@ class Configuration { } } -_loadYamlFile(Angel app, File yamlFile) async { +_loadYamlFile(Angel app, File yamlFile, Map env) async { if (await yamlFile.exists()) { - Map config = loadYaml(await yamlFile.readAsString()); + var config = loadYaml(await yamlFile.readAsString()); + if (config is! Map) { + stderr.writeln( + 'WARNING: The configuration at "${yamlFile.absolute.path}" is not a Map. Refusing to load it.'); + return; + } for (String key in config.keys) { - app.properties[key] = config[key]; + app.properties[key] = _applyEnv(config[key], env ?? {}); } } } -loadConfigurationFile( +_applyEnv(var v, Map env) { + if (v is String) { + if (v.startsWith(r'$') && v.length > 1) { + var key = v.substring(1); + if (env.containsKey(key)) + return env[key]; + else { + stderr.writeln( + 'Your configuration calls for loading the value of "$key" from the system environment, but it is not defined. Defaulting to `null`.'); + return null; + } + } else + return v; + } else if (v is Iterable) { + return v.map((x) => _applyEnv(x, env ?? {})).toList(); + } else if (v is Map) { + return v.keys + .fold({}, (out, k) => out..[k] = _applyEnv(v[k], env ?? {})); + } else + return v; +} + +/// Dynamically loads application configuration from configuration files. +AngelConfigurer loadConfigurationFile( {String directoryPath: "./config", String overrideEnvironmentName}) { return (Angel app) async { Directory sourceDirectory = new Directory(directoryPath); - String environmentName = Platform.environment['ANGEL_ENV'] ?? 'development'; + var env = dotenv.env; + var envFile = new File.fromUri(sourceDirectory.uri.resolve('.env')); + + if (await envFile.exists()) { + try { + dotenv.load(envFile.absolute.uri.toFilePath()); + } catch (_) { + stderr.writeln( + 'WARNING: Found an environment configuration at ${envFile.absolute.path}, but it was invalidly formatted. Refusing to load it.'); + } + } + + String environmentName = env['ANGEL_ENV'] ?? 'development'; if (overrideEnvironmentName != null) { environmentName = overrideEnvironmentName; } - File defaultYaml = new File.fromUri( - sourceDirectory.absolute.uri.resolve("default.yaml")); - await _loadYamlFile(app, defaultYaml); + File defaultYaml = + new File.fromUri(sourceDirectory.absolute.uri.resolve("default.yaml")); + await _loadYamlFile(app, defaultYaml, env); String configFilePath = "$environmentName.yaml"; - File configFile = new File.fromUri( - sourceDirectory.absolute.uri.resolve(configFilePath)); + File configFile = + new File.fromUri(sourceDirectory.absolute.uri.resolve(configFilePath)); - await _loadYamlFile(app, configFile); + await _loadYamlFile(app, configFile, env); app.container.singleton(new Configuration(app)); }; -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index faddbc34..d46edc7f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_configuration -description: Isomorphic YAML configuration loader for Angel. -version: 1.0.3 +description: Automatic YAML configuration loader for Angel. +version: 1.0.4 author: Tobe O homepage: https://github.com/angel-dart/angel_configuration dependencies: @@ -8,6 +8,7 @@ dependencies: angel_framework: ">=1.0.0-dev < 2.0.0" angel_route: ">=1.0.0-dev < 2.0.0" barback: ">=0.15.2 < 0.16.0" + dotenv: ">=0.1.3 <0.2.0" yaml: ">= 2.1.8 < 2.2.0" dev_dependencies: test: ">= 0.12.13 < 0.13.0" diff --git a/test/all_test.dart b/test/all_test.dart index cc5b752a..cdef5909 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -5,27 +5,37 @@ import 'transformer.dart' as transformer; main() async { // Note: Set ANGEL_ENV to 'development' - - Angel angel = new Angel(); - await angel.configure( + var app = new Angel(); + await app.configure( loadConfigurationFile(directoryPath: './test/config')); test('can load based on ANGEL_ENV', () async { - expect(angel.properties['hello'], equals('world')); - expect(angel.properties['foo']['version'], equals('bar')); + expect(app.properties['hello'], equals('world')); + expect(app.properties['foo']['version'], equals('bar')); }); test('will load default.yaml if exists', () { - expect(angel.properties["set_via"], equals("default")); + expect(app.properties["set_via"], equals("default")); }); + test('will load .env if exists', () { + expect(app.properties['artist'], 'Timberlake'); + expect(app.properties['angel'], {'framework': 'cool'}); + }); + + test('non-existent environment defaults to null', () { + expect(app.properties.keys, contains('must_be_null')); + expect(app.properties['must_be_null'], null); + }); test('can override ANGEL_ENV', () async { - await angel.configure(loadConfigurationFile( + await app.configure(loadConfigurationFile( directoryPath: './test/config', overrideEnvironmentName: 'override')); - expect(angel.properties['hello'], equals('goodbye')); - expect(angel.properties['foo']['version'], equals('baz')); + expect(app.properties['hello'], equals('goodbye')); + expect(app.properties['foo']['version'], equals('baz')); }); + + group("transformer", transformer.main); } diff --git a/test/config/.env b/test/config/.env new file mode 100644 index 00000000..89c5d02b --- /dev/null +++ b/test/config/.env @@ -0,0 +1,2 @@ +ANGEL_FRAMEWORK=cool +JUSTIN=Timberlake \ No newline at end of file diff --git a/test/config/default.yaml b/test/config/default.yaml index 5a95bc23..11f67070 100644 --- a/test/config/default.yaml +++ b/test/config/default.yaml @@ -1 +1,5 @@ -set_via: default \ No newline at end of file +set_via: default +artist: $JUSTIN +angel: + framework: $ANGEL_FRAMEWORK +must_be_null: $NONEXISTENT_KEY_FOO_BAR_BAZ_QUUX \ No newline at end of file