diff --git a/.env b/.env new file mode 100644 index 0000000..b36b49a --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +app.name = 'CUBETIQ Solution' +export NAME='Sambo Chea' \ No newline at end of file diff --git a/example/example.dart b/example/example.dart new file mode 100644 index 0000000..31ed13f --- /dev/null +++ b/example/example.dart @@ -0,0 +1,3 @@ +void main() { + print('Example'); +} \ No newline at end of file diff --git a/lib/dotenv/dotenv.dart b/lib/dotenv/dotenv.dart new file mode 100644 index 0000000..a84f42c --- /dev/null +++ b/lib/dotenv/dotenv.dart @@ -0,0 +1,54 @@ +/// Loads environment variables from a `.env` file. +/// +/// ## usage +/// +/// Once you call [load], the top-level [env] map is available. +/// You may wish to prefix the import. +/// +/// import 'package:dotenv/dotenv.dart' show load, env; +/// +/// void main() { +/// load(); +/// var x = env['foo']; +/// // ... +/// } +/// +/// Verify required variables are present: +/// +/// const _requiredEnvVars = const ['host', 'port']; +/// bool get hasEnv => isEveryDefined(_requiredEnvVars); +library dotenv; + +import 'dart:io'; + +import 'package:meta/meta.dart'; + +part 'parser.dart'; + +var _env = Map.from(Platform.environment); + +/// A copy of [Platform.environment](dart:io) including variables loaded at runtime from a file. +Map get env => _env; + +/// Overwrite [env] with a new writable copy of [Platform.environment](dart:io). +Map clean() => _env = Map.from(Platform.environment); + +/// True if all supplied variables have nonempty value; false otherwise. +/// Differs from [containsKey](dart:core) by excluding null values. +/// Note [load] should be called first. +bool isEveryDefined(Iterable vars) => + vars.every((k) => _env[k] != null && (_env[k]?.isNotEmpty ?? false)); + +/// Read environment variables from [filename] and add them to [env]. +/// Logs to [stderr] if [filename] does not exist. +void load([String filename = '.env', Parser psr = const Parser()]) { + var file = File.fromUri(Uri.file(filename)); + var lines = _verify(file); + _env.addAll(psr.parse(lines)); +} + +List _verify(File file) { + if (file.existsSync()) return file.readAsLinesSync(); + stderr.writeln('[dotenv] Load failed: file not found: $file'); + return []; +} \ No newline at end of file diff --git a/lib/dotenv/parser.dart b/lib/dotenv/parser.dart new file mode 100644 index 0000000..2b6dec2 --- /dev/null +++ b/lib/dotenv/parser.dart @@ -0,0 +1,92 @@ +part of dotenv; + +/// Creates key-value pairs from strings formatted as environment +/// variable definitions. +class Parser { + static const _singleQuot = "'"; + static const _keyword = 'export'; + + static final _comment = RegExp(r'''#.*(?:[^'"])$'''); + static final _surroundQuotes = RegExp(r'''^(['"])(.*)\1$'''); + static final _bashVar = RegExp(r'(?:\\)?(\$)(?:{)?([a-zA-Z_][\w]*)+(?:})?'); + + /// [Parser] methods are pure functions. + const Parser(); + + /// Creates a [Map](dart:core) suitable for merging into [Platform.environment](dart:io). + /// Duplicate keys are silently discarded. + Map parse(Iterable lines) { + var out = {}; + lines.forEach((line) { + var kv = parseOne(line, env: out); + if (kv.isEmpty) return; + out.putIfAbsent(kv.keys.single, () => kv.values.single); + }); + return out; + } + + /// Parses a single line into a key-value pair. + @visibleForTesting + Map parseOne(String line, + {Map env: const {}}) { + var stripped = strip(line); + if (!_isValid(stripped)) return {}; + + var idx = stripped.indexOf('='); + var lhs = stripped.substring(0, idx); + var k = swallow(lhs); + if (k.isEmpty) return {}; + + var rhs = stripped.substring(idx + 1, stripped.length).trim(); + var quotChar = surroundingQuote(rhs); + var v = unquote(rhs); + + if (quotChar == _singleQuot) { + return {k: v}; + } + + return {k: interpolate(v, env)}; + } + + /// Substitutes $bash_vars in [val] with values from [env]. + @visibleForTesting + String interpolate(String val, Map env) => + val.replaceAllMapped(_bashVar, (m) { + var k = m.group(2)!; + return (!_has(env, k)) ? _tryPlatformEnv(k) ?? '' : env[k] ?? ''; + }); + + /// If [val] is wrapped in single or double quotes, returns the quote character. + /// Otherwise, returns the empty string. + @visibleForTesting + String surroundingQuote(String val) { + if (!_surroundQuotes.hasMatch(val)) return ''; + return _surroundQuotes.firstMatch(val)!.group(1)!; + } + + /// Removes quotes (single or double) surrounding a value. + @visibleForTesting + String unquote(String val) => + val.replaceFirstMapped(_surroundQuotes, (m) => m[2]!).trim(); + + /// Strips comments (trailing or whole-line). + @visibleForTesting + String strip(String line) => line.replaceAll(_comment, '').trim(); + + /// Omits 'export' keyword. + @visibleForTesting + String swallow(String line) => line.replaceAll(_keyword, '').trim(); + + bool _isValid(String s) => s.isNotEmpty && s.contains('='); + + /// [null] is a valid value in a Dart map, but the env var representation is empty string, not the string 'null' + bool _has(Map map, String key) => + map.containsKey(key) && map[key] != null; + + String? _tryPlatformEnv(String key) { + if (!_has(Platform.environment, key)) { + return ''; + } + return Platform.environment[key]; + } +} diff --git a/lib/dotenv_configuration_provider.dart b/lib/dotenv_configuration_provider.dart new file mode 100644 index 0000000..6e65370 --- /dev/null +++ b/lib/dotenv_configuration_provider.dart @@ -0,0 +1,20 @@ +import 'package:configurable/configuration_provider.dart'; +import 'package:configurable/dotenv/dotenv.dart' show env; + +class DotenvConfigurationProvider implements ConfigurationProvider { + @override + bool containsKey(String key) { + return env.containsKey(key); + } + + @override + String? getOrNull(String key, {String? defaultValue}) { + var value = env[key]; + + if (value == null) { + return defaultValue; + } + + return value; + } +} diff --git a/pubspec.lock b/pubspec.lock index 0f5fc71..97ed65e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -135,7 +135,7 @@ packages: source: hosted version: "0.12.10" meta: - dependency: transitive + dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 4ffc547..c54d04c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: test: ^1.16.8 + meta: ^1.3.0 dev_dependencies: pedantic: ^1.9.0 diff --git a/test/config_test.dart b/test/config_test.dart index a0ef06c..0ca2ca0 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -10,4 +10,5 @@ void main() { expect(value, equals(result)); expect(value, equals(SystemConfig.getOrNull(key))); }); + } diff --git a/test/dotenv_test.dart b/test/dotenv_test.dart new file mode 100644 index 0000000..2f359b8 --- /dev/null +++ b/test/dotenv_test.dart @@ -0,0 +1,20 @@ +import 'package:configurable/dotenv/dotenv.dart'; +import 'package:test/test.dart'; + +void main() { + test('just call env', () { + load(); + + var result = env['app.name']; + + expect('CUBETIQ Solution', equals(result)); + }); + + test('get export variable', () { + load(); + + var result = env['NAME']; + + expect('Sambo Chea', equals(result)); + }); +}