embedded_config 0.1.3

  • Readme
  • Changelog
  • Example
  • Installing
  • 69

embedded_config #

A package which allows application configurations to be embedded directly into source code at build-time. Configuration can come from JSON files, environment variables, and build.yaml files.

Contents #

Usage #

1. Installing #

This package makes use of package:build_runner. To avoid making build_runner a normal dependency, embedded_config has a partner package embedded_config_annotations.

The embedded_config_annotations package should be added as a normal dependency and embedded_config should be added as a dev dependency:

dependencies:
  embedded_config_annotations: ^0.1.0

dev_dependencies:
  embedded_config: ^0.1.0

The build_runner package should also be added as a dev dependency to your application.

2. Create an embedded config class #

Configuration is embedded into your application by generating a class that extends an abstract "embedded config" class in your application. The generated extending class contains hard-coded configuration values, which come from configuration sources (we'll get to that next).

Create an abstract class annotated with @EmbeddedConfig. This class must also have a default const constructor. The @EmbeddedConfig annotation is given a configuration source key which we'll connect later. In this class, you may define abstract getters which map to keys from configuration sources.

// file: app_config.dart

import 'package:embedded_config_annotations/embedded_config_annotations.dart';

// Add the generated file as a part
part 'app_config.embedded.dart';

@EmbeddedConfig('app_config')
abstract class AppConfig {
  String get apiUrl;

  const AppConfig();
}

Getter return types can be any of the following:

Note: When the element type of a list is explicitly String or the value type of a map is explicitly String, values from config sources will automatically be converted to strings.

Example: A getter of List<String> given the config value of [24] will get an embedded value of ["24"].

3. Map configuration source(s) #

Next, we need the actual configuration. Configuration can be specified in two places: JSON files in your application package and build.yaml files. Both of these sources can additionally make use of environment variables.

Configuration sources are mapped to annotated classes inside of your application's build.yaml files. This allows you to swap out configuration values for different builds.

In any of the application package's build.*.yaml files, configure the embedded_config builder to map configuration sources:

targets:
  $default:
    builders:
      embedded_config:
        options:
          # Maps the configuration source key 'app_config' to the
          # JSON file at 'lib/app_config.json'
          #
          # The key 'app_config' is the same key given to the
          # @EmbeddedConfig annotation on the embedded config class
          app_config: 'lib/app_config.json'

          # This can also be written like so, both mean the same thing
          # app_config:
          #  source: 'lib/app_config.json'

To complete the example, given the AppConfig class defined earlier, the app_config.json file could contain:

{
  "apiUrl": "/api"
}

When the application package is then built using the build.yaml file configuring embedded_config, a file is generated containing the hard-coded configuration values extending the app_config.dart file defined earlier. These generated files are always in the same directory as the annotated class's file and is named <original file name>.embedded.dart (in this case it would be named app_config.embedded.dart).

The contents of the generated file in this case would look like:

part of 'app_config.dart';

class _$AppConfigEmbedded extends AppConfig {
  const _$AppConfigEmbedded();

  @override
  final apiUrl = '/api';
}

The annotated AppConfig class can then expose the generated class in any way you choose. Since embedded configs are marked with const constructors, one nice way is to expose it as a static const singleton value:

// ...

@EmbeddedConfig('app_config')
abstract class AppConfig {
  static const AppConfig instance = _$AppConfigEmbedded();

  // ...
}

Merging configuration #

Multiple configuration sources can be mapped to a single annotated class. This could allow you to define a "base" app_config.json and then environment specific app_config.dev.json and app_config.prod.json files. A build.dev.yaml file could map the dev json onto the base file, and build.prod.yaml with prod.

For example, lets define two files app_config.json and app_config.dev.json:

// app_config.json
{
  "prop1": "value1",
  "sub": {
    "prop2": true
  }
}

// app_config.dev.json
{
  "prop1": "value2",
  "sub": {
    "prop3": "value3"
  }
}

Then, in build.dev.yaml specify both as a source with dev being last so that it overrides the base file:

targets:
  $default:
    builders:
      embedded_config:
        options:
          app_config:
            # Order matters, later sources override earlier sources!
            source:
              - 'lib/app_config.json'
              - 'lib/app_config.dev.json'

When merging configurations, values are overridden at the lowest level possible! This means that building with build.dev.yaml in this example would use the equivalent of this merged JSON document:

{
  "prop1": "value2",
  "sub": {
    "prop2": true,
    "prop3": "value3"
  }
}

Keys cannot be removed when merging configurations. Properties can of course still be set to null (in this example the sub object could be "removed" by setting it to null, but prop2 inside of sub cannot be removed otherwise).

Inline configuration sources #

Configuration can also be specified directly in build.yaml files. This is done through the inline property:

targets:
  $default:
    builders:
      embedded_config:
        options:
          app_config:
            inline:
              apiUrl: '/api2'

Inline configuration can also be combined with file sources, however inline will always be applied last and override all file sources. See Merging configuration to understand how inline sources would override file sources, it works the same as a file overriding another file.

Complex configuration models #

When configuration is not a flat set of key/value pairs, multiple annotated embedded config classes can be used. This works by setting the path property of the @EmbeddedConfig annotation. The path is a . separated list of configuration keys.

For example, to embed the following configuration:

{
  "prop1": "value1",
  "sub": {
    "prop2": "value2",
    "sub2": {
      "prop3": "value3"
    }
  }
}

You would need to create three annotated classes:

import 'package:embedded_config_annotations/embedded_config_annotations.dart';

// Embeds the top-level of the configuration
@EmbeddedConfig('app_config')
abstract class AppConfig {
  String get prop1;
  // Other embedded config classes in the same Dart library 
  // can be referenced as a getter type
  AppSubConfig get sub;

  const AppConfig();
}

// Embeds the top-level contents of the "sub" object
@EmbeddedConfig('app_config', path: 'sub')
abstract class AppSubConfig {
  String get prop2;
  AppSub2Config get sub2;

  const AppSubConfig();
}

// Embeds the top-level contents of the "sub2" object inside
// of the "sub" object
@EmbeddedConfig('app_config', path: 'sub.sub2')
abstract class AppSub2Config {
  String get prop3;

  const AppSub2Config();
}

The path property can also be used outside of this use-case and does not require a 'parent' class to also be defined.

Note: Any annotated classes which reference each other must be declared in the same Dart library.

Environment variables #

Environment variables can be substituted for any string value in the configuration. This is done by starting a value with $. For example, $BUILD_ID would be substituted with the value of the environment variable BUILD_ID.

Escaping environment variables #

If a configuration value literally starts with $ and is not intended to be substituted for an environment variable, you can escape it with another $. For example, to embed the literal value $BUILD_ID your configuration would need the value $$BUILD_ID. This also means that embedding the literal value $$BUILD_ID requires the configuration value $$$BUILD_ID and so forth as any value starting with $$ has those two first characters replaced with a single $.

Example: Embedding a build identifier from CI #

The following is an example of how you could embed a build identifier from CI exposed as an environment variable into your application:

import 'package:embedded_config_annotations/embedded_config_annotations.dart';

@EmbeddedConfig('environment')
abstract class Environment {
  String get buildId;

  const Environment();
}

You could specify the environment variable in a JSON source:

{
  "buildId": "$BUILD_ID"
}

Or, more simply in this case, specify it inline in build.yaml:

targets:
  $default:
    builders:
      embedded_config:
        options:
          environment:
            inline:
              buildId: '$BUILD_ID'

v0.1.3 #

  • Non-abstract getters are now ignored when generating the embedded config class.

v0.1.2 #

  • Fix usage of multiple configuration sources with embedded configs mapped to specific paths not working.

v0.1.1 #

  • Address a few package scoring issues including public API documentation.

v0.1.0 #

  • Configuration can now be sourced from JSON files and/or inline build.yaml.
  • Build errors now specify the language element which caused the error.
  • Added support for numeric configuration values.
  • Added support for non-string-only lists.
  • Added support for environment variables.

example/example.md

This example showcases how to embed a multi-level JSON config into an application, additionally including the use of environment variables.

Assuming the config file is the following and is located at lib/app_config.json:

{
    "apiUrl": "/api",
    "auth": {
        "clientId": "$AUTH_CLIENT_ID"
    }
}

The previous file can be represented in code using the following classes:

// file: lib/app_config.dart

import 'package:embedded_config_annotations/embedded_config_annotations.dart';

part 'app_config.embedded.dart';

// Mark an *abstract* class with @EmbeddedConfig and
// assign a key to allow this class to be mapped to
// config sources.
@EmbeddedConfig('app_config')
abstract class AppConfig {
  // An example of one way to expose the generated class which
  // contains the embedded config values.
  static const AppConfig instance = _$AppConfigEmbedded();

  // Value will be set to '/api'.
  String get apiUrl;

  // Value will be set to an instance of the generated
  // embedded config class of AppAuthConfig.
  AppAuthConfig get auth;

  // Embedded config classes must declare a default
  // const constructor.
  const AppConfig();
}

// Since this class represents a sub-object in the config,
// specify a '.' separated 'path' to the object from the
// root of the config.
@EmbeddedConfig('app_config', path: 'auth')
abstract class AppAuthConfig {
  // Value will be set to the value of the environment 
  // variable 'AUTH_CLIENT_ID' as stated in the app_config.json
  // file at the time of build.
  String get clientId;

  const AppAuthConfig();
}

After this, the classes must be mapped to at least one config source by the key defined on each class inside of any build.yaml file in the project. Use different build.yaml files (e.g. build.prod.yaml vs build.dev.yaml) to change the source of config values embedded into the application at build time. This allows you to configure your application for different environments.

targets:
  $default:
    builders:
      embedded_config:
        # Optional, but may provide a build performance boost as
        # the builder does analyze all Dart files by default
        # generate_for:
        #   - 'lib/app_config.dart'
        options:
          app_config: 'lib/app_config.json'
          # This may also be specified with the 'source' property
          # app_config:
          #   source: 'lib/app_config.json'
          # And, additionally as a single element of a source list
          # app_config:
          #   source:
          #     - 'lib/app_config.json'

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  embedded_config: ^0.1.3

2. Install it

You can install packages from the command line:

with pub:


$ pub get

Alternatively, your editor might support pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:embedded_config/embedded_config.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
38
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
69
Learn more about scoring.

We analyzed this package on Apr 4, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6

Health suggestions

Format lib/embedded_config.dart.

Run dartfmt to format lib/embedded_config.dart.

Format lib/src/build_exception.dart.

Run dartfmt to format lib/src/build_exception.dart.

Format lib/src/config_generator.dart.

Run dartfmt to format lib/src/config_generator.dart.

Format lib/src/key_config.dart.

Run dartfmt to format lib/src/key_config.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0 <3.0.0
analyzer >=0.32.4 <0.40.0 0.39.5
build ^1.0.0 1.2.2
code_builder ^3.2.0 3.2.1
embedded_config_annotations >=0.1.0 <0.2.0 0.1.1
source_gen ^0.9.0 0.9.5
Transitive dependencies
_fe_analyzer_shared 2.0.0
args 1.6.0
async 2.4.1
built_collection 4.3.2
built_value 7.0.9
charcode 1.1.3
collection 1.14.12
convert 2.1.1
crypto 2.1.4
csslib 0.16.1
dart_style 1.3.3
fixnum 0.10.11
glob 1.2.0
html 0.14.0+3
js 0.6.1+1
logging 0.11.4
matcher 0.12.6
meta 1.1.8
node_interop 1.0.3
node_io 1.0.1+2
package_config 1.9.3
path 1.6.4
pub_semver 1.4.4
quiver 2.1.3
source_span 1.7.0
stack_trace 1.9.3
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.1.6
watcher 0.9.7+14
yaml 2.2.0
Dev dependencies
pedantic ^1.9.0 1.9.0