diff --git a/packages/deriv_env/README.md b/packages/deriv_env/README.md index c03ca600a..43862781b 100644 --- a/packages/deriv_env/README.md +++ b/packages/deriv_env/README.md @@ -31,12 +31,12 @@ You can check if the environment variable is initialized using the `isInitialize bool isInitialized = Env().isInitialized; ``` -### Loading environment variables +### Initializing the Env -Before using any environment variables, you need to load them from a file. The `load` method loads the variables from a file with the specified filename (default is `.env`). +Before using any environment variables, you need to intialize the `Env` class and pass it an instnace of the EnvLoader and the path to the file. The `initialize` method loads the variables from a file with the specified filename (default is `.env`). ```dart -await Env().load(); +await Env().initialize(EnvLoader(filePath: '.env')); ``` The load method expects the file to contain key-value pairs separated by an equals sign (`=`) and each pair separated by a newline character (`\n`). Blank lines and comments (lines starting with a `#`) are ignored. diff --git a/packages/deriv_env/analysis_options.yaml b/packages/deriv_env/analysis_options.yaml index 91390bcb1..bf3a92f06 100644 --- a/packages/deriv_env/analysis_options.yaml +++ b/packages/deriv_env/analysis_options.yaml @@ -58,12 +58,11 @@ linter: - flutter_style_todos - hash_and_equals - implementation_imports - - iterable_contains_unrelated_type + - collection_methods_unrelated_type - join_return_with_assignment - library_names - library_prefixes # - lines_longer_than_80_chars - - list_remove_unrelated_type - no_adjacent_strings_in_list - no_duplicate_case_values - no_leading_underscores_for_local_identifiers diff --git a/packages/deriv_env/lib/base_env.dart b/packages/deriv_env/lib/base_env_loader.dart similarity index 55% rename from packages/deriv_env/lib/base_env.dart rename to packages/deriv_env/lib/base_env_loader.dart index 24700d332..59afabd53 100644 --- a/packages/deriv_env/lib/base_env.dart +++ b/packages/deriv_env/lib/base_env_loader.dart @@ -1,16 +1,19 @@ /// Base class for retrieve environment variables providers. -abstract class BaseEnv { +abstract class BaseEnvLoader { /// Returns `true` if [Env] is initialized, otherwise `false`. bool get isInitialized; /// Returns all environment variables as a [Map]. Map get entries; - /// Loads environment variables from a `.env` file. - /// - /// If [filename] is not provided, it will default to `.env`. - Future load([String filename = '.env']); + /// Loads environment variables. + Future loadEnvironment(); /// Retrieves an environment variable value by key. - T get(String key, {T? defaultValue}); + T get( + String key, { + T? defaultValue, + T Function(String value)? parser, + String decryptionKey = '', + }); } diff --git a/packages/deriv_env/lib/deriv_env.dart b/packages/deriv_env/lib/deriv_env.dart new file mode 100644 index 000000000..27ef791bf --- /dev/null +++ b/packages/deriv_env/lib/deriv_env.dart @@ -0,0 +1,3 @@ +export 'env.dart'; +export 'env_loader.dart'; +export 'base_env_loader.dart'; diff --git a/packages/deriv_env/lib/env.dart b/packages/deriv_env/lib/env.dart index 2d6af2877..3383f64a4 100644 --- a/packages/deriv_env/lib/env.dart +++ b/packages/deriv_env/lib/env.dart @@ -1,114 +1,56 @@ -import 'package:flutter/services.dart'; - -import 'base_env.dart'; -import 'cipher.dart'; - -/// [Env] class is a singleton class that provides access to environment variables. -class Env extends BaseEnv { - /// Returns the singleton instance of [Env]. - factory Env() => _instance; - - Env._(); +import 'base_env_loader.dart'; +import 'env_loader.dart'; + +/// This class is used to load environment variables from a .env file +class Env { + /// The singleton instance of [EnvLoader]. + factory Env() { + _instance ??= Env._internal(); + return _instance!; + } - static final Env _instance = Env._(); + Env._internal(); - bool _isInitialized = false; + static Env? _instance; - final Map _entries = {}; + /// The environment variables provider. + BaseEnvLoader? _env; - @override - bool get isInitialized => _isInitialized; + /// The instance of [BaseEnv]. + BaseEnvLoader? get env => _env; - @override - Map get entries { - _checkInitialization(); + /// Returns `true` if [Env] is initialized, otherwise `false`. + bool get isInitialized => _env?.isInitialized ?? false; - return _entries; + /// Initializes [EnvLoader] with an instance of [BaseEnv]. + /// Loads environment variables from a `.env` file. + /// + /// If [filename] is not provided, it will default to `.env`. + Future initialize(BaseEnvLoader env) async { + _env = env; + return _env!.loadEnvironment(); } - @override - Future load([String filename = '.env']) async { - _entries.clear(); - - final List fileEntries = await _getEntriesFromFile(filename); - - for (final String entry in fileEntries) { - final List items = entry.split('='); - - if (items.length > 1) { - _entries[items.first.trim()] = items.sublist(1).join('=').trim(); - } - } - - _isInitialized = true; + /// Loads environment variables from a `.env` file. + @Deprecated('Use initialize() method instead.') + Future load([String filePath = '.env']) async { + _env ??= EnvLoader(filePath: filePath); + return _env!.loadEnvironment(); } - @override + /// Retrieves an environment variable value by key. T get( String key, { T? defaultValue, T Function(String value)? parser, String decryptionKey = '', - }) { - _checkInitialization(); - - if (!_entries.containsKey(key)) { - if (defaultValue == null) { - throw Exception('$runtimeType does not contain a value for key: $key.'); - } - - return defaultValue; - } - - final String value = decryptionKey.isEmpty - ? _entries[key] - : Cipher().decrypt(message: _entries[key], key: decryptionKey); - - if (parser != null) { - return parser(value); - } - - switch (T) { - case int: - return int.tryParse(value) as T; - case double: - return double.tryParse(value) as T; - case bool: - return (value.toLowerCase() == 'true') as T; - - default: - return value as T; - } - } - - Future> _getEntriesFromFile(String filename) async { - final String envFileContent = await rootBundle.loadString(filename); - - if (envFileContent.isEmpty) { - throw Exception('$runtimeType: $filename is empty.'); - } - - final List entries = []; - final List content = envFileContent.split('\n'); - - for (final String line in content) { - if (line.isEmpty || line.startsWith('#')) { - continue; - } - - entries.add(line); - } - - return entries; - } - - void _checkInitialization() { - if (_isInitialized) { - return; - } - - throw Exception( - '$runtimeType is not initialized, call load() method first.', - ); - } + }) => + isInitialized + ? _env!.get( + key, + defaultValue: defaultValue, + parser: parser, + decryptionKey: decryptionKey, + ) + : throw Exception('EnvLoader is not initialized.'); } diff --git a/packages/deriv_env/lib/env_loader.dart b/packages/deriv_env/lib/env_loader.dart new file mode 100644 index 000000000..c3a8d9412 --- /dev/null +++ b/packages/deriv_env/lib/env_loader.dart @@ -0,0 +1,119 @@ +import 'package:flutter/services.dart'; + +import 'base_env_loader.dart'; +import 'cipher.dart'; + +/// [Env] class is a singleton class that provides access to environment variables. +class EnvLoader extends BaseEnvLoader { + /// Creates a new instance of [EnvLoader]. + EnvLoader({required this.filePath}); + + /// The path to the file. + final String filePath; + + bool _isInitialized = false; + + final Map _entries = {}; + + @override + bool get isInitialized => _isInitialized; + + @override + Map get entries { + _checkInitialization(); + + return _entries; + } + + @override + Future loadEnvironment() async { + _entries.clear(); + + final List fileEntries = await _getEntriesFromFile(filePath); + + for (final String entry in fileEntries) { + final List items = entry.split('='); + + if (items.length > 1) { + _entries[items.first.trim()] = items.sublist(1).join('=').trim(); + } + } + + _isInitialized = true; + } + + @override + T get( + String key, { + T? defaultValue, + T Function(String value)? parser, + String decryptionKey = '', + }) { + _checkInitialization(); + + if (!_entries.containsKey(key)) { + if (defaultValue == null) { + throw Exception('$runtimeType does not contain a value for key: $key.'); + } + + return defaultValue; + } + + final String value = decryptionKey.isEmpty + ? _entries[key] + : Cipher().decrypt(message: _entries[key], key: decryptionKey); + + if (parser != null) { + return parser(value); + } + + switch (T) { + case int: + return int.tryParse(value) as T; + case double: + return double.tryParse(value) as T; + case bool: + if (value.toLowerCase() == 'true') { + return true as T; + } else if (value.toLowerCase() == 'false') { + return false as T; + } else { + throw FormatException('Invalid boolean value: $value'); + } + + default: + return value as T; + } + } + + Future> _getEntriesFromFile(String filename) async { + final String envFileContent = await rootBundle.loadString(filename); + + if (envFileContent.isEmpty) { + throw Exception('$runtimeType: $filename is empty.'); + } + + final List entries = []; + final List content = envFileContent.split('\n'); + + for (final String line in content) { + if (line.isEmpty || line.startsWith('#')) { + continue; + } + + entries.add(line); + } + + return entries; + } + + void _checkInitialization() { + if (_isInitialized) { + return; + } + + throw Exception( + '$runtimeType is not initialized, call load() method first.', + ); + } +} diff --git a/packages/deriv_env/test/env_test.dart b/packages/deriv_env/test/env_test.dart index 1221a7108..59b338fca 100644 --- a/packages/deriv_env/test/env_test.dart +++ b/packages/deriv_env/test/env_test.dart @@ -1,3 +1,4 @@ +import 'package:deriv_env/env_loader.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:deriv_env/env.dart'; @@ -5,6 +6,14 @@ import 'package:deriv_env/env.dart'; void main() { setUpAll(() => TestWidgetsFlutterBinding.ensureInitialized()); + const String stringKey = 'STRING_VAR'; + const String intKey = 'INT_VAR'; + const String doubleKey = 'DOUBLE_VAR'; + const String boolKey = 'BOOL_VAR'; + const String varWithEqualsKey = 'VAR_WITH_EQUALS'; + const String varWithHashKey = 'VAR_WITH_HASH'; + const String encryptedKey = 'ENCRYPTED_VAR'; + group('env class test =>', () { test('get() method should throw exception if env is not initialized.', () async { @@ -14,27 +23,27 @@ void main() { test('load() method should populate env map.', () async { expect(Env().isInitialized, isFalse); - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); expect(Env().isInitialized, isTrue); - expect(Env().entries.length, 7); + expect(Env().env!.entries.length, 7); - expect(Env().entries['STRING_VAR'], 'hello world'); - expect(Env().entries['INT_VAR'], '123'); - expect(Env().entries['DOUBLE_VAR'], '3.14'); - expect(Env().entries['BOOL_VAR'], 'true'); - expect(Env().entries['VAR_WITH_EQUALS'], 'hello=world'); - expect(Env().entries['VAR_WITH_HASH'], 'hello#world'); + expect(Env().env!.entries[stringKey], 'hello world'); + expect(Env().env!.entries[intKey], '123'); + expect(Env().env!.entries[doubleKey], '3.14'); + expect(Env().env!.entries[boolKey], 'true'); + expect(Env().env!.entries[varWithEqualsKey], 'hello=world'); + expect(Env().env!.entries[varWithHashKey], 'hello#world'); expect( - Env().entries['ENCRYPTED_VAR'], + Env().env!.entries[encryptedKey], 'dVyH3QjdHYcjcS2TQ1XenmDVvf5ViN8ZpSVEcjfFhsk=', ); }); test('get() method should return default value if key is not found.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); expect( Env().get('INVALID_KEY', defaultValue: 'default'), @@ -43,31 +52,31 @@ void main() { }); test('get() method should parse value as int.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); - expect(Env().get('INT_VAR'), 123); + expect(Env().get(intKey), 123); }); test('get() method should parse value as double.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); - expect(Env().get('DOUBLE_VAR'), 3.14); + expect(Env().get(doubleKey), 3.14); }); test('get() method should parse value as bool.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); - expect(Env().get('BOOL_VAR'), isTrue); + expect(Env().get(boolKey), isTrue); }); test( 'get() method should parse value with a parser factory if it is provided.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); expect( Env().get( - 'STRING_VAR', + stringKey, parser: (String value) => value.toUpperCase(), ), 'HELLO WORLD', @@ -75,7 +84,7 @@ void main() { expect( Env().get( - 'INT_VAR', + intKey, parser: (String value) => int.parse(value) * 2, ), 246, @@ -83,7 +92,7 @@ void main() { expect( Env().get( - 'DOUBLE_VAR', + doubleKey, parser: (String value) => double.parse(value) * 2, ), 6.28, @@ -91,7 +100,7 @@ void main() { expect( Env().get( - 'DOUBLE_VAR', + doubleKey, parser: (String value) => double.parse(value) > 3.14, ), false, @@ -100,18 +109,18 @@ void main() { test('check handling variables with special characters like `#` and `=`.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); - expect(Env().entries['VAR_WITH_EQUALS'], 'hello=world'); - expect(Env().entries['VAR_WITH_HASH'], 'hello#world'); + expect(Env().env!.entries[varWithEqualsKey], 'hello=world'); + expect(Env().env!.entries[varWithHashKey], 'hello#world'); }); test('handle encrypted variable.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); expect( Env().get( - 'ENCRYPTED_VAR', + encryptedKey, decryptionKey: 'TbKjMndW1L8vczgGQfPo2IyUxh6XAEay', ), 'ecnrypted message', @@ -119,7 +128,9 @@ void main() { }); test('throws an exception if file is empty.', () async { - expect(() => Env().load('test/.env.empty.test'), throwsException); + expect( + () => Env().initialize(EnvLoader(filePath: 'test/.env.empty.test')), + throwsException); }); test('throws an exception if env is not initialized.', () async { @@ -127,7 +138,7 @@ void main() { }); test('throws an exception if key is not found.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); expect(() => Env().get('INVALID_KEY'), throwsException); }); @@ -135,7 +146,7 @@ void main() { test( 'does not throw an exception if key is not found and a default value is provided.', () async { - await Env().load('test/.env.test'); + await Env().initialize(EnvLoader(filePath: 'test/.env.test')); expect(() => Env().get('INVALID_KEY', defaultValue: 42), returnsNormally); });