diene_config 1.0.0 copy "diene_config: ^1.0.0" to clipboard
diene_config: ^1.0.0 copied to clipboard

Layered, schema-composed configuration for Dart and Flutter apps — base YAML, landscape overlay, dev override, then Dart defines, validated once.

example/diene_config_example.dart

// A single pass through the diene_config surface: the four-layer ladder, the
// service-composed schema, typed slices, the landscape accessor, and the
// failure channel.
//
// Run it with:
//   dart run example/diene_config_example.dart
//
// This file prints, which the repository lints against in library code for good
// reason. An example's whole job is to show a reader what each call returns, so
// the lint is suppressed HERE and nowhere else; `lib/` stays print-free.
// ignore_for_file: avoid_print

import 'package:diene_config/diene_config.dart';
import 'package:diene_problems/diene_problems.dart';
import 'package:diene_result/diene_result.dart';

/// An engine- or service-owned settings type.
///
/// In a real app this lives beside the code that READS it — in `auth_engine`
/// for auth settings, in `api_engine` for API settings — and the app composes
/// the exported blocks. `diene_config` never defines one.
final class ApiSettings {
  const ApiSettings({required this.baseUrl, required this.scopes});

  final Uri baseUrl;
  final List<String> scopes;

  @override
  String toString() => 'ApiSettings($baseUrl, $scopes)';
}

/// The block its owner exports next to [ApiSettings].
final ConfigBlock<ApiSettings> apiBlock = ConfigBlock<ApiSettings>(
  key: 'api',
  decode: (Map<String, Object?> values) {
    final Object? baseUrl = values['baseUrl'];
    if (baseUrl is! String || Uri.tryParse(baseUrl)?.hasScheme != true) {
      throw FormatException('baseUrl must be an absolute URI, got $baseUrl');
    }
    return ApiSettings(
      baseUrl: Uri.parse(baseUrl),
      scopes: (values['scopes'] as List<Object?>? ?? const <Object?>[])
          .map((Object? scope) => '$scope')
          .toList(growable: false),
    );
  },
);

/// A full base document: every overrideable path, with defaults.
const String baseYaml = '''
api:
  baseUrl: https://api.local.test
  scopes: [openid]
''';

/// A sparse landscape overlay: only what this landscape changes.
const String landscapeYaml = '''
api:
  baseUrl: https://api.lapras.atomi.cloud
''';

Future<void> main() async {
  await _theLadder();
  await _failuresAreValues();
  _theLandscapeAccessor();
}

Future<void> _theLadder() async {
  print('--- the four-layer ladder ---');

  final ConfigLoader loader = ConfigLoader(
    // 1. Full base YAML. A real app passes File(path).readAsString or
    //    rootBundle.loadString here; the package touches neither dart:io nor
    //    Flutter itself.
    base: YamlConfigSource.string(baseYaml, name: 'config/base.yaml'),
    // 2. One sparse flavor/landscape overlay.
    overlay: YamlConfigSource.string(landscapeYaml, name: 'config/lapras.yaml'),
    // 3. An optional development hook. Omit it (or pass null) in release.
    developmentOverride: YamlConfigSource.string(
      'api:\n  scopes: [openid, offline_access]\n',
      name: 'config/development.yaml',
    ),
    // 4. Enumerated --dart-define values, applied LAST. Dart has no runtime
    //    environment enumeration, so the app lists the keys it accepts.
    dartDefines: const DartDefineOverrides(
      prefix: 'ACME_',
      values: <String, String>{
        // Lists come ONLY from indexed keys — never JSON, never commas.
        'ACME_API__SCOPES__0': 'openid',
        'ACME_API__SCOPES__1': 'profile',
        // A blank define is UNSET and cannot erase the base value.
        'ACME_API__BASE_URL': '',
      },
    ),
    // 5. The service composes the root schema from engine-owned blocks.
    schema: ConfigSchema(blocks: <ConfigBlockSchema>[apiBlock]),
  );

  final Result<DieneConfig> loaded = await loader.load();
  print('loaded              = ${_show(loaded)}');

  loaded.run((DieneConfig config) {
    final ApiSettings api = config.slice(apiBlock);
    // The landscape overlay won the URL; the blank define did not erase it.
    print('typed slice         = $api');
    // The indexed defines replaced the development layer's list wholesale.
    print('scopes              = ${api.scopes}');
    print('raw slice           = ${_show(config.rawSlice('api'))}');
    print('stable projection   = ${_show(config.stableProjection())}');
  });
}

