diff --git a/docs/config/appsettings/configuration.md b/docs/config/appsettings/configuration.md index 2f055548..9b2e0756 100644 --- a/docs/config/appsettings/configuration.md +++ b/docs/config/appsettings/configuration.md @@ -36,6 +36,7 @@ Within the Project we can now provide any configuration values we need to either "delimiter": ";", "namespace": "Helpers", "rootNamespace": null, + "prefix": "BuildTools_", "properties": [ // Property Definitions ] @@ -128,3 +129,141 @@ Other times we may have non-sensitive values that we need to configure defaults } } ``` + +#### Handling Duplicate Property Names + +There may be times in which you have more than one project in your solution, or perhaps just more than one generated settings class that require duplicate property names. An example of this could be an API Settings class. + +```json +{ + "$schema": "https://mobilebuildtools.com/schemas/v2/buildtools.schema.json", + "appSettings": { + "AwesomeApp": [ + { + "className": "FooApiSettings", + "properties": [ + { + "name": "BaseUri", + "type": "Uri" + } + ] + }, + { + "className": "BarApiSettings", + "properties": [ + { + "name": "BaseUri", + "type": "Uri" + } + ] + } + ] + } +} +``` + +In this sample we have 2 generated classes with the `BaseUri` property, this creates a few problems for us because we need to distinguish which Uri belongs to which class and ultimately our JSON would be invalid because it has a duplicated key + +```json +{ + "BaseUri": "https://api.foo.com", + "BaseUri": "https://api.bar.com" +} +``` + +To solve this problem we can use the Prefix property on our generated class settings. In this way we can specify a unique variable prefix that will be used to identify which Base Uri property belongs to which generated class + +```json +{ + "$schema": "https://mobilebuildtools.com/schemas/v2/buildtools.schema.json", + "appSettings": { + "AwesomeApp": [ + { + "className": "FooApiSettings", + "prefix": "FooApi_", + "properties": [ + { + "name": "BaseUri", + "type": "Uri" + } + ] + }, + { + "className": "BarApiSettings", + "prefix": "BarApi_", + "properties": [ + { + "name": "BaseUri", + "type": "Uri" + } + ] + } + ] + } +} +``` + +With the Prefix added we can now update our appsettings.json to be: + +```json +{ + "FooApi_BaseUri": "https://api.foo.com", + "BarApi_BaseUri": "https://api.bar.com" +} +``` + +> [!NOTE] +> If your prefix does not end with an underscore one will automatically be inserted. In the above example if we did not explicitly have the underscore, the Mobile.BuildTools would still be expecting the same values in our `appsettings.json` or as an Environment variable. + +#### Setting Variables from the environment + +The Mobile.BuildTools allows us to "Fake" environment variables. There may be times such as the previous sample with our previous example where the values aren't particularly sensitive but simply something that may change based on our Build... + +```json +{ + "$schema": "https://mobilebuildtools.com/schemas/v2/buildtools.schema.json", + "environment": { + "defaults": { + "FooApi_BaseUri": "https://dev.api.foo.com", + "BarApi_BaseUri": "https://dev.api.bar.com" + }, + } +} +``` + +It's also possible that we may want to further customize this without the need to update a CI Build environment for variables that aren't particularly sensitive. In this case we can provide Build Configuration specific settings: + +```json +{ + "$schema": "https://mobilebuildtools.com/schemas/v2/buildtools.schema.json", + "environment": { + "configuration": { + "Debug": { + "FooApi_BaseUri": "https://dev.api.foo.com", + "BarApi_BaseUri": "https://dev.api.bar.com" + }, + "QA": { + "FooApi_BaseUri": "https://qa.api.foo.com", + "BarApi_BaseUri": "https://qa.api.bar.com" + }, + "Release": { + "FooApi_BaseUri": "https://api.foo.com", + "BarApi_BaseUri": "https://api.bar.com" + } + }, + } +} +``` + +#### Fuzzy Matching + +From time to time you may want to make use of Fuzzy Matching. Fuzzy Matching allows you to provide configurations that aren't tied to a specific Build Configuration name. For example we might have an `appsettings.QA.json` with a `DebugQA` build configuration. We might also have a `ReleaseQA` build configuration. In the case we build with either of these build configurations we might want it to pick up the QA build configuration. We can pick these configurations up by enabling Fuzzy Matching in our environment. + +```json +{ + "$schema": "https://mobilebuildtools.com/schemas/v2/buildtools.schema.json", + "environment": { + "enableFuzzyMatching": true + } +} +``` diff --git a/docs/schemas/v2/buildtools.schema.json b/docs/schemas/v2/buildtools.schema.json index 8808b358..0960f348 100644 --- a/docs/schemas/v2/buildtools.schema.json +++ b/docs/schemas/v2/buildtools.schema.json @@ -85,7 +85,12 @@ "null" ], "properties": { + "enableFuzzyMatching": { + "type": "boolean", + "description": "This will allow fuzzy matching for environment variables and appsettings." + }, "defaults": { + "description": "This expects a dictionary of key value pairs to use as defaults for the environment.", "type": [ "object", "null" @@ -98,6 +103,7 @@ } }, "configuration": { + "description": "This expects a key which may be explicitly for the Build Configuration, the Platform (Android, iOS, etc), or a combination of the Platform and Build Configuration separated by an underscore (iOS_Debug), with a value of a dictionary of key value pairs to use as defaults for the environment.", "type": [ "object", "null" @@ -535,4 +541,4 @@ "type": "boolean" } } -} \ No newline at end of file +} diff --git a/src/Mobile.BuildTools.AppSettings/Generators/AppSettingsGenerator.cs b/src/Mobile.BuildTools.AppSettings/Generators/AppSettingsGenerator.cs index f63f3ec7..bb0602d0 100644 --- a/src/Mobile.BuildTools.AppSettings/Generators/AppSettingsGenerator.cs +++ b/src/Mobile.BuildTools.AppSettings/Generators/AppSettingsGenerator.cs @@ -1,272 +1,264 @@ -using System.Diagnostics; -using System.Text.Json; -using CodeGenHelpers; -using Microsoft.CodeAnalysis; -using Mobile.BuildTools.AppSettings.Diagnostics; -using Mobile.BuildTools.AppSettings.Extensions; -using Mobile.BuildTools.Models.Settings; -using Mobile.BuildTools.Utils; - -namespace Mobile.BuildTools.AppSettings.Generators -{ - [Generator] - public sealed class AppSettingsGenerator : GeneratorBase - { - private const string DefaultPrefix = "BuildTools_"; - - public const string AutoGeneratedMessage = @"This code was generated by Mobile.BuildTools. For more information please visit -https://mobilebuildtools.com or to file an issue please see -https://github.com/dansiegel/Mobile.BuildTools - -Changes to this file may cause incorrect behavior and will be lost when -the code is regenerated. - -When I wrote this, only God and I understood what I was doing -Now, God only knows. - -NOTE: This file should be excluded from source control."; - - protected override void Generate() - { - var settings = ConfigHelper.GetSettingsConfig(ProjectName, Config); - if (settings is null || !settings.Any()) - return; - - var i = 0; - - var assembly = typeof(AppSettingsGenerator).Assembly; - var toolVersion = FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion; - var compileGeneratedAttribute = @$"[GeneratedCodeAttribute(""{typeof(AppSettingsGenerator).FullName}"", ""{toolVersion}"")]"; - foreach (var settingsConfig in settings) - { - if (string.IsNullOrEmpty(settingsConfig.ClassName)) - settingsConfig.ClassName = i++ > 0 ? $"AppSettings{i}" : "AppSettings"; - else - { - settingsConfig.ClassName = settingsConfig.ClassName.Trim(); - if (settingsConfig.ClassName == "AppSettings") - i++; - } - - if (string.IsNullOrEmpty(settingsConfig.Namespace)) - settingsConfig.Namespace = "Helpers"; - else - settingsConfig.Namespace = settingsConfig.Namespace.Trim(); - - if (string.IsNullOrEmpty(settingsConfig.Delimiter)) - settingsConfig.Delimiter = ";"; - else - settingsConfig.Delimiter = settingsConfig.Delimiter.Trim(); - - if (string.IsNullOrEmpty(settingsConfig.Prefix)) - settingsConfig.Prefix = DefaultPrefix; - else - settingsConfig.Prefix = settingsConfig.Prefix.Trim(); - - if (string.IsNullOrEmpty(settingsConfig.RootNamespace)) - settingsConfig.RootNamespace = RootNamespace; - else - settingsConfig.RootNamespace = settingsConfig.RootNamespace.Trim(); - - var mergedSecrets = GetMergedSecrets(settingsConfig, out var hasErrors); - if (hasErrors) - continue; - - var namespaceParts = new[] - { - settingsConfig.RootNamespace, - settingsConfig.Namespace == "." ? string.Empty : settingsConfig.Namespace - }; - var fullyQualifiedNamespace = string.Join(".", namespaceParts.Where(x => !string.IsNullOrEmpty(x))); - - var builder = TryGetTypeSymbol($"{fullyQualifiedNamespace}.{settingsConfig.ClassName}", out var typeSymbol) - ? CodeBuilder.Create(typeSymbol) - : CodeBuilder.Create(fullyQualifiedNamespace) - .AddClass(settingsConfig.ClassName) - .WithAccessModifier(settingsConfig.Accessibility.ToRoslynAccessibility()) - .MakeStaticClass(); - - IEnumerable interfaces = []; - if(typeSymbol != null) - { - if (typeSymbol.IsStatic) - builder.MakeStaticClass(); - - interfaces = typeSymbol.Interfaces; - } - - builder.AddNamespaceImport("System") - .AddNamespaceImport("GeneratedCodeAttribute = System.CodeDom.Compiler.GeneratedCodeAttribute") - .Builder - .WithAutoGeneratedMessage(AutoGeneratedMessage); - - foreach(var valueConfig in settingsConfig.Properties) - { - AddProperty(ref builder, mergedSecrets, valueConfig, interfaces, settingsConfig.Delimiter, compileGeneratedAttribute); - } - - AddSource(builder); - } - } - - private void AddProperty(ref ClassBuilder builder, IDictionary secrets, ValueConfig valueConfig, IEnumerable interfaces, string delimeter, string compileGeneratedAttribute) - { - if (!secrets.ContainsKey(valueConfig.Name)) - return; - - var value = secrets[valueConfig.Name]; - var output = string.Empty; - var isArray = valueConfig.IsArray ?? false; - var mapping = valueConfig.PropertyType.GetPropertyTypeMapping(); - var valueHandler = mapping.Handler; - var typeDeclaration = mapping.Type.GetStandardTypeName(); - var type = isArray ? mapping.Type.MakeArrayType() : mapping.Type; - var propBuilder = builder.AddProperty(valueConfig.Name) - .SetType(type) - .MakePublicProperty(); - - var symbol = interfaces.SelectMany(x => x.GetMembers()) - .OfType() - .Where(x => x.Name == valueConfig.Name) - .FirstOrDefault(); - - var valueType = GetValueType(value, valueConfig.PropertyType); - if (value is null || value.ToLower() == "null" || value.ToLower() == "default") - { - if (type == typeof(bool) && !isArray) - { - output = bool.FalseString.ToLower(); - } - else if (isArray) - { - output = $"global::System.Array.Empty<{typeDeclaration}>()"; - } - else - { - output = "default"; - } - } - else if (isArray) - { - var valueArray = GetValueArray(value, delimeter).Select(x => valueHandler.Format(x, false)); - output = "new " + typeDeclaration + "[] { " + string.Join(", ", valueArray) + " }"; - if (type == typeof(bool)) - { - output = output.ToLower(); - } - } - else - { - output = valueHandler.Format(value, false); - if (type == typeof(bool)) - { - output = output.ToLower(); - } - } - - if (symbol is not null) - { - propBuilder.WithGetterExpression(output); - } - else if (!isArray && valueConfig.PropertyType == PropertyType.String) - { - propBuilder.WithConstValue(output); - } - else - { - propBuilder.MakeStatic() - .WithReadonlyValue(output, valueType: valueType); - } - - propBuilder.AddAttribute(compileGeneratedAttribute); - } - - private string[] GetValueArray(string value, string delimeter) => - value.Split(delimeter[0]) - .Select(x => x.Trim()) - .Where(x => !string.IsNullOrEmpty(x)) - .ToArray(); - - private CodeGenHelpers.ValueType GetValueType(string value, PropertyType propertyType) - { - switch(value?.ToLower()) - { - case null when propertyType == PropertyType.String: - case "null" when propertyType == PropertyType.String: - return CodeGenHelpers.ValueType.Null; - case null: - case "null": - case "default": - return CodeGenHelpers.ValueType.Default; - default: - return CodeGenHelpers.ValueType.UserSpecified; - } - } - - private IDictionary GetEnvironmentSettings() - { - var buildToolsEnvFile = GeneratorContext.AdditionalFiles.FirstOrDefault(x => Path.GetFileName(x.Path) == Constants.BuildToolsEnvironmentSettings); - if (buildToolsEnvFile is null) - return new Dictionary(); - - var json = buildToolsEnvFile.GetText().ToString(); - if (string.IsNullOrEmpty(json)) - return new Dictionary(); - - return JsonSerializer.Deserialize(json, new JsonSerializerOptions(JsonSerializerDefaults.General)).Environment; - } - - internal IDictionary GetMergedSecrets(SettingsConfig settingsConfig, out bool hasErrors) - { - if (string.IsNullOrEmpty(settingsConfig.Prefix)) - settingsConfig.Prefix = "BuildTools_"; - - var env = GetEnvironmentSettings(); - var secrets = new Dictionary(); - hasErrors = false; - foreach (var prop in settingsConfig.Properties) - { - var prefixKey = settingsConfig.Prefix.EndsWith("_") ? $"{settingsConfig.Prefix}{prop.Name}" : $"{settingsConfig.Prefix}_{prop.Name}"; - - var searchKeys = new List - { - prop.Name, - prefixKey - }; - - if (settingsConfig.Prefix != DefaultPrefix) - { - searchKeys.Add($"{DefaultPrefix}{prefixKey}"); - } - - string key = null; - foreach (var searchKey in searchKeys) - { - if (!string.IsNullOrEmpty(key)) - break; - - key = env.Keys.FirstOrDefault(x => - x.Equals(searchKey, StringComparison.InvariantCultureIgnoreCase) || - x.Equals($"{BuildConfiguration}_{searchKey}", StringComparison.InvariantCultureIgnoreCase)); - } - - if (string.IsNullOrEmpty(key)) - { - if (string.IsNullOrEmpty(prop.DefaultValue)) - { - ReportDiagnostic(Descriptors.MissingAppSettingsProperty, prop.Name); - hasErrors = true; - continue; - } - - secrets[prop.Name] = prop.DefaultValue == "null" || prop.DefaultValue == "default" ? null : prop.DefaultValue; - continue; - } - - secrets[prop.Name] = env[key]; - } - - return secrets; - } - } -} +using System.Diagnostics; +using System.Text.Json; +using CodeGenHelpers; +using Microsoft.CodeAnalysis; +using Mobile.BuildTools.AppSettings.Diagnostics; +using Mobile.BuildTools.AppSettings.Extensions; +using Mobile.BuildTools.Models.Settings; +using Mobile.BuildTools.Utils; + +namespace Mobile.BuildTools.AppSettings.Generators +{ + [Generator] + public sealed class AppSettingsGenerator : GeneratorBase + { + private const string _defaultPrefix = "BuildTools_"; + + public const string AutoGeneratedMessage = @"This code was generated by Mobile.BuildTools. For more information please visit +https://mobilebuildtools.com or to file an issue please see +https://github.com/dansiegel/Mobile.BuildTools + +Changes to this file may cause incorrect behavior and will be lost when +the code is regenerated. + +When I wrote this, only God and I understood what I was doing +Now, God only knows. + +NOTE: This file should be excluded from source control."; + + protected override void Generate() + { + var settings = ConfigHelper.GetSettingsConfig(ProjectName, Config); + if (settings is null || !settings.Any()) + return; + + var i = 0; + + var assembly = typeof(AppSettingsGenerator).Assembly; + var toolVersion = FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion; + var compileGeneratedAttribute = @$"[GeneratedCodeAttribute(""{typeof(AppSettingsGenerator).FullName}"", ""{toolVersion}"")]"; + foreach (var settingsConfig in settings) + { + if (string.IsNullOrEmpty(settingsConfig.ClassName)) + settingsConfig.ClassName = i++ > 0 ? $"AppSettings{i}" : "AppSettings"; + else + { + settingsConfig.ClassName = settingsConfig.ClassName.Trim(); + if (settingsConfig.ClassName == "AppSettings") + i++; + } + + if (string.IsNullOrEmpty(settingsConfig.Namespace)) + settingsConfig.Namespace = "Helpers"; + else + settingsConfig.Namespace = settingsConfig.Namespace.Trim(); + + if (string.IsNullOrEmpty(settingsConfig.Delimiter)) + settingsConfig.Delimiter = ";"; + else + settingsConfig.Delimiter = settingsConfig.Delimiter.Trim(); + + if (string.IsNullOrEmpty(settingsConfig.Prefix)) + settingsConfig.Prefix = _defaultPrefix; + else + settingsConfig.Prefix = settingsConfig.Prefix.Trim(); + + if (string.IsNullOrEmpty(settingsConfig.RootNamespace)) + settingsConfig.RootNamespace = RootNamespace; + else + settingsConfig.RootNamespace = settingsConfig.RootNamespace.Trim(); + + var mergedSecrets = GetMergedSecrets(settingsConfig, out var hasErrors); + if (hasErrors) + continue; + + var namespaceParts = new[] + { + settingsConfig.RootNamespace, + settingsConfig.Namespace == "." ? string.Empty : settingsConfig.Namespace + }; + var fullyQualifiedNamespace = string.Join(".", namespaceParts.Where(x => !string.IsNullOrEmpty(x))); + + var builder = TryGetTypeSymbol($"{fullyQualifiedNamespace}.{settingsConfig.ClassName}", out var typeSymbol) + ? CodeBuilder.Create(typeSymbol) + : CodeBuilder.Create(fullyQualifiedNamespace) + .AddClass(settingsConfig.ClassName) + .WithAccessModifier(settingsConfig.Accessibility.ToRoslynAccessibility()) + .MakeStaticClass(); + + IEnumerable interfaces = []; + if(typeSymbol != null) + { + if (typeSymbol.IsStatic) + builder.MakeStaticClass(); + + interfaces = typeSymbol.Interfaces; + } + + builder.AddNamespaceImport("System") + .AddNamespaceImport("GeneratedCodeAttribute = System.CodeDom.Compiler.GeneratedCodeAttribute") + .Builder + .WithAutoGeneratedMessage(AutoGeneratedMessage); + + foreach(var valueConfig in settingsConfig.Properties) + { + AddProperty(ref builder, mergedSecrets, valueConfig, interfaces, settingsConfig.Delimiter, compileGeneratedAttribute); + } + + AddSource(builder); + } + } + + private void AddProperty(ref ClassBuilder builder, IDictionary secrets, ValueConfig valueConfig, IEnumerable interfaces, string delimeter, string compileGeneratedAttribute) + { + if (!secrets.ContainsKey(valueConfig.Name)) + return; + + var value = secrets[valueConfig.Name]; + var output = string.Empty; + var isArray = valueConfig.IsArray ?? false; + var mapping = valueConfig.PropertyType.GetPropertyTypeMapping(); + var valueHandler = mapping.Handler; + var typeDeclaration = mapping.Type.GetStandardTypeName(); + var type = isArray ? mapping.Type.MakeArrayType() : mapping.Type; + var propBuilder = builder.AddProperty(valueConfig.Name) + .SetType(type) + .MakePublicProperty(); + + var symbol = interfaces.SelectMany(x => x.GetMembers()) + .OfType() + .Where(x => x.Name == valueConfig.Name) + .FirstOrDefault(); + + var valueType = GetValueType(value, valueConfig.PropertyType); + if (value is null || value.ToLower() == "null" || value.ToLower() == "default") + { + if (type == typeof(bool) && !isArray) + { + output = bool.FalseString.ToLower(); + } + else if (isArray) + { + output = $"global::System.Array.Empty<{typeDeclaration}>()"; + } + else + { + output = "default"; + } + } + else if (isArray) + { + var valueArray = GetValueArray(value, delimeter).Select(x => valueHandler.Format(x, false)); + output = "new " + typeDeclaration + "[] { " + string.Join(", ", valueArray) + " }"; + if (type == typeof(bool)) + { + output = output.ToLower(); + } + } + else + { + output = valueHandler.Format(value, false); + if (type == typeof(bool)) + { + output = output.ToLower(); + } + } + + if (symbol is not null) + { + propBuilder.WithGetterExpression(output); + } + else if (!isArray && valueConfig.PropertyType == PropertyType.String) + { + propBuilder.WithConstValue(output); + } + else + { + propBuilder.MakeStatic() + .WithReadonlyValue(output, valueType: valueType); + } + + propBuilder.AddAttribute(compileGeneratedAttribute); + } + + private string[] GetValueArray(string value, string delimeter) => + value.Split(delimeter[0]) + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToArray(); + + private CodeGenHelpers.ValueType GetValueType(string value, PropertyType propertyType) + { + switch(value?.ToLower()) + { + case null when propertyType == PropertyType.String: + case "null" when propertyType == PropertyType.String: + return CodeGenHelpers.ValueType.Null; + case null: + case "null": + case "default": + return CodeGenHelpers.ValueType.Default; + default: + return CodeGenHelpers.ValueType.UserSpecified; + } + } + + internal IDictionary GetMergedSecrets(SettingsConfig settingsConfig, out bool hasErrors) + { + if (string.IsNullOrEmpty(settingsConfig.Prefix)) + settingsConfig.Prefix = "BuildTools_"; + + var env = Environment.Environment; + var secrets = new Dictionary(); + hasErrors = false; + foreach (var prop in settingsConfig.Properties) + { + var prefixKey = settingsConfig.Prefix.EndsWith("_") ? $"{settingsConfig.Prefix}{prop.Name}" : $"{settingsConfig.Prefix}_{prop.Name}"; + + var searchKeys = new List + { + $"{Environment.TargetPlatform}_{Environment.BuildConfiguration}_{prefixKey}", + $"{Environment.TargetPlatform}_{prefixKey}", + prefixKey, + }; + + if (settingsConfig.Prefix != _defaultPrefix) + { + searchKeys.Add($"{Environment.TargetPlatform}_{Environment.BuildConfiguration}_{_defaultPrefix}_{prefixKey}"); + searchKeys.Add($"{Environment.TargetPlatform}_{_defaultPrefix}_{prefixKey}"); + searchKeys.Add($"{_defaultPrefix}{prefixKey}"); + } + + searchKeys.Add(prop.Name); + + string key = null; + foreach (var searchKey in searchKeys) + { + if (!string.IsNullOrEmpty(key)) + break; + + key = env.Keys.FirstOrDefault(x => + x.Equals(searchKey, StringComparison.InvariantCultureIgnoreCase) || + x.Equals($"{BuildConfiguration}_{searchKey}", StringComparison.InvariantCultureIgnoreCase)); + } + + if (string.IsNullOrEmpty(key)) + { + if (string.IsNullOrEmpty(prop.DefaultValue)) + { + ReportDiagnostic(Descriptors.MissingAppSettingsProperty, prop.Name); + hasErrors = true; + continue; + } + + secrets[prop.Name] = prop.DefaultValue == "null" || prop.DefaultValue == "default" ? null : prop.DefaultValue; + continue; + } + + secrets[prop.Name] = env[key]; + } + + return secrets; + } + } +} diff --git a/src/Mobile.BuildTools.AppSettings/Generators/GeneratorBase.cs b/src/Mobile.BuildTools.AppSettings/Generators/GeneratorBase.cs index 4f8ecdca..e294b438 100644 --- a/src/Mobile.BuildTools.AppSettings/Generators/GeneratorBase.cs +++ b/src/Mobile.BuildTools.AppSettings/Generators/GeneratorBase.cs @@ -22,6 +22,8 @@ public abstract class GeneratorBase : ISourceGenerator protected BuildToolsConfig Config { get; private set; } + protected BuildEnvironment Environment { get; private set; } + public void Execute(GeneratorExecutionContext context) { GeneratorContext = context; @@ -39,6 +41,10 @@ public void Execute(GeneratorExecutionContext context) var json = buildToolsConfig.GetText().ToString(); Config = JsonSerializer.Deserialize(json, ConfigHelper.GetSerializerSettings()); + var buildToolsEnvFile = GeneratorContext.AdditionalFiles.FirstOrDefault(x => Path.GetFileName(x.Path) == Constants.BuildToolsEnvironmentSettings); + json = buildToolsEnvFile.GetText().ToString(); + Environment = JsonSerializer.Deserialize(json, new JsonSerializerOptions(JsonSerializerDefaults.General)) ?? new BuildEnvironment(); + try { Generate(); diff --git a/src/Mobile.BuildTools.Core/Tasks/EnvironmentSettingsTask.cs b/src/Mobile.BuildTools.Core/Tasks/EnvironmentSettingsTask.cs index 138455a6..53c91009 100644 --- a/src/Mobile.BuildTools.Core/Tasks/EnvironmentSettingsTask.cs +++ b/src/Mobile.BuildTools.Core/Tasks/EnvironmentSettingsTask.cs @@ -1,59 +1,62 @@ -using System.Text.Json; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Mobile.BuildTools.Build; -using Mobile.BuildTools.Utils; - -namespace Mobile.BuildTools.Tasks; - -public class EnvironmentSettingsTask : BuildToolsTaskBase -{ - [Output] - public ITaskItem[] EnvironmentSettings { get; private set; } = []; - internal override void ExecuteInternal(IBuildConfiguration config) - { - var outputPath = Path.Combine(IntermediateOutputPath, "Mobile.BuildTools", Constants.BuildToolsEnvironmentSettings); - - var fileInfo = new FileInfo(outputPath); - if (!fileInfo.Directory.Exists) - { - fileInfo.Directory.Create(); - } - - if (fileInfo.Exists) - { - fileInfo.Delete(); - } - - var environment = new BuildEnvironment - { - BuildNumber = CIBuildEnvironmentUtils.BuildNumber, - IsCI = CIBuildEnvironmentUtils.IsCI, - IsAppCenter = CIBuildEnvironmentUtils.IsAppCenter, - IsAppVeyor = CIBuildEnvironmentUtils.IsAppVeyor, - IsAzureDevOps = CIBuildEnvironmentUtils.IsAzureDevOps, - IsBitBucket = CIBuildEnvironmentUtils.IsBitBucket, - IsBuildHost = CIBuildEnvironmentUtils.IsBuildHost, - IsGitHubActions = CIBuildEnvironmentUtils.IsGitHubActions, - IsJenkins = CIBuildEnvironmentUtils.IsJenkins, - IsTeamCity = CIBuildEnvironmentUtils.IsTeamCity, - IsTravisCI = CIBuildEnvironmentUtils.IsTravisCI - }; - - if (this is IBuildConfiguration buildConfiguration && buildConfiguration.Configuration.AppSettings is not null && - buildConfiguration.Configuration.AppSettings.TryGetValue(ProjectName, out var settings) && settings.Any()) - { - var env = EnvironmentAnalyzer.GatherEnvironmentVariables(this); - if (env.Count > 0) - { - environment.Environment = env; - } - } - - File.WriteAllText(outputPath, JsonSerializer.Serialize(environment, new JsonSerializerOptions(JsonSerializerDefaults.General) - { - WriteIndented = true - })); - EnvironmentSettings = [new TaskItem(outputPath)]; - } -} +using System.Text.Json; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mobile.BuildTools.Build; +using Mobile.BuildTools.Utils; + +namespace Mobile.BuildTools.Tasks; + +public class EnvironmentSettingsTask : BuildToolsTaskBase +{ + [Output] + public ITaskItem[] EnvironmentSettings { get; private set; } = []; + internal override void ExecuteInternal(IBuildConfiguration config) + { + var outputPath = Path.Combine(IntermediateOutputPath, "Mobile.BuildTools", Constants.BuildToolsEnvironmentSettings); + + var fileInfo = new FileInfo(outputPath); + if (!fileInfo.Directory.Exists) + { + fileInfo.Directory.Create(); + } + + if (fileInfo.Exists) + { + fileInfo.Delete(); + } + + var environment = new BuildEnvironment + { + BuildNumber = CIBuildEnvironmentUtils.BuildNumber, + IsCI = CIBuildEnvironmentUtils.IsCI, + IsAppCenter = CIBuildEnvironmentUtils.IsAppCenter, + IsAppVeyor = CIBuildEnvironmentUtils.IsAppVeyor, + IsAzureDevOps = CIBuildEnvironmentUtils.IsAzureDevOps, + IsBitBucket = CIBuildEnvironmentUtils.IsBitBucket, + IsBuildHost = CIBuildEnvironmentUtils.IsBuildHost, + IsGitHubActions = CIBuildEnvironmentUtils.IsGitHubActions, + IsJenkins = CIBuildEnvironmentUtils.IsJenkins, + IsTeamCity = CIBuildEnvironmentUtils.IsTeamCity, + IsTravisCI = CIBuildEnvironmentUtils.IsTravisCI, + BuildConfiguration = config.BuildConfiguration, + TargetPlatform = config.Platform + }; + + if (config.Configuration.AppSettings is not null && + config.Configuration.AppSettings.TryGetValue(ProjectName, out var settings) && + settings.Any()) + { + var env = EnvironmentAnalyzer.GatherEnvironmentVariables(this); + if (env.Count > 0) + { + environment.Environment = env; + } + } + + File.WriteAllText(outputPath, JsonSerializer.Serialize(environment, new JsonSerializerOptions(JsonSerializerDefaults.General) + { + WriteIndented = true + })); + EnvironmentSettings = [new TaskItem(outputPath)]; + } +} diff --git a/src/Mobile.BuildTools.Reference/Models/EnvironmentSettings.cs b/src/Mobile.BuildTools.Reference/Models/EnvironmentSettings.cs index df863352..887bb7b2 100644 --- a/src/Mobile.BuildTools.Reference/Models/EnvironmentSettings.cs +++ b/src/Mobile.BuildTools.Reference/Models/EnvironmentSettings.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Mobile.BuildTools.Models -{ - public class EnvironmentSettings - { - public EnvironmentSettings() - { - Defaults = new Dictionary(); - Configuration = new Dictionary>(); - } - - [JsonPropertyName("defaults")] - public Dictionary Defaults { get; set; } - - [JsonPropertyName("configuration")] - public Dictionary> Configuration { get; set; } - } -} +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Mobile.BuildTools.Models +{ + public class EnvironmentSettings + { + [JsonPropertyName("enableFuzzyMatching")] + public bool EnableFuzzyMatching { get; set; } + + [JsonPropertyName("defaults")] + public Dictionary Defaults { get; set; } = []; + + [JsonPropertyName("configuration")] + public Dictionary> Configuration { get; set; } = []; + } +} diff --git a/src/Mobile.BuildTools.Reference/Utils/BuildEnvironment.cs b/src/Mobile.BuildTools.Reference/Utils/BuildEnvironment.cs index 22be7257..6a7ae1e6 100644 --- a/src/Mobile.BuildTools.Reference/Utils/BuildEnvironment.cs +++ b/src/Mobile.BuildTools.Reference/Utils/BuildEnvironment.cs @@ -1,20 +1,22 @@ -using System.Collections.Generic; - -namespace Mobile.BuildTools.Utils -{ - public class BuildEnvironment - { - public bool IsCI { get; set; } - public bool IsAppCenter { get; set; } - public bool IsAppVeyor { get; set; } - public bool IsTeamCity { get; set; } - public bool IsJenkins { get; set; } - public bool IsAzureDevOps { get; set; } - public bool IsBitBucket { get; set; } - public bool IsGitHubActions { get; set; } - public bool IsTravisCI { get; set; } - public bool IsBuildHost { get; set; } - public string BuildNumber { get; set; } - public IDictionary Environment { get; set; } = new Dictionary(); - } -} +using System.Collections.Generic; + +namespace Mobile.BuildTools.Utils +{ + public class BuildEnvironment + { + public bool IsCI { get; set; } + public bool IsAppCenter { get; set; } + public bool IsAppVeyor { get; set; } + public bool IsTeamCity { get; set; } + public bool IsJenkins { get; set; } + public bool IsAzureDevOps { get; set; } + public bool IsBitBucket { get; set; } + public bool IsGitHubActions { get; set; } + public bool IsTravisCI { get; set; } + public bool IsBuildHost { get; set; } + public string BuildNumber { get; set; } + public string BuildConfiguration { get; set; } + public Platform TargetPlatform { get; set; } + public IDictionary Environment { get; set; } = new Dictionary(); + } +} diff --git a/src/Mobile.BuildTools.Reference/Utils/EnvironmentAnalyzer.cs b/src/Mobile.BuildTools.Reference/Utils/EnvironmentAnalyzer.cs index 97083bb4..9b749adf 100644 --- a/src/Mobile.BuildTools.Reference/Utils/EnvironmentAnalyzer.cs +++ b/src/Mobile.BuildTools.Reference/Utils/EnvironmentAnalyzer.cs @@ -1,346 +1,400 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using Mobile.BuildTools.Build; -using Mobile.BuildTools.Handlers; - -namespace Mobile.BuildTools.Utils -{ - public static class EnvironmentAnalyzer - { - private const string DefaultSecretPrefix = "BuildTools_"; - private const string LegacySecretPrefix = "Secret_"; - private const string DefaultManifestPrefix = "Manifest_"; - - public static IDictionary GatherEnvironmentVariables(IBuildConfiguration buildConfiguration = null, bool includeManifest = false) - { - var env = new Dictionary(); - if (buildConfiguration is null) - { - foreach (var key in Environment.GetEnvironmentVariables().Keys) - { - env[key.ToString()] = Environment.GetEnvironmentVariable(key.ToString()); - } - - return env; - } - - env = GetEnvironmentVariables(buildConfiguration); - - var projectDirectory = new DirectoryInfo(buildConfiguration.ProjectDirectory); - var solutionDirectory = new DirectoryInfo(buildConfiguration.SolutionDirectory); - var configuration = buildConfiguration.BuildConfiguration; - var directories = new List - { - solutionDirectory, - projectDirectory - }; - - if (buildConfiguration.Platform != Platform.Unsupported) - { - new DirectoryInfo[] - { - new (Path.Combine(projectDirectory.FullName, buildConfiguration.Platform.ToString())), - new (Path.Combine(projectDirectory.FullName, "Platforms", buildConfiguration.Platform.ToString())) - } - .Where(x => x.Exists) - .ForEach(directories.Add); +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; +using Mobile.BuildTools.Build; +using Mobile.BuildTools.Handlers; +using Mobile.BuildTools.Models; + +namespace Mobile.BuildTools.Utils +{ + public static class EnvironmentAnalyzer + { + private const string DefaultSecretPrefix = "BuildTools_"; + private const string LegacySecretPrefix = "Secret_"; + private const string DefaultManifestPrefix = "Manifest_"; + + public static IDictionary GatherEnvironmentVariables(IBuildConfiguration buildConfiguration = null, bool includeManifest = false) + { + var env = GetEnvironmentVariables(buildConfiguration); + + var configuration = buildConfiguration.BuildConfiguration; + + if (buildConfiguration is null) + { + return env; + } + + var projectDirectory = new DirectoryInfo(buildConfiguration.ProjectDirectory); + var solutionDirectory = new DirectoryInfo(buildConfiguration.SolutionDirectory); + var directories = new List + { + solutionDirectory, + projectDirectory + }; + + if (buildConfiguration.Platform != Platform.Unsupported) + { + new DirectoryInfo[] + { + new (Path.Combine(projectDirectory.FullName, buildConfiguration.Platform.ToString())), + new (Path.Combine(projectDirectory.FullName, "Platforms", buildConfiguration.Platform.ToString())) + } + .Where(x => x.Exists) + .ForEach(directories.Add); + } + + var stoppingDir = solutionDirectory.Parent; + var lookupDir = projectDirectory; + do + { + lookupDir = lookupDir.Parent; + if (lookupDir is null || stoppingDir.FullName == lookupDir.FullName) + { + break; + } + else if (!directories.Contains(lookupDir)) + { + directories.Add(lookupDir); + } + } while (lookupDir != solutionDirectory); + + directories = directories.Select(x => + { + var dir = x.FullName; + if (dir.EndsWith($"{Path.DirectorySeparatorChar}")) + dir = dir.Substring(0, dir.Length - 1); + + return dir; + }) + .Distinct() + .Select(x => new DirectoryInfo(x)) + .Where(x => x.Exists) + .ToList(); + + string[] expectedFileNames = + [ + Constants.SecretsJsonFileName, + string.Format(Constants.SecretsJsonConfigurationFileFormat, configuration) + ]; + foreach (var fileName in expectedFileNames) + { + foreach (var directory in directories) + { + var file = new FileInfo(Path.Combine(directory.FullName, fileName)); + if (file.Exists) + { + buildConfiguration.Logger.LogWarning($"The secrets.json has been deprecated and will no longer be supported in a future version. Please migrate '{fileName}' to appsettings.json"); + LoadSecrets(file.FullName, ref env); + break; + } + } + } + + foreach (var directory in directories) + { + var files = directory.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly); + if (!files.Any(x => x.Name.StartsWith("appsettings"))) + continue; + + var didLoad = false; + bool TryLoadFile(string fileName) + { + if (files.Any(x => x.Name == fileName)) + { + didLoad = true; + LoadSecrets(Path.Combine(directory.FullName, fileName), ref env); + return true; + } + + return false; + } + + var searchConfiguration = configuration; + if (buildConfiguration.Configuration.Environment?.EnableFuzzyMatching ?? false) + { + var availableMatches = files.Select(x => + { + var match = Regex.Match(x.Name, @"appsettings\.(?[a-zA-Z0-9]+)\.json"); + if (match.Success) + { + return match.Groups["config"].Value; + } + return null; + }).Where(x => !string.IsNullOrEmpty(x)); + + if (!availableMatches.Any(x => x == searchConfiguration) && availableMatches.Any(searchConfiguration.Contains)) + { + searchConfiguration = availableMatches.First(searchConfiguration.Contains); + } + } + + TryLoadFile(Constants.AppSettingsJsonFileName); + TryLoadFile(string.Format(Constants.AppSettingsJsonConfigurationFileFormat, searchConfiguration)); + TryLoadFile(string.Format(Constants.AppSettingsJsonConfigurationFileFormat, $"{buildConfiguration.Platform}")); + TryLoadFile(string.Format(Constants.AppSettingsJsonConfigurationFileFormat, $"{buildConfiguration.Platform}.{searchConfiguration}")); + + if (didLoad) + break; + } + + if (includeManifest) + { + directories + .SelectMany(x => + x.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly) + .Where(x => x.Name == Constants.ManifestJsonFileName)) + .ForEach(x => LoadSecrets(x.FullName, ref env)); + LoadSecrets(Path.Combine(projectDirectory.FullName, Constants.ManifestJsonFileName), ref env); + LoadSecrets(Path.Combine(solutionDirectory.FullName, Constants.ManifestJsonFileName), ref env); } - var stoppingDir = solutionDirectory.Parent; - var lookupDir = projectDirectory; - do + if (buildConfiguration.Configuration.Environment?.EnableFuzzyMatching ?? false) { - lookupDir = lookupDir.Parent; - if (lookupDir is null || stoppingDir.FullName == lookupDir.FullName) + var keys = env.Keys.ToArray(); + foreach (var key in keys) { - break; - } - else if (!directories.Contains(lookupDir)) - { - directories.Add(lookupDir); - } - } while (lookupDir != solutionDirectory); - - directories = directories.Select(x => - { - var dir = x.FullName; - if (dir.EndsWith($"{Path.DirectorySeparatorChar}")) - dir = dir.Substring(0, dir.Length - 1); - - return dir; - }) - .Distinct() - .Select(x => new DirectoryInfo(x)) - .Where(x => x.Exists) - .ToList(); - - string[] expectedFileNames = - [ - Constants.SecretsJsonFileName, - string.Format(Constants.SecretsJsonConfigurationFileFormat, configuration) - ]; - foreach (var fileName in expectedFileNames) - { - foreach (var directory in directories) - { - var file = new FileInfo(Path.Combine(directory.FullName, fileName)); - if (file.Exists) + var match = Regex.Match(key, @"^(?[a-zA-Z0-9]+)_(?.+)$"); + if (match.Success) { - buildConfiguration.Logger.LogWarning($"The secrets.json has been deprecated and will no longer be supported in a future version. Please migrate '{fileName}' to appsettings.json"); - LoadSecrets(file.FullName, ref env); - break; + var newKey = match.Groups["key"].Value; + if (!env.ContainsKey(newKey)) + { + env[newKey] = env[key]; + } } } - } - - expectedFileNames = - [ - Constants.AppSettingsJsonFileName, - string.Format(Constants.AppSettingsJsonConfigurationFileFormat, configuration), - string.Format(Constants.AppSettingsJsonConfigurationFileFormat, $"{buildConfiguration.Platform}"), - string.Format(Constants.AppSettingsJsonConfigurationFileFormat, $"{buildConfiguration.Platform}.{configuration}") - ]; - - foreach(var fileName in expectedFileNames) - { - foreach(var directory in directories) - { - var file = new FileInfo(Path.Combine(directory.FullName, fileName)); - if (file.Exists) - { - LoadSecrets(file.FullName, ref env); - break; - } - } - } - - if (includeManifest) - { - directories - .SelectMany(x => - x.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly) - .Where(x => x.Name == Constants.ManifestJsonFileName)) - .ForEach(x => LoadSecrets(x.FullName, ref env)); - LoadSecrets(Path.Combine(projectDirectory.FullName, Constants.ManifestJsonFileName), ref env); - LoadSecrets(Path.Combine(solutionDirectory.FullName, Constants.ManifestJsonFileName), ref env); - } - - if(buildConfiguration?.Configuration?.Environment != null) - { - var settings = buildConfiguration.Configuration.Environment; - var defaultSettings = settings.Defaults ?? []; - if(settings.Configuration != null && settings.Configuration.ContainsKey(configuration)) - { - foreach ((var key, var value) in settings.Configuration[configuration]) - defaultSettings[key] = value; - } - - UpdateVariables(defaultSettings, ref env); - } - - return env; - } - - private static Dictionary GetEnvironmentVariables(IBuildConfiguration buildConfiguration) - { - var env = new Dictionary(); - foreach ((var key, var value) in buildConfiguration.Configuration.Environment.Defaults) - { - env[key] = value; - } - - if (buildConfiguration.Configuration.Environment.Configuration.ContainsKey(buildConfiguration.BuildConfiguration)) - { - var configEnvironment = buildConfiguration.Configuration.Environment.Configuration[buildConfiguration.BuildConfiguration]; - if(configEnvironment is not null) - { - foreach((var key, var value) in configEnvironment) - { - env[key] = value; - } - } - } - - foreach (var key in Environment.GetEnvironmentVariables().Keys) - { - env[key.ToString()] = Environment.GetEnvironmentVariable(key.ToString()); - } - - return env; - } - - internal static void UpdateVariables(IDictionary settings, ref Dictionary output) - { - if (settings is null || settings.Count < 1) - return; - - foreach((var key, var value) in settings) - { - if (!output.ContainsKey(key)) - output[key] = value; - } - } - - // This should stop looking when: - // - We have found a solution - // - Either the Current directory or Parent is the Root i.e. c:\ - // - Either the Current directory or Parent is the User directory i.e. c:\Users\Dan - // - The Current directory contains the .git folder - public static string LocateSolution(string searchDirectory) - { - var di = new DirectoryInfo(searchDirectory); - if (di.EnumerateFiles("*.sln").Any() - || IsRootPath(di.Parent) - || di.EnumerateDirectories().Any(x => x.Name == ".git") - || IsRootPath(di)) - { - return searchDirectory; - } - - return LocateSolution(Directory.GetParent(searchDirectory).FullName); - } - - public static IEnumerable GetManifestPrefixes(Platform platform, string knownPrefix) - { - var prefixes = new List(GetSecretPrefixes(platform, forceIncludeDefault: true)) - { - DefaultManifestPrefix - }; - - if(!string.IsNullOrEmpty(knownPrefix)) - { - prefixes.Add(knownPrefix); - } - - var platformPrefix = GetPlatformManifestPrefix(platform); - if(!string.IsNullOrWhiteSpace(platformPrefix)) - { - prefixes.Add(platformPrefix); - } - - return prefixes; - } - - private static string GetPlatformManifestPrefix(Platform platform) => - platform switch - { - Platform.Android => "DroidManifest_", - Platform.iOS => "iOSManifest_", - Platform.Windows => "WindowsManifest_", - Platform.UWP => "UWPManifest_", - Platform.macOS => "MacManifest_", - Platform.Tizen => "TizenManifest_", - _ => null, - }; - - public static string[] GetPlatformSecretPrefix(Platform platform) => - platform switch - { - Platform.Android => ["DroidSecret_"], - Platform.iOS => ["iOSSecret_"], - Platform.Windows => ["WindowsSecret_"], - Platform.UWP => ["UWPSecret_"], - Platform.macOS => ["MacSecret_"], - Platform.Tizen => ["TizenSecret_"], - _ => [DefaultSecretPrefix, LegacySecretPrefix], - }; - - public static IEnumerable GetSecretPrefixes(Platform platform, bool forceIncludeDefault = false) - { - var prefixes = new List(GetPlatformSecretPrefix(platform)) - { - "SharedSecret_" - }; - - if(platform != Platform.Unsupported) - { - prefixes.Add("PlatformSecret_"); - } - - if(forceIncludeDefault && !prefixes.Contains(DefaultSecretPrefix)) - { - prefixes.Add(DefaultSecretPrefix); - prefixes.Add($"MB{DefaultManifestPrefix}"); - } - - return prefixes; - } - - public static IEnumerable GetSecretKeys(IEnumerable prefixes) - { - var variables = GatherEnvironmentVariables(); - return variables.Keys.Where(k => prefixes.Any(p => k.StartsWith(p))); - } - - public static IDictionary GetSecrets(IBuildConfiguration build, string knownPrefix) - { - var prefixes = GetSecretPrefixes(build.Platform); - if(!string.IsNullOrEmpty(knownPrefix)) - { - prefixes = new List(prefixes) - { - knownPrefix - }; - } - var keys = GetSecretKeys(prefixes); - var variables = GatherEnvironmentVariables().Where(p => keys.Any(k => k == p.Key)); - var output = new Dictionary(); - foreach(var prefix in prefixes) - { - foreach(var pair in variables.Where(v => v.Key.StartsWith(prefix))) - { - var key = Regex.Replace(pair.Key, prefix, ""); - output.Add(key, pair.Value); - } - } - - if (build?.Configuration?.Environment != null) - { - var configuration = build.BuildConfiguration; - var settings = build.Configuration.Environment; - var defaultSettings = settings.Defaults ?? []; - if (settings.Configuration != null && settings.Configuration.ContainsKey(configuration)) - { - foreach ((var key, var value) in settings.Configuration[configuration]) - defaultSettings[key] = value; - } - - UpdateVariables(defaultSettings, ref output); - } - - return output; - } - - public static bool IsInGitRepo(string projectPath) - { - var di = new DirectoryInfo(projectPath); - if (di.EnumerateDirectories().Any(x => x.Name == ".git")) - return true; - - if (IsRootPath(di)) - return false; - - return IsInGitRepo(di.Parent.FullName); - } - - private static bool IsRootPath(DirectoryInfo directoryPath) => - directoryPath.Root.FullName == directoryPath.FullName || - directoryPath.FullName == Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - - private static void LoadSecrets(string path, ref Dictionary env) - { - if (!File.Exists(path)) return; - - var json = File.ReadAllText(path); - var document = JsonDocument.Parse(json); - foreach(var setting in document.RootElement.EnumerateObject()) - { - env[setting.Name] = setting.GetPropertyValueAsString(); - } - } - } -} + } + + return new SortedDictionary(env); + } + + private static Dictionary GetEnvironmentVariables(IBuildConfiguration buildConfiguration) + { + var env = new Dictionary(); + var configuration = buildConfiguration.BuildConfiguration; + if (buildConfiguration?.Configuration?.Environment != null) + { + var settings = buildConfiguration.Configuration.Environment; + var defaultSettings = settings.Defaults ?? []; + if (settings.Configuration is not null) + { + bool ContainsConfigKey(string key, [MaybeNullWhen(false)] out string configurationKey) + { + if (settings.Configuration.ContainsKey(key)) + { + configurationKey = key; + return true; + } + else if (settings.EnableFuzzyMatching) + { + var availableKey = settings.Configuration.Keys.FirstOrDefault(key.StartsWith); + if (!string.IsNullOrEmpty(availableKey)) + { + configurationKey = availableKey; + return true; + } + } + + configurationKey = null; + return false; + } + + void MergeDefaultSettings(string configurationKey) + { + foreach ((var key, var value) in settings.Configuration[configurationKey]) + defaultSettings[key] = value; + } + + if (ContainsConfigKey(configuration, out var configurationKey)) + MergeDefaultSettings(configurationKey); + if (ContainsConfigKey($"{buildConfiguration.Platform}", out configurationKey)) + MergeDefaultSettings(configurationKey); + if (ContainsConfigKey($"{buildConfiguration.Platform}_{configuration}", out configurationKey)) + MergeDefaultSettings(configurationKey); + } + + UpdateVariables(defaultSettings, ref env); + } + + foreach (var key in Environment.GetEnvironmentVariables().Keys) + { + env[key.ToString()] = Environment.GetEnvironmentVariable(key.ToString()); + } + return env; + } + + internal static void UpdateVariables(IDictionary settings, ref Dictionary output) + { + if (settings is null || settings.Count < 1) + return; + + foreach((var key, var value) in settings) + { + if (!output.ContainsKey(key)) + output[key] = value; + } + } + + // This should stop looking when: + // - We have found a solution + // - Either the Current directory or Parent is the Root i.e. c:\ + // - Either the Current directory or Parent is the User directory i.e. c:\Users\Dan + // - The Current directory contains the .git folder + public static string LocateSolution(string searchDirectory) + { + var di = new DirectoryInfo(searchDirectory); + if (di.EnumerateFiles("*.sln").Any() + || IsRootPath(di.Parent) + || di.EnumerateDirectories().Any(x => x.Name == ".git") + || IsRootPath(di)) + { + return searchDirectory; + } + + return LocateSolution(Directory.GetParent(searchDirectory).FullName); + } + + public static IEnumerable GetManifestPrefixes(Platform platform, string knownPrefix) + { + var prefixes = new List(GetSecretPrefixes(platform, forceIncludeDefault: true)) + { + DefaultManifestPrefix + }; + + if(!string.IsNullOrEmpty(knownPrefix)) + { + prefixes.Add(knownPrefix); + } + + var platformPrefix = GetPlatformManifestPrefix(platform); + if(!string.IsNullOrWhiteSpace(platformPrefix)) + { + prefixes.Add(platformPrefix); + } + + return prefixes; + } + + private static string GetPlatformManifestPrefix(Platform platform) => + platform switch + { + Platform.Android => "DroidManifest_", + Platform.iOS => "iOSManifest_", + Platform.Windows => "WindowsManifest_", + Platform.UWP => "UWPManifest_", + Platform.macOS => "MacManifest_", + Platform.Tizen => "TizenManifest_", + _ => null, + }; + + public static string[] GetPlatformSecretPrefix(Platform platform) => + platform switch + { + Platform.Android => ["DroidSecret_"], + Platform.iOS => ["iOSSecret_"], + Platform.Windows => ["WindowsSecret_"], + Platform.UWP => ["UWPSecret_"], + Platform.macOS => ["MacSecret_"], + Platform.Tizen => ["TizenSecret_"], + _ => [DefaultSecretPrefix, LegacySecretPrefix], + }; + + public static IEnumerable GetSecretPrefixes(Platform platform, bool forceIncludeDefault = false) + { + var prefixes = new List(GetPlatformSecretPrefix(platform)) + { + "SharedSecret_" + }; + + if(platform != Platform.Unsupported) + { + prefixes.Add("PlatformSecret_"); + } + + if(forceIncludeDefault && !prefixes.Contains(DefaultSecretPrefix)) + { + prefixes.Add(DefaultSecretPrefix); + prefixes.Add($"MB{DefaultManifestPrefix}"); + } + + return prefixes; + } + + public static IEnumerable GetSecretKeys(IEnumerable prefixes) + { + var variables = GatherEnvironmentVariables(); + return variables.Keys.Where(k => prefixes.Any(p => k.StartsWith(p))); + } + + public static IDictionary GetSecrets(IBuildConfiguration build, string knownPrefix) + { + var prefixes = GetSecretPrefixes(build.Platform); + if(!string.IsNullOrEmpty(knownPrefix)) + { + prefixes = new List(prefixes) + { + knownPrefix + }; + } + var keys = GetSecretKeys(prefixes); + var variables = GatherEnvironmentVariables().Where(p => keys.Any(k => k == p.Key)); + var output = new Dictionary(); + foreach(var prefix in prefixes) + { + foreach(var pair in variables.Where(v => v.Key.StartsWith(prefix))) + { + var key = Regex.Replace(pair.Key, prefix, ""); + output.Add(key, pair.Value); + } + } + + if (build?.Configuration?.Environment != null) + { + var configuration = build.BuildConfiguration; + var settings = build.Configuration.Environment; + var defaultSettings = settings.Defaults ?? []; + if (settings.Configuration != null && settings.Configuration.ContainsKey(configuration)) + { + foreach ((var key, var value) in settings.Configuration[configuration]) + defaultSettings[key] = value; + } + + UpdateVariables(defaultSettings, ref output); + } + + return output; + } + + public static bool IsInGitRepo(string projectPath) + { + var di = new DirectoryInfo(projectPath); + if (di.EnumerateDirectories().Any(x => x.Name == ".git")) + return true; + + if (IsRootPath(di)) + return false; + + return IsInGitRepo(di.Parent.FullName); + } + + private static bool IsRootPath(DirectoryInfo directoryPath) => + directoryPath.Root.FullName == directoryPath.FullName || + directoryPath.FullName == Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + private static void LoadSecrets(string path, ref Dictionary env) + { + if (!File.Exists(path)) return; + + var json = File.ReadAllText(path); + var document = JsonDocument.Parse(json); + foreach(var setting in document.RootElement.EnumerateObject()) + { + env[setting.Name] = setting.GetPropertyValueAsString(); + } + } + } +} diff --git a/tests/Mobile.BuildTools.Tests/Fixtures/Utils/EnvironmentAnalyzerFixture.cs b/tests/Mobile.BuildTools.Tests/Fixtures/Utils/EnvironmentAnalyzerFixture.cs index 20791bb4..f14d1f3b 100644 --- a/tests/Mobile.BuildTools.Tests/Fixtures/Utils/EnvironmentAnalyzerFixture.cs +++ b/tests/Mobile.BuildTools.Tests/Fixtures/Utils/EnvironmentAnalyzerFixture.cs @@ -1,220 +1,322 @@ -using System.Text.Json; -using Mobile.BuildTools.Models.Settings; -using Mobile.BuildTools.Utils; -using Xunit; -using Xunit.Abstractions; - -namespace Mobile.BuildTools.Tests.Fixtures.Utils -{ - public class EnvironmentAnalyzerFixture(ITestOutputHelper testOutputHelper) : FixtureBase(null, testOutputHelper), IDisposable - { - [Theory] - [InlineData("secrets.json")] - [InlineData("appsettings.json")] - public void GetsValuesFromJson(string filename) - { - var config = GetConfiguration($"{nameof(GetsValuesFromJson)}-{Path.GetFileNameWithoutExtension(filename)}"); - var settingsConfig = new SettingsConfig - { - Properties = - [ - new ValueConfig - { - Name = "SampleProp", - PropertyType = PropertyType.String - } - ] - }; - config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); - - var secrets = new - { - SampleProp = "Hello Tests" - }; - File.WriteAllText(Path.Combine(config.ProjectDirectory, filename), JsonSerializer.Serialize(secrets)); - - var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); - - Assert.True(mergedSecrets.ContainsKey("SampleProp")); - Assert.Equal(secrets.SampleProp, mergedSecrets["SampleProp"]); - } - - [Fact] - public void GetsValueFromBuildConfigurationOverrideJson() - { - var config = GetConfiguration(); - var settingsConfig = new SettingsConfig - { - Properties = - [ - new ValueConfig - { - Name = "SampleProp", - PropertyType = PropertyType.String - } - ] - }; - - config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); - - var secrets = new - { - SampleProp = "Hello Tests" - }; - File.WriteAllText(Path.Combine(config.ProjectDirectory, "appsettings.json"), JsonSerializer.Serialize(secrets)); - var buildConfigurationSecrets = new - { - SampleProp = $"Hello {config.BuildConfiguration}" - }; - File.WriteAllText(Path.Combine(config.ProjectDirectory, $"appsettings.{config.BuildConfiguration}.json"), JsonSerializer.Serialize(buildConfigurationSecrets)); - - var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); - - Assert.True(mergedSecrets.ContainsKey("SampleProp")); - Assert.Equal(buildConfigurationSecrets.SampleProp, mergedSecrets["SampleProp"]); - } - - [Fact] - public void GetsValuesFromConfigurationEnvironment() - { - var config = GetConfiguration(); - var settingsConfig = new SettingsConfig - { - Properties = - [ - new ValueConfig - { - Name = "SampleProp", - PropertyType = PropertyType.String - } - ] - }; - config.Configuration.Environment.Defaults.Add("SampleProp", "Hello Tests"); - config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); - - var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); - - Assert.True(mergedSecrets.ContainsKey("SampleProp")); - Assert.Equal(config.Configuration.Environment.Defaults["SampleProp"], mergedSecrets["SampleProp"]); - } - - [Fact] - public void GetsValuesForBuildConfigurationFromConfigurationEnvironment() - { - var config = GetConfiguration(); - var settingsConfig = new SettingsConfig - { - Properties = - [ - new ValueConfig - { - Name = "SampleProp", - PropertyType = PropertyType.String - } - ] - }; - config.Configuration.Environment.Defaults.Add("SampleProp", "Hello Tests"); - config.Configuration.Environment.Configuration[config.BuildConfiguration] = new Dictionary - { - { "SampleProp", "Hello Override" } - }; - config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); - - var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); - - Assert.True(mergedSecrets.ContainsKey("SampleProp")); - Assert.Equal(config.Configuration.Environment.Configuration[config.BuildConfiguration]["SampleProp"], mergedSecrets["SampleProp"]); - } - - [Fact] - public void GetsValuesFromHostEnvironment() - { - var config = GetConfiguration(); - var settingsConfig = new SettingsConfig - { - Properties = - [ - new ValueConfig - { - Name = "SampleProp1", - PropertyType = PropertyType.String - } - ] - }; - config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); - Environment.SetEnvironmentVariable("SampleProp1", nameof(GetsValuesFromHostEnvironment), EnvironmentVariableTarget.Process); - - var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); - - Assert.True(mergedSecrets.ContainsKey("SampleProp1")); - Assert.Equal(nameof(GetsValuesFromHostEnvironment), mergedSecrets["SampleProp1"]); - } - - - - [Fact] - public void OverridesConfigEnvironmentFromHostEnvironment() - { - var config = GetConfiguration(); - var settingsConfig = new SettingsConfig - { - Properties = - [ - new ValueConfig - { - Name = "SampleProp3", - PropertyType = PropertyType.String - } - ] - }; - config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); - config.Configuration.Environment.Defaults["SampleProp3"] = "Hello Config Environment"; - - Environment.SetEnvironmentVariable("SampleProp3", nameof(OverridesConfigEnvironmentFromHostEnvironment), EnvironmentVariableTarget.Process); - - var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); - - Assert.True(mergedSecrets.ContainsKey("SampleProp3")); - Assert.Equal(nameof(OverridesConfigEnvironmentFromHostEnvironment), mergedSecrets["SampleProp3"]); - } - - [Theory] - [InlineData("secrets.json")] - [InlineData("appsettings.json")] - public void OverridesConfigEnvironmentFromJson(string filename) - { - var config = GetConfiguration($"{nameof(OverridesConfigEnvironmentFromJson)}-{Path.GetFileNameWithoutExtension(filename)}"); - var settingsConfig = new SettingsConfig - { - Properties = - [ - new ValueConfig - { - Name = "SampleProp", - PropertyType = PropertyType.String - } - ] - }; - config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); - config.Configuration.Environment.Defaults["SampleProp"] = "Hello Config Environment"; - var secrets = new - { - SampleProp = "Hello Tests" - }; - File.WriteAllText(Path.Combine(config.ProjectDirectory, filename), JsonSerializer.Serialize(secrets)); - - var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); - - Assert.True(mergedSecrets.ContainsKey("SampleProp")); - Assert.Equal(secrets.SampleProp, mergedSecrets["SampleProp"]); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable("SampleProp", null, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("SampleProp1", null, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("SampleProp2", null, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("SampleProp3", null, EnvironmentVariableTarget.Process); - } - } -} +using System.Text.Json; +using Mobile.BuildTools.Models.Settings; +using Mobile.BuildTools.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace Mobile.BuildTools.Tests.Fixtures.Utils +{ + public class EnvironmentAnalyzerFixture(ITestOutputHelper testOutputHelper) : FixtureBase(null, testOutputHelper), IDisposable + { + [Theory] + [InlineData("secrets.json")] + [InlineData("appsettings.json")] + public void GetsValuesFromJson(string filename) + { + var config = GetConfiguration($"{nameof(GetsValuesFromJson)}-{Path.GetFileNameWithoutExtension(filename)}"); + var settingsConfig = new SettingsConfig + { + Properties = + [ + new ValueConfig + { + Name = "SampleProp", + PropertyType = PropertyType.String + } + ] + }; + config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); + + var secrets = new + { + SampleProp = "Hello Tests" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, filename), JsonSerializer.Serialize(secrets)); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp")); + Assert.Equal(secrets.SampleProp, mergedSecrets["SampleProp"]); + } + + [Fact] + public void GetsValueFromBuildConfigurationOverrideJson() + { + var config = GetConfiguration(); + config.BuildConfiguration = "Debug"; + var settingsConfig = new SettingsConfig + { + Properties = + [ + new ValueConfig + { + Name = "SampleProp", + PropertyType = PropertyType.String + } + ] + }; + + config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); + + var secrets = new + { + SampleProp = "Hello Tests" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, "appsettings.json"), JsonSerializer.Serialize(secrets)); + var debugBuildSecrets = new + { + SampleProp = $"Hello Debug" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, $"appsettings.Debug.json"), JsonSerializer.Serialize(debugBuildSecrets)); + var releaseBuildSecrets = new + { + SampleProp = $"Hello Release" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, $"appsettings.Release.json"), JsonSerializer.Serialize(releaseBuildSecrets)); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp")); + Assert.Equal(debugBuildSecrets.SampleProp, mergedSecrets["SampleProp"]); + } + + [Theory] + [InlineData("DebugQA", "Debug")] + [InlineData("DebugQA", "QA")] + public void GetsValueFromBuildFuzzyConfigurationOverrideJson(string buildConfiguration, string fuzzyConfiguration) + { + var config = GetConfiguration(); + config.BuildConfiguration = buildConfiguration; + config.Configuration.Environment ??= new BuildTools.Models.EnvironmentSettings(); + config.Configuration.Environment.EnableFuzzyMatching = true; + + var secrets = new + { + SampleProp = "Hello Tests" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, "appsettings.json"), JsonSerializer.Serialize(secrets)); + var debugBuildSecrets = new + { + SampleProp = $"Hello {fuzzyConfiguration}" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, $"appsettings.{fuzzyConfiguration}.json"), JsonSerializer.Serialize(debugBuildSecrets)); + var releaseBuildSecrets = new + { + SampleProp = "Hello Release" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, $"appsettings.Release.json"), JsonSerializer.Serialize(releaseBuildSecrets)); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp")); + Assert.Equal(debugBuildSecrets.SampleProp, mergedSecrets["SampleProp"]); + } + + [Fact] + public void GetsValuesFromConfigurationEnvironment() + { + var config = GetConfiguration(); + var settingsConfig = new SettingsConfig + { + Properties = + [ + new ValueConfig + { + Name = "SampleProp", + PropertyType = PropertyType.String + } + ] + }; + config.Configuration.Environment.Defaults.Add("SampleProp", "Hello Tests"); + config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp")); + Assert.Equal(config.Configuration.Environment.Defaults["SampleProp"], mergedSecrets["SampleProp"]); + } + + [Fact] + public void GetsValueFromProcessEnvironment() + { + var expectedValue = Guid.NewGuid().ToString(); + var key = nameof(GetsValueFromProcessEnvironment); + Environment.SetEnvironmentVariable(key, expectedValue, EnvironmentVariableTarget.Process); + + var config = GetConfiguration(); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.TryGetValue(key, out var actualValue)); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void GetsValuesForBuildConfigurationFromConfigurationEnvironment() + { + var config = GetConfiguration(); + var settingsConfig = new SettingsConfig + { + Properties = + [ + new ValueConfig + { + Name = "SampleProp", + PropertyType = PropertyType.String + } + ] + }; + config.Configuration.Environment.Defaults.Add("SampleProp", "Hello Tests"); + config.Configuration.Environment.Configuration[config.BuildConfiguration] = new Dictionary + { + { "SampleProp", "Hello Override" } + }; + config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp")); + Assert.Equal(config.Configuration.Environment.Configuration[config.BuildConfiguration]["SampleProp"], mergedSecrets["SampleProp"]); + } + + [Fact] + public void GetsValuesFromHostEnvironment() + { + var config = GetConfiguration(); + var settingsConfig = new SettingsConfig + { + Properties = + [ + new ValueConfig + { + Name = "SampleProp1", + PropertyType = PropertyType.String + } + ] + }; + config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); + Environment.SetEnvironmentVariable("SampleProp1", nameof(GetsValuesFromHostEnvironment), EnvironmentVariableTarget.Process); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp1")); + Assert.Equal(nameof(GetsValuesFromHostEnvironment), mergedSecrets["SampleProp1"]); + } + + [Fact] + public void OverridesConfigEnvironmentFromHostEnvironment() + { + var config = GetConfiguration(); + var settingsConfig = new SettingsConfig + { + Properties = + [ + new ValueConfig + { + Name = "SampleProp3", + PropertyType = PropertyType.String + } + ] + }; + config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); + config.Configuration.Environment.Defaults["SampleProp3"] = "Hello Config Environment"; + + Environment.SetEnvironmentVariable("SampleProp3", nameof(OverridesConfigEnvironmentFromHostEnvironment), EnvironmentVariableTarget.Process); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp3")); + Assert.Equal(nameof(OverridesConfigEnvironmentFromHostEnvironment), mergedSecrets["SampleProp3"]); + } + + [Theory] + [InlineData("secrets.json")] + [InlineData("appsettings.json")] + public void OverridesConfigEnvironmentFromJson(string filename) + { + var config = GetConfiguration($"{nameof(OverridesConfigEnvironmentFromJson)}-{Path.GetFileNameWithoutExtension(filename)}"); + var settingsConfig = new SettingsConfig + { + Properties = + [ + new ValueConfig + { + Name = "SampleProp", + PropertyType = PropertyType.String + } + ] + }; + config.Configuration.AppSettings[config.ProjectName] = new List([settingsConfig]); + config.Configuration.Environment.Defaults["SampleProp"] = "Hello Config Environment"; + var secrets = new + { + SampleProp = "Hello Tests" + }; + File.WriteAllText(Path.Combine(config.ProjectDirectory, filename), JsonSerializer.Serialize(secrets)); + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + + Assert.True(mergedSecrets.ContainsKey("SampleProp")); + Assert.Equal(secrets.SampleProp, mergedSecrets["SampleProp"]); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("SampleProp", null, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("SampleProp1", null, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("SampleProp2", null, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("SampleProp3", null, EnvironmentVariableTarget.Process); + } + + [Theory] + [InlineData("Debug", Platform.Unsupported, "Debug", false)] + [InlineData("DebugApk", Platform.Unsupported, "Debug", true)] + [InlineData("QA", Platform.Unsupported, "QA", false)] + [InlineData("Release", Platform.Unsupported, "Release", false)] + [InlineData("Debug", Platform.Android, "Android_Debug", false)] + [InlineData("Debug", Platform.iOS, "iOS_Debug", false)] + [InlineData("Stage", Platform.Android, "Android", false)] + public void GetsValuesFromBuildConfiguration(string buildConfiguration, Platform platform, string expectedEnvironment, bool fuzzyMatching) + { + var config = GetConfiguration($"{nameof(GetsValuesFromBuildConfiguration)}-{buildConfiguration}"); + config.BuildConfiguration = buildConfiguration; + config.Platform = platform; + config.Configuration.Environment.EnableFuzzyMatching = fuzzyMatching; + const string key = "EnvironmentProp"; + + config.Configuration.Environment.Configuration["Debug"] = new Dictionary + { + { key, "Hello Debug" } + }; + config.Configuration.Environment.Configuration["Android_Debug"] = new Dictionary + { + { key, "Hello Android Debug" } + }; + config.Configuration.Environment.Configuration["iOS_Debug"] = new Dictionary + { + { key, "Hello iOS Debug" } + }; + config.Configuration.Environment.Configuration["QA"] = new Dictionary + { + { key, "Hello QA" } + }; + config.Configuration.Environment.Configuration["Release"] = new Dictionary + { + { key, "Hello Release" } + }; + config.Configuration.Environment.Configuration["Android"] = new Dictionary + { + { key, "Hello Android" } + }; + config.Configuration.Environment.Configuration["Stage"] = new Dictionary + { + { key, "Hello Stage" } + }; + var expectedValue = config.Configuration.Environment.Configuration[expectedEnvironment][key]; + + var mergedSecrets = EnvironmentAnalyzer.GatherEnvironmentVariables(config); + Assert.True(mergedSecrets.ContainsKey(key)); + Assert.Equal(expectedValue, mergedSecrets[key]); + } + } +}