davianspace_options 1.0.3 copy "davianspace_options: ^1.0.3" to clipboard
davianspace_options: ^1.0.3 copied to clipboard

Strongly typed configuration binding for Flutter apps, Dart CLI tools, backend services, and web applications inspired by Microsoft.Extensions.Options.

example/example.dart

// Examples use print for observable output; this is intentional in example code.
// ignore_for_file: avoid_print
import 'package:davianspace_options/davianspace_options.dart';

// ---------------------------------------------------------------------------
// Domain models
// ---------------------------------------------------------------------------

/// Represents database connection settings for a single connection target.
class DatabaseOptions {
  String host = 'localhost';
  int port = 5432;
  String database = 'app';
  Duration connectTimeout = const Duration(seconds: 30);
  int maxPoolSize = 10;
  bool enableSsl = false;

  String get connectionString =>
      '${enableSsl ? 'postgresql+ssl' : 'postgresql'}://$host:$port/$database'
      '?connect_timeout=${connectTimeout.inSeconds}'
      '&pool_size=$maxPoolSize';
}

/// Represents feature-flag settings that may change at runtime.
class FeatureFlags {
  bool darkMode = false;
  bool betaSearchEnabled = false;
  String rolloutStage = 'stable'; // stable | beta | canary
}

/// Represents API gateway configuration used by outbound HTTP clients.
class ApiGatewayOptions {
  String baseUrl = '';
  Duration requestTimeout = const Duration(seconds: 10);
  int maxRetries = 3;
  String apiKey = '';
}

// ---------------------------------------------------------------------------
// Example 1 — Singleton options (Options<T>)
//
// A single, permanently-cached configuration instance shared for the
// lifetime of the application. Suitable for immutable startup config.
// ---------------------------------------------------------------------------

void exampleSingletonOptions() {
  print('── Example 1: Singleton options ─────────────────────────────────');

  final factory = OptionsFactoryImpl<DatabaseOptions>(
    instanceFactory: DatabaseOptions.new,
    configureOptions: [
      ConfigureNamedOptions(
        name: null, // applies to every named instance
        configure: (opts) {
          opts.host = 'db.prod.internal';
          opts.port = 5432;
          opts.database = 'orders';
          opts.enableSsl = true;
          opts.maxPoolSize = 25;
        },
      ),
    ],
    postConfigureOptions: [
      // Post-configure runs after all configure actions and is a good place
      // for cross-cutting defaults or environment-specific overrides.
      PostConfigureNamedOptions(
        name: null,
        postConfigure: (opts) {
          if (opts.maxPoolSize > 50) opts.maxPoolSize = 50; // enforce cap
        },
      ),
    ],
    validators: [
      DelegateValidateOptions(
        name: null,
        validate: (name, opts) {
          final errors = <String>[];
          if (opts.host.isEmpty) errors.add('host must not be empty');
          if (opts.database.isEmpty) errors.add('database must not be empty');
          if (opts.port < 1 || opts.port > 65535) {
            errors.add('port must be in range 1–65535');
          }
          if (opts.maxPoolSize < 1) {
            errors.add('maxPoolSize must be at least 1');
          }
          return errors.isEmpty
              ? ValidateOptionsResult.success()
              : ValidateOptionsResult.failMany(errors);
        },
      ),
    ],
  );

  // value is constructed once on first access and cached permanently.
  final options = OptionsManager<DatabaseOptions>(factory: factory);
  final db = options.value;
  print('  Connection string : ${db.connectionString}');
  print('  Pool size         : ${db.maxPoolSize}');
  print('  SSL enabled       : ${db.enableSsl}');
}

// ---------------------------------------------------------------------------
// Example 2 — Named options (OptionsSnapshot<T>)
//
// Separate configuration branches per logical name. Common for multi-region
// databases, per-tenant configs, or primary/replica pools.
// ---------------------------------------------------------------------------