Future<void> _failuresAreValues() async {
  print('\n--- failures are values, not exceptions ---');

  // An unreadable layer: the read callback threw, so the layer is reported
  // rather than crashing the load.
  final Result<DieneConfig> unreadable = await ConfigLoader(
    base: YamlConfigSource(
      name: 'config/absent.yaml',
      read: () => throw StateError('asset not found'),
    ),
    dartDefines: const DartDefineOverrides(prefix: 'ACME_'),
    schema: ConfigSchema(blocks: <ConfigBlockSchema>[apiBlock]),
  ).load();
  print('missing layer       = ${_show(unreadable)}');

  // An invalid FINAL tree. Validation runs once, at the end — a base that is
  // incomplete on its own is fine if a later layer completes it.
  final Result<DieneConfig> invalid = await ConfigLoader(
    base: YamlConfigSource.string('api:\n  baseUrl: not-a-uri\n'),
    dartDefines: const DartDefineOverrides(prefix: 'ACME_'),
    schema: ConfigSchema(blocks: <ConfigBlockSchema>[apiBlock]),
  ).load();
  print('invalid final tree  = ${_show(invalid)}');

  // ...and the same base, repaired by a later layer, is accepted.
  final Result<DieneConfig> repaired = await ConfigLoader(
    base: YamlConfigSource.string('api:\n  baseUrl: not-a-uri\n'),
    dartDefines: const DartDefineOverrides(
      prefix: 'ACME_',
      values: <String, String>{'ACME_API__BASE_URL': 'https://api.test'},
    ),
    schema: ConfigSchema(blocks: <ConfigBlockSchema>[apiBlock]),
  ).load();
  print('repaired by define  = ${_show(repaired)}');

  // A malformed define key is reported with the core-utils coercion envelope,
  // UNCHANGED, so its precise vocabulary survives.
  final Result<DieneConfig> malformed = await ConfigLoader(
    base: YamlConfigSource.string(baseYaml),
    dartDefines: const DartDefineOverrides(
      prefix: 'ACME_',
      values: <String, String>{'ACME_API____BASE_URL': 'https://api.test'},
    ),
    schema: ConfigSchema(blocks: <ConfigBlockSchema>[apiBlock]),
  ).load();
  print('malformed define    = ${_show(malformed)}');
}

void _theLandscapeAccessor() {
  print('\n--- the landscape accessor ---');

  // A store build supplies --dart-define=DIENE_LANDSCAPE=<track>. The accessor
  // reads that define and NOTHING else: no hostname sniffing, no runtime probe.
  print(
    'injected            = '
    '${_show(landscape(source: const DartDefineLandscapeSource(value: 'lapras')))}',
  );

  // Without the define there is no identity, and it says so as a value.
  print('absent define       = ${_show(landscape())}');
}

/// Renders either channel of a `Result` for display.
String _show(Result<Object?> result) => result.match(
  ok: (Object? value) => 'Ok($value)',
  err: (Problem problem) =>
      'Err(${problem.status} ${problem.title}; data=${problem.data})',
);
0
likes
160
points
32
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Layered, schema-composed configuration for Dart and Flutter apps — base YAML, landscape overlay, dev override, then Dart defines, validated once.

Repository (GitHub)
View/report issues

Topics

#configuration #yaml #schema #layered

License

MIT (license)

Dependencies

diene_core_utils, diene_problems, diene_result, yaml

More

Packages that depend on diene_config