strict_config 2.1.1 copy "strict_config: ^2.1.1" to clipboard
strict_config: ^2.1.1 copied to clipboard

Configuration parser with error checking, validation and logging config support.

example/example.dart

#!/usr/bin/env dart

import 'dart:io';

import 'package:logging/logging.dart';
import 'package:strict_config/strict_config.dart';

final _logApp = Logger('app');
final _logConfig = Logger('app.config');
final _logRsrc = Logger('app.resource');
final _logHeaders = Logger('app.resource.headers');
final _logCert = Logger('app.resource.cert');

//----------------------------------------------------------------

class ExampleConfig2 {
  /// Extract from a config map.

  ExampleConfig2(ConfigMap m) {
    name = m.string('name');
    desc = m.stringOptional('description', keepWhitespace: true);
    server = ServerConfig(m.map('server'));

    // Using `defaultValue`
    //
    // The "min-days" and "debug" keys do not have to be provided in the
    // config, but with _defaultValue_ they will always have a value
    // in this program. Note use of range checking for "max-retries".

    minDaysToExpiry =
        m.integer('min-days-to-expiry', min: 0, max: 365, defaultValue: 30);
    ignoreBadCert = m.boolean('ignore-bad-certificate', defaultValue: false);

    // List of strings
    //
    // The "paths" key is optional in the config, but if it is present the
    // value cannot be an empty list.

    pathSegments = m.stringsOptional('path-segments', allowEmptyList: false);

    // Config map.
    //
    // The account config map is optional, so it may return null.
    // If it is present, extract values from it into an `AccountConfig` object.

    final accountMap = m.mapOptional('account');
    if (accountMap != null) {
      account = AccountConfig(accountMap);
    }

    // List of config maps.
    //
    // If these comments were not here, convenience methods like this can make
    // the extraction code very compact (like the first few lines of this
    // method).

    headers = HeaderConfig.mapsOptional('headers', m);

    // Extract logger levels and use them to setup logging

    _setupLogging(m);

    // Extra error checking: throws an exception if the config map contains
    // keys that have not been processed. This can be helpful for debugging
    // mis-typed keys and other errors. But in some situations, you might want
    // to skip this check to allow the config to contain other keys that the
    // program ignores.

    m.unusedKeysCheck();
  }

  /// Extract from a file.

  factory ExampleConfig2.fromFile(String configFilename) {
    // Read text from the file

    final configText = File(configFilename).readAsStringSync();

    // Parse and extract values from it

    final config = ConfigMap(configText);
    return ExampleConfig2(config);
  }

  //----------------

  late String name;
  String? desc; // optional, so it maybe null
  late ServerConfig server;

  late int minDaysToExpiry;
  late bool ignoreBadCert;
  List<String>? pathSegments; // optional
  AccountConfig? account; // optional
  late List<HeaderConfig> headers; // optional, empty list if not present

  //----------------
  // Extract logger levels from the config map and setup logging.

  void _setupLogging(ConfigMap m) {
    final levels = LoggerConfig.optional(m); // default key = 'logger'
    if (levels != null) {
      // The "logger" key exists in the config map: setup logging

      hierarchicalLoggingEnabled = true;
      Logger.root.onRecord.listen((r) {
        final t = r.time.toUtc();
        stdout.write('$t: ${r.loggerName}: ${r.level.name}: ${r.message}\n');
      });

      Logger.root.level = Level.OFF;
      levels.applyLevels(); // setup levels specified in the config
    }
  }
}

//----------------------------------------------------------------

class ServerConfig {
  factory ServerConfig(ConfigMap m) {
    final host = m.string('host');
    final tls = m.boolean('tls', defaultValue: true);
    final port =
        m.integer('port', min: 1, max: 65535, defaultValue: tls ? 443 : 80);
    m.unusedKeysCheck();

    return ServerConfig._init(host, tls, port);
  }

  ServerConfig._init(this.host, this.tls, this.port);

  String host;
  bool tls;
  int port;
}

//----------------------------------------------------------------
/// Account config.

class AccountConfig {
  /// Constructor
  ///
  /// Extracts values from a config map.
  ///
  /// Will be invoked with a config map extracted another (the top-level) config
  /// map.