void exampleNamedOptions() {
  print('\n── Example 2: Named options (primary / replica pattern) ──────────');

  final factory = OptionsFactoryImpl<DatabaseOptions>(
    instanceFactory: DatabaseOptions.new,
    configureOptions: [
      // Shared defaults applied first (name: null → all instances).
      ConfigureNamedOptions(
        name: null,
        configure: (opts) {
          opts.database = 'orders';
          opts.enableSsl = true;
        },
      ),
      // Primary: high-pool read/write target.
      ConfigureNamedOptions(
        name: 'primary',
        configure: (opts) {
          opts.host = 'primary.db.prod.internal';
          opts.port = 5432;
          opts.maxPoolSize = 20;
        },
      ),
      // Replica: read-only target with a deeper pool for read-heavy workloads.
      ConfigureNamedOptions(
        name: 'replica',
        configure: (opts) {
          opts.host = 'replica.db.prod.internal';
          opts.port = 5433;
          opts.maxPoolSize = 40;
        },
      ),
    ],
  );

  // OptionsManager acts as a scoped snapshot when a new instance is created
  // per scope (e.g. per HTTP request).  Within one scope the same named
  // instance is returned on repeated calls to get().
  final snapshot = OptionsManager<DatabaseOptions>(factory: factory);
  final primary = snapshot.get('primary');
  final replica = snapshot.get('replica');

  print('  Primary  → ${primary.connectionString}');
  print('  Replica  → ${replica.connectionString}');
}

// ---------------------------------------------------------------------------
// Example 3 — Validation pipeline
//
// Validators run after every configure/postConfigure step. All failures are
// aggregated and raised together, giving operators the full picture at once.
// ---------------------------------------------------------------------------

void exampleValidationPipeline() {
  print('\n── Example 3: Validation pipeline ───────────────────────────────');

  final factory = OptionsFactoryImpl<ApiGatewayOptions>(
    instanceFactory: ApiGatewayOptions.new,
    configureOptions: [
      ConfigureNamedOptions(
        name: null,
        configure: (opts) {
          // Simulate loading from environment variables.
          opts.baseUrl = const String.fromEnvironment('API_BASE_URL');
          opts.apiKey = const String.fromEnvironment('API_KEY');
          opts.maxRetries = 3;
          opts.requestTimeout = const Duration(seconds: 15);
        },
      ),
    ],
    validators: [
      DelegateValidateOptions(
        name: null,
        validate: (name, opts) {
          final errors = <String>[];
          if (opts.baseUrl.isEmpty) {
            errors.add('baseUrl is required (set API_BASE_URL env var)');
          }
          if (opts.apiKey.isEmpty) {
            errors.add('apiKey is required (set API_KEY env var)');
          }
          if (opts.maxRetries < 0 || opts.maxRetries > 10) {
            errors.add('maxRetries must be between 0 and 10');
          }
          return errors.isEmpty
              ? ValidateOptionsResult.success()
              : ValidateOptionsResult.failMany(errors);
        },
      ),
    ],
  );

  try {
    factory.create(Options.defaultName);
    print('  ApiGatewayOptions validated successfully.');
  } on OptionsValidationException catch (e) {
    // In a real app, surface these at startup so misconfiguration is
    // caught before the application accepts traffic.
    print('  Configuration is invalid — refusing to start:');
    for (final failure in e.failures) {
      print('    • $failure');
    }
  }
}

// ---------------------------------------------------------------------------
// Example 4 — Fluent builder with multiple configure layers
//
// OptionsBuilder composes configure → postConfigure → validate in one place,
// producing the lists passed to OptionsFactoryImpl.
// ---------------------------------------------------------------------------

