platform/packages/configuration/lib/angel3_configuration.dart
2024-10-12 03:35:14 -07:00

147 lines
4.3 KiB
Dart

library angel3_configuration;
import 'dart:async';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:dotenv/dotenv.dart';
import 'package:file/file.dart';
import 'package:belatuk_merge_map/belatuk_merge_map.dart';
import 'package:yaml/yaml.dart';
Future<void> _loadYamlFile(
Map map, File yamlFile, DotEnv env, void Function(String msg) warn) async {
if (await yamlFile.exists()) {
var config = loadYaml(await yamlFile.readAsString());
if (config is! Map) {
warn(
'The configuration at "${yamlFile.absolute.path}" is not a Map. Refusing to load it.');
return;
}
var out = {};
var configMap = Map.of(config);
// Check for _include
if (configMap.containsKey('_include')) {
var include = configMap.remove('_include');
var includeList = include is Iterable ? include.toList() : [include];
for (var inc in includeList) {
if (inc is! String) {
warn('Included item $inc is not a String.');
} else {
var p = yamlFile.fileSystem.path;
var includeFilePath = p.join(yamlFile.parent.path, inc);
var includeFile = yamlFile.fileSystem.file(includeFilePath);
if (!await includeFile.exists()) {
warn(
'Included configuration file "$includeFilePath" does not exist.');
} else {
await _loadYamlFile(out, includeFile, env, warn);
}
}
}
}
for (var key in configMap.keys) {
out[key] = _applyEnv(configMap[key], env, warn);
}
map.addAll(mergeMap(
[
map,
out,
],
acceptNull: true,
));
}
}
Object? _applyEnv(var v, DotEnv env, void Function(String msg) warn) {
if (v is String) {
if (v.startsWith(r'$') && v.length > 1) {
var key = v.substring(1);
if (env.isDefined(key)) {
return env[key];
} else {
warn(
'Your configuration calls for loading the value of "$key" from the system environment, but it is not defined. Defaulting to `null`.');
return null;
}
} else {
return v;
}
} else if (v is Iterable) {
return v.map((x) => _applyEnv(x, env, warn)).toList();
} else if (v is Map) {
return v.keys
.fold<Map>({}, (out, k) => out..[k] = _applyEnv(v[k], env, warn));
} else {
return v;
}
}
/// Loads [configuration], and returns a [Map].
///
/// You can override [onWarning]; otherwise, configuration errors will throw.
Future<Map> loadStandaloneConfiguration(FileSystem fileSystem,
{String directoryPath = './config',
String? overrideEnvironmentName,
String? envPath,
void Function(String message)? onWarning}) async {
var sourceDirectory = fileSystem.directory(directoryPath);
var env = DotEnv(includePlatformEnvironment: true);
var envFile = sourceDirectory.childFile(envPath ?? '.env');
if (await envFile.exists()) {
env.load([envFile.absolute.uri.toFilePath()]);
}
var environmentName = env['ANGEL_ENV'] ?? 'development';
if (overrideEnvironmentName != null) {
environmentName = overrideEnvironmentName;
}
onWarning ??= (String message) => throw StateError(message);
var out = {};
var defaultYaml = sourceDirectory.childFile('default.yaml');
await _loadYamlFile(out, defaultYaml, env, onWarning);
var configFilePath = '$environmentName.yaml';
var configFile = sourceDirectory.childFile(configFilePath);
await _loadYamlFile(out, configFile, env, onWarning);
return out;
}
/// Dynamically loads application configuration from configuration files.
///
/// You can modify which [directoryPath] to search in, or explicitly
/// load from a [overrideEnvironmentName].
///
/// You can also specify a custom [envPath] to load system configuration from.
ProtevusConfigurer configuration(FileSystem fileSystem,
{String directoryPath = './config',
String? overrideEnvironmentName,
String? envPath}) {
return (Protevus app) async {
var config = await loadStandaloneConfiguration(
fileSystem,
directoryPath: directoryPath,
overrideEnvironmentName: overrideEnvironmentName,
envPath: envPath,
onWarning: (msg) => app.logger.warning('WARNING: $msg'),
);
app.configuration.addAll(mergeMap(
[
app.configuration,
config,
],
acceptNull: true,
));
};
}