// Copyright Epic Games, Inc. All Rights Reserved.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
namespace HordeServer.Configuration
{
using JsonObject = System.Text.Json.Nodes.JsonObject;
///
/// Context for reading a tree of config files
///
[DebuggerDisplay("{CurrentFile}")]
public class ConfigContext
{
///
/// Options for serializing config files
///
public JsonSerializerOptions JsonOptions { get; }
///
/// Stack of included files
///
public Stack IncludeStack { get; } = new Stack();
///
/// Stack of properties
///
public Stack ScopeStack { get; } = new Stack();
///
/// Map of property path to the file declaring a value for it
///
public Dictionary PropertyPathToFile { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase);
///
/// Sources to read config files from
///
public IReadOnlyDictionary Sources { get; }
///
/// Map of macro name to value
///
public List> MacroScopes { get; } = new List>();
///
/// Tracks files read as part of the configuration
///
public Dictionary Files { get; } = new Dictionary();
///
/// Logger for config messages
///
public ILogger Logger { get; }
///
/// Uri of the current file
///
public Uri CurrentFile => (IncludeStack.Count > 0) ? IncludeStack.Peek().Uri : null!;
///
/// Current property scope
///
public string CurrentScope => ScopeStack.Peek();
readonly Func _getMacroValue;
///
/// Constructor
///
public ConfigContext(JsonSerializerOptions jsonOptions, IReadOnlyDictionary sources, ILogger logger)
{
JsonOptions = jsonOptions;
Sources = sources;
ScopeStack.Push("$");
Logger = logger;
_getMacroValue = GetMacroValue;
}
///
/// Marks a property as defined in the current file
///
///
public void AddProperty(string name)
{
if (!TryAddProperty(name, out Uri? otherFile))
{
throw new ConfigException(this, $"Property {CurrentScope}.{name} was already defined in {otherFile}.");
}
}
///
/// Marks a property as defined in the current file
///
/// Name of the property within the current scope
/// If the property is not added, the file that previously defined it
public bool TryAddProperty(string name, [NotNullWhen(false)] out Uri? otherFile)
{
string propertyPath = $"{CurrentScope}.{name}";
Uri currentFile = CurrentFile;
if (PropertyPathToFile.TryAdd(propertyPath, currentFile))
{
otherFile = null;
return true;
}
else
{
otherFile = PropertyPathToFile[propertyPath];
return false;
}
}
///
/// Pushes a scope to the property stack
///
///
public void EnterScope(string name)
{
ScopeStack.Push($"{CurrentScope}.{name}");
}
///
/// Pops a scope from the property stack
///
public void LeaveScope()
{
ScopeStack.Pop();
}
///
/// Expand macros in a string property
///
public string ExpandMacros(string text) => StringUtils.ExpandProperties(text, _getMacroValue);
///
/// Gets the value of a named macro
///
string? GetMacroValue(string name)
{
for (int idx = MacroScopes.Count - 1; idx >= 0; idx--)
{
if (MacroScopes[idx].TryGetValue(name, out string? value))
{
return value;
}
}
return null;
}
///
/// Reads the contents of a file using the appropriate source
///
/// Uri of the file to read. The scheme indicates the source to read from.
/// Cancellation token for the operation
/// Information about the file
public async ValueTask ReadFileAsync(Uri uri, CancellationToken cancellationToken)
{
IConfigSource? source = Sources[uri.Scheme];
if (source == null)
{
throw new ConfigException(this, $"Invalid/unknown scheme for config file {uri}");
}
IConfigFile? file;
if (!Files.TryGetValue(uri, out file))
{
file = await source.GetAsync(uri, cancellationToken);
Files.Add(uri, file);
}
return file;
}
///
/// Parses a config file as a json object
///
/// File to parse
/// Cancellation token for the operation
/// The parsed file
public async ValueTask ParseFileAsync(IConfigFile file, CancellationToken cancellationToken = default)
{
ReadOnlyMemory data = await file.ReadAsync(cancellationToken);
JsonObject? obj = JsonSerializer.Deserialize(data.Span, JsonOptions);
if (obj == null)
{
throw new ConfigException(this, $"Config file {file.Uri} contains a null object.");
}
return obj;
}
///
/// Reads an object from a particular URL
///
/// Location of the file to read
/// Root node describing the preprocessor fields
/// Cancellation token for the operation
///
public async Task PreprocessFileAsync(Uri uri, ObjectConfigNode rootNode, CancellationToken cancellationToken)
{
IConfigFile file = await ReadFileAsync(uri, cancellationToken);
JsonObject obj = await ParseFileAsync(file, cancellationToken);
IncludeStack.Push(file);
obj = await rootNode.PreprocessAsync(obj, this, cancellationToken);
IncludeStack.Pop();
return obj;
}
///
/// Reads an object from a particular URL
///
/// Type of object to read
/// Location of the file to read
/// Cancellation token for the operation
///
public async Task ReadAsync(Uri uri, CancellationToken cancellationToken) where T : class, new()
{
ObjectConfigNode type = new ObjectConfigNode(typeof(T));
JsonObject obj = await PreprocessFileAsync(uri, type, cancellationToken);
return JsonSerializer.Deserialize(obj, JsonOptions) ?? new T();
}
}
}