void exampleFluentBuilder() {
  print('\n── Example 4: Fluent builder with layered configuration ──────────');

  final builder = OptionsBuilder<FeatureFlags>(
    factory: FeatureFlags.new,
  )
      // Layer 1 — apply defaults from remote config (simulated).
      .configure((opts) {
    opts.rolloutStage = 'stable';
    opts.darkMode = true;
  })
      // Layer 2 — override for specific named instance 'beta-cohort'.
      .configureNamed('beta-cohort', (opts) {
    opts.rolloutStage = 'beta';
    opts.betaSearchEnabled = true;
  })
      // PostConfigure — enforce invariants after all configure layers.
      .postConfigure((opts) {
    // Beta search must only be on in non-stable stages.
    if (opts.rolloutStage == 'stable') {
      opts.betaSearchEnabled = false;
    }
  }).validate((name, opts) {
    const allowed = {'stable', 'beta', 'canary'};
    return allowed.contains(opts.rolloutStage)
        ? ValidateOptionsResult.success()
        : ValidateOptionsResult.fail(
            '$name: rolloutStage "${opts.rolloutStage}" is not a valid stage.',
          );
  });

  final factory = OptionsFactoryImpl<FeatureFlags>(
    instanceFactory: builder.factory,
    configureOptions: builder.configureActions,
    postConfigureOptions: builder.postConfigureActions,
    validators: builder.validators,
  );

  final defaultInstance = factory.create(Options.defaultName);
  final betaCohort = factory.create('beta-cohort');

  print('  [default]     stage=${defaultInstance.rolloutStage}'
      '  darkMode=${defaultInstance.darkMode}'
      '  betaSearch=${defaultInstance.betaSearchEnabled}');
  print('  [beta-cohort] stage=${betaCohort.rolloutStage}'
      '  darkMode=${betaCohort.darkMode}'
      '  betaSearch=${betaCohort.betaSearchEnabled}');
}

// ---------------------------------------------------------------------------
// Example 5 — Live change notifications (OptionsMonitor<T>)
//
// OptionsMonitor keeps the current value live and fires listeners when the
// underlying configuration source signals a change.  Combine with an
// OptionsChangeNotifier to drive reloads from any source (file watcher,
// remote config poll, internal event bus, etc.).
// ---------------------------------------------------------------------------

void exampleOptionsMonitor() {
  print('\n── Example 5: Live options monitor ──────────────────────────────');

  // The notifier is the bridge between an external change source and the
  // monitor.  In production this would be triggered by a file watcher,
  // a remote configuration service, or an admin API endpoint.
  final notifier = OptionsChangeNotifier();

  // Simulates a mutable config source (e.g. a remote key-value store).
  var remoteRolloutStage = 'stable';

  final factory = OptionsFactoryImpl<FeatureFlags>(
    instanceFactory: FeatureFlags.new,
    configureOptions: [
      ConfigureNamedOptions(
        name: null,
        configure: (opts) => opts.rolloutStage = remoteRolloutStage,
      ),
    ],
  );

  final monitor = OptionsMonitorImpl<FeatureFlags>(
    factory: factory,
    notifier: notifier,
  );

  // Register a listener — dispose the returned registration when the
  // subscriber is torn down (e.g. widget dispose, service shutdown).
  final registration = monitor.onChange((flags, name) {
    print('  [onChange] "$name" reloaded → stage=${flags.rolloutStage}');
  });

  print('  Initial stage : ${monitor.currentValue.rolloutStage}');

  // Simulate a remote config push: update the source and signal the notifier.
  remoteRolloutStage = 'canary';
  notifier.notifyChange(Options.defaultName);

  print('  After reload  : ${monitor.currentValue.rolloutStage}');

  // Always dispose listeners and the monitor when done to prevent leaks.
  registration.dispose();
  monitor.dispose();
}

// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------

void main() {
  exampleSingletonOptions();
  exampleNamedOptions();
  exampleValidationPipeline();
  exampleFluentBuilder();
  exampleOptionsMonitor();
}
0
likes
160
points
142
downloads

Documentation

API reference

Publisher

verified publisherdavian.space

Weekly Downloads

Strongly typed configuration binding for Flutter apps, Dart CLI tools, backend services, and web applications inspired by Microsoft.Extensions.Options.

Repository (GitHub)
View/report issues
Contributing

Topics

#configuration #options #flutter #architecture #app-settings

License

MIT (license)

More

Packages that depend on davianspace_options