  AccountConfig(ConfigMap m) {
    // Mandatory username: never null
    //
    // The "username" key must appear in the config.
    //
    // Whitespaces are tidied up and empty strings (zero length) are rejected
    // by throwing an exception.

    username = m.string('username');

    // Optional password: may be null
    //
    // The "password" key may or may not be in the config. If it is not present,
    // the value is null.
    //
    // Whitespaces are significant, so keep them in the value. Allow blank
    // strings (made up entirely of whitespace) and empty strings (zero length).

    password = m.stringOptional('password',
        keepWhitespace: true, allowBlank: true, allowEmpty: true);

    // Authentication scheme: optional in config, but program always has a value
    //
    // If the key is not in the config, the provided `defaultValue` is returned.
    // Since there is always a value returned, this is uses `string` method
    // instead of the `stringOptional` method. The key is optional in the
    // config, but the extracted value is never null.
    //
    // If the key is in the config, the value must be one of the `permitted`
    // values. Otherwise an exception is thrown.

    scheme = m.string('auth-scheme',
        permitted: ['Basic', 'Digest'], defaultValue: 'Basic');

    // Check for any unexpected keys in the config.
    //
    // The config map keeps track of the keys passed to the extraction methods,
    // so it knows "username", "password" and "two-factor" are expected keys.
    // Any other keys in the config map will cause an exception to be thrown.
    //
    // This can be useful for users. For example, if they have mistyped the name
    // of a key, they will know it is an error instead of wondering why the
    // value in the config is not being used.

    m.unusedKeysCheck();
  }

  late String username;
  String? password; // optional: could be null
  String? scheme; // optional, but will always have a value
}

//----------------------------------------------------------------
/// Header config.

class HeaderConfig {
  HeaderConfig(ConfigMap m) {
    name = m.string('name');
    value = m.stringOptional('value', allowEmpty: true);

    m.unusedKeysCheck();
  }

  late String name;
  String? value; // optional

  @override
  String toString() => '$name: $value';

  /// Convenience method for extracting an optional list of HeaderConfigs.
  ///
  /// Always returns a list. If the configs are not present, the empty list
  /// is returned.

  static List<HeaderConfig> mapsOptional(String name, ConfigMap parentMap) {
    // Since allowEmptyList defaults to true, this could return null or
    // an empty list as two distinct values. But this method will treat them
    // both to mean the same thing: no headers.
    //
    // To make the code that uses the headers simpler, this method always
    // returns a list. An alternative design is to return null.

    final childList = parentMap.mapsOptional(name);

    if (childList != null) {
      // List of values or a list that is empty
      return childList.map((e) => HeaderConfig(e)).toList();
    } else {
      return []; // treat a missing key the same as an empty list
    }
  }
}

//================================================================

// The future is null on success, or a string on error.

Future<String?> checkResource(ExampleConfig2 config) async {
  final uri = Uri(
      scheme: config.server.tls ? 'https' : 'http',
      host: config.server.host,
      port: config.server.port,
      pathSegments: config.pathSegments);
  _logRsrc.info('URI: $uri');

  // Set up HttpClient

  final client = HttpClient();

  if (config.ignoreBadCert) {
    client.badCertificateCallback = (cert, host, port) => true;
  }

  final account = config.account;
  if (account != null) {
    client.authenticate = (url, scheme, realm) async {
      if (scheme == account.scheme) {
        late HttpClientCredentials cred;

        if (scheme == 'Basic') {
          cred = HttpClientBasicCredentials(
              account.username, account.password ?? '');
        } else if (scheme == 'Digest') {
          cred = HttpClientDigestCredentials(
              account.username, account.password ?? '');
        }

        client.addCredentials(url, realm ?? '', cred);
        return true;
      } else {
        _logRsrc.severe('authentication scheme not supported: $scheme');
        return false; // scheme not supported
      }
    };
  }

  try {
    // Open request

    final req = await client.getUrl(uri);
    for (final h in config.headers) {
      req.headers.add(h.name, h.value ?? '');
    }

    // Get response

    final resp = await req.close();
    try {
      final cert = resp.certificate;
      if (cert != null) {
        // TLS server certificate

        _logCert
          ..fine('Server cert subject: ${cert.subject}')
          ..finest('Server cert issuer: ${cert.issuer}')
          ..finer('Server cert start validity: ${cert.startValidity}')
          ..finer('Server cert end validity: ${cert.endValidity}');

        // Produce alert if will expire soon (or has already expired)

        final daysToExpiry = cert.endValidity.difference(DateTime.now()).inDays;
        if (daysToExpiry <= 0) {
          return 'certificate expired';
        }
        if (daysToExpiry < config.minDaysToExpiry) {
          return 'certificate expires in $daysToExpiry days';
        }
      }

      resp.headers.forEach((name, values) {
        for (final value in values) {
          _logHeaders.fine('$name: $value');
        }
      });

      // HTTP status

      _logRsrc.fine('HTTP status = ${resp.statusCode}');
      if (HttpStatus.ok <= resp.statusCode && resp.statusCode < 300) {
        return null; // success
      } else {
        return 'status=${resp.statusCode}';
      }
    } finally {
      await resp.drain(); // without this the program does not exit
    }
  } on HandshakeException catch (e) {
    _logRsrc.fine('HandshakeException: $e');
    return 'TLS handshake failed';
  } finally {
    client.close(); // without this the program does not exit
  }
}

//================================================================

Future<void> main(List<String> args) async {
  // Simple command line processing

  final exeName = Platform.script.pathSegments.last.replaceAll('.dart', '');

  if (args.contains('-h') || args.contains('--help')) {
    stdout.write('Usage: $exeName [-h|--help] [-v|--verbose] configFile\n');
    exit(0);
  }
  final remainingArgs = List<String>.from(args);
  final verbose =
      remainingArgs.remove('-v') || remainingArgs.remove('--verbose');

  if (remainingArgs.isEmpty) {
    stderr.write('Usage error: missing config file\n');
    exit(2);
  } else if (1 < remainingArgs.length) {
    stderr.write('Usage error: too many arguments ("-h" for help)\n');
    exit(2);
  }
  final configFilename = remainingArgs.first;

  // Load the config

  ExampleConfig2 config;
  try {
    config = ExampleConfig2.fromFile(configFilename);
    logConfig(configFilename, config);
  } on ConfigException catch (e) {
    stderr.write('Config error: $configFilename: $e\n');
    exit(1);
  } on FileSystemException catch (e) {
    stderr.write('Error: ${e.path}: ${e.message}\n');
    exit(1);
  }

  // Use the config

  _logApp.finer('begin');

  final errorMessage = await checkResource(config);

  _logApp
    ..fine('result: $errorMessage')
    ..finer('end');

  if (errorMessage == null) {
    stdout.write('${config.name}: ok\n');
    exitCode = 0;
  } else {
    stdout.write('${config.name}: $errorMessage\n');
    exitCode = 1;
  }

  if (verbose) {
    stdout.write('${config.desc}\n');
  }
}

/// Use a logger to output the config values.
///
/// Try changing the logger level of the "app.config" logger to control which
/// log entries are outputted.

void logConfig(String configFilename, ExampleConfig2 config) {
  _logConfig
    ..config('config file: "$configFilename"')
    ..fine('name="${config.name}"')
    ..finer('description="${config.desc}"')
    ..finest('server host: ${config.server.host}')
    ..finest('server port: ${config.server.port}')
    ..finest('server TLS: ${config.server.tls}')
    ..info('minimum days to expiry: ${config.minDaysToExpiry}');

  if (config.pathSegments != null) {
    _logConfig.finest('path segments: ${config.pathSegments}');
  } else {
    _logConfig.finest('path segments: none');
  }

  final acc = config.account;
  if (acc != null) {
    // Optional account is available
    final p =
        acc.password != null ? 'password is provided' : 'prompt for password';
    _logConfig.finest('account: ${acc.username} [${acc.scheme}] $p');
    if (acc.password != null && acc.password!.length < 10) {
      _logConfig.warning('password is insecure: it is too short');
    }
    if (acc.password != null && acc.password!.isEmpty) {
      _logConfig.severe('password is very insecure: it is the empty string');
    }
  }

  _logConfig.finest('headers: ${config.headers}');
}
0
likes
130
pub points
30%
popularity

Publisher

verified publisherhoylen.com

Configuration parser with error checking, validation and logging config support.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (LICENSE)

Dependencies

logging, yaml

More

Packages that depend on strict_config