Skip to content

Commit

Permalink
support nested variables on non-primitive JsonNodes (#161)
Browse files Browse the repository at this point in the history
Variables can now contain other variables in objects and arrays

**Sample:**

appsettings.json
```json
{
  "foo": "$test:variable"
}
```

variables.json
```json
{
  "variable": ["foo", "bar", "$test:someVariable"],
  "someVariable": "baz"
}
```

will result in 

```json
{
  "foo": ["foo", "bar", "baz"]
}
```

this would also work for nested objects
  • Loading branch information
RohrerF authored Dec 4, 2023
1 parent 4668e2e commit 463f8d6
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 128 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Confix.Variables;

public sealed class CircularVariableReferenceException : Exception
{
public CircularVariableReferenceException(VariablePath path)
: base($"Variable {path} could not be resolved, due to circular references")
{
Path = path;
}

public VariablePath Path { get; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text.Json;
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using Confix.Tool.Commands.Logging;
using Json.Schema;
Expand All @@ -20,21 +20,68 @@ public VariableReplacerService(IVariableResolver variableResolver)
{
return null;
}
var variables = GetVariables(node).ToArray();
App.Log.DetectedVariables(variables.Length);

var resolved = await _variableResolver.ResolveVariables(variables, cancellationToken);
return await RewriteAsync(node, ImmutableHashSet<VariablePath>.Empty, cancellationToken);
}

private async Task<JsonNode> RewriteAsync(
JsonNode node,
IReadOnlySet<VariablePath> resolvedPaths,
CancellationToken cancellationToken)
{
var resolvedVariables = await ResolveVariables(node, resolvedPaths, cancellationToken);

return new JsonVariableRewriter().Rewrite(node, new(resolved));
return new JsonVariableRewriter().Rewrite(node, new(resolvedVariables));
}

private static IEnumerable<VariablePath> GetVariables(JsonNode node)
=> JsonParser.ParseNode(node).Values
.Where(v => v.GetSchemaValueType() == SchemaValueType.String)
.SelectMany(v => v!.ToString().GetVariables());
{
if (node is JsonValue value)
{
return value.GetSchemaValueType() == SchemaValueType.String
? value.ToString().GetVariables()
: Enumerable.Empty<VariablePath>();
}

return JsonParser.ParseNode(node).Values
.Where(v => v.GetSchemaValueType() == SchemaValueType.String)
.SelectMany(v => v!.ToString().GetVariables());
}

private async Task<IReadOnlyDictionary<VariablePath, JsonNode>> ResolveVariables(
JsonNode node,
IReadOnlySet<VariablePath> resolvedPaths,
CancellationToken cancellationToken)
{
var variables = GetVariables(node).ToArray();
if (variables.Length == 0)
{
return ImmutableDictionary<VariablePath, JsonNode>.Empty;
}
App.Log.DetectedVariables(variables.Length);
if (resolvedPaths.Overlaps(variables))
{
throw new CircularVariableReferenceException(variables.First(v => resolvedPaths.Contains(v)));
}

var resolvedVariables = await _variableResolver.ResolveVariables(variables, cancellationToken);

var resolved = new Dictionary<VariablePath, JsonNode>();
foreach (var variable in resolvedVariables)
{
var currentPath = new HashSet<VariablePath>() { variable.Key };
currentPath.UnionWith(resolvedPaths);
resolved[variable.Key] = await RewriteAsync(
variable.Value,
currentPath,
cancellationToken);
}

return resolved;
}
}

file static class LogExtensionts
file static class LogExtensions
{
public static void DetectedVariables(this IConsoleLogger log, int count)
{
Expand Down
50 changes: 4 additions & 46 deletions src/Confix.Tool/src/Confix.Library/Variables/VariableResolver.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using System.Text.Json.Nodes;
using Confix.Tool;
using Confix.Tool.Commands.Logging;
using Confix.Tool.Middlewares;
using Json.Schema;

namespace Confix.Variables;

Expand Down Expand Up @@ -85,14 +83,8 @@ public async Task<JsonNode> ResolveVariable(
App.Log.ResolvingVariable(key);
var configuration = GetProviderConfiguration(key.ProviderName);
await using var provider = _variableProviderFactory.CreateProvider(configuration);
var resolvedValue = await provider.ResolveAsync(key.Path, cancellationToken);

if (resolvedValue.IsVariable(out VariablePath? variablePath))
{
return await ResolveVariable(variablePath.Value, cancellationToken);
}

return resolvedValue;

return await provider.ResolveAsync(key.Path, cancellationToken);
}

public async Task<IReadOnlyDictionary<VariablePath, JsonNode>> ResolveVariables(
Expand Down Expand Up @@ -120,30 +112,14 @@ private async Task<IReadOnlyDictionary<VariablePath, JsonNode>> ResolveVariables
{
App.Log.ResolvingVariables(providerName, paths.Count);
var resolvedVariables = new Dictionary<VariablePath, JsonNode>();
var nestedVariables = new Dictionary<VariablePath, string>();


var providerConfiguration = GetProviderConfiguration(providerName);
await using var provider = _variableProviderFactory.CreateProvider(providerConfiguration);

var resolvedValues = await provider.ResolveManyAsync(paths, cancellationToken);
foreach (var (key, value) in resolvedValues)
{
if (value.IsVariable(out var variablePath))
{
nestedVariables.Add(variablePath.Value, key);
}
else
{
resolvedVariables.Add(new(providerName, key), value);
}
}

var resolvedNestedVariables =
await ResolveVariables(nestedVariables.Keys.ToList(), cancellationToken);

foreach (var (key, value) in resolvedNestedVariables)
{
resolvedVariables.Add(new(providerName, nestedVariables[key]), value);
resolvedVariables.Add(new(providerName, key), value);
}

return resolvedVariables;
Expand All @@ -155,24 +131,6 @@ private VariableProviderConfiguration GetProviderConfiguration(string providerNa
$"No VariableProvider with name '{providerName.AsHighlighted()}' configured.");
}

public static class Extension
{
public static bool IsVariable(
this JsonNode node,
[NotNullWhen(true)] out VariablePath? variablePath)
{
if (node.GetSchemaValueType() == SchemaValueType.String
&& VariablePath.TryParse((string) node!, out var parsed))
{
variablePath = parsed;
return true;
}

variablePath = default;
return false;
}
}

file static class Log
{
public static void ResolvingVariable(this IConsoleLogger log, VariablePath key)
Expand Down
Loading

0 comments on commit 463f8d6

Please sign in to comment.