// 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(); } } }