From 8a7bef9f2daccc7a13693cf449b94815c27b3bfc Mon Sep 17 00:00:00 2001 From: Christian Plesner Hansen Date: Thu, 19 Jun 2025 13:20:20 +0200 Subject: [PATCH 1/3] Have includes occur in the correct order --- src/Hocon/HoconParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Hocon/HoconParser.cs b/src/Hocon/HoconParser.cs index 1edb46fb..be6d7488 100644 --- a/src/Hocon/HoconParser.cs +++ b/src/Hocon/HoconParser.cs @@ -469,6 +469,7 @@ private void ParseObject(ref HoconValue owner) $"found `{_tokens.Current.Type}` instead."); owner.ReParent(ParseInclude()); + hoconObject = owner.GetObject(); valueWasParsed = true; break; From 26f76ba0863e3c9ad01cf60865429b1e4d127736 Mon Sep 17 00:00:00 2001 From: Christian Plesner Hansen Date: Fri, 1 Aug 2025 11:17:14 +0200 Subject: [PATCH 2/3] Include context information with includes --- .../Hocon.Configuration.csproj | 3 +- .../HoconConfigurationFactory.cs | 15 ++- src/Hocon/Hocon.csproj | 1 + src/Hocon/HoconParser.cs | 91 +++++++++++++++++-- 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/Hocon.Configuration/Hocon.Configuration.csproj b/src/Hocon.Configuration/Hocon.Configuration.csproj index b661b867..f809f1d7 100644 --- a/src/Hocon.Configuration/Hocon.Configuration.csproj +++ b/src/Hocon.Configuration/Hocon.Configuration.csproj @@ -5,6 +5,7 @@ Hocon $(HoconPackageTags) HOCON (Human-Optimized Config Object Notation) parser and application-ready implementation. + $(NoWarn);NU1506 @@ -14,7 +15,7 @@ - + diff --git a/src/Hocon.Configuration/HoconConfigurationFactory.cs b/src/Hocon.Configuration/HoconConfigurationFactory.cs index 697adfa0..4ce6a589 100644 --- a/src/Hocon.Configuration/HoconConfigurationFactory.cs +++ b/src/Hocon.Configuration/HoconConfigurationFactory.cs @@ -41,6 +41,19 @@ public static Config ParseString(string hocon, HoconIncludeCallbackAsync include return new Config(res); } + /// + /// Generates a configuration defined in the supplied + /// HOCON (Human-Optimized Config Object Notation) string. + /// + /// A document that contains configuration options to use. + /// callback used to resolve includes + /// The configuration defined in the supplied HOCON string. + public static Config ParseDocument(HoconDocument document, HoconIncludeDocumentCallbackAsync includeCallback) + { + HoconRoot res = HoconParser.Parse(document, includeCallback); + return new Config(res); + } + /// /// Generates a configuration defined in the supplied /// HOCON (Human-Optimized Config Object Notation) string. @@ -99,7 +112,7 @@ public static Config FromFile(string filePath) foreach(var extension in DefaultHoconFileExtensions) { var path = $"{filePath}.{extension}"; - if (File.Exists(path)) + if (File.Exists(path)) return ParseString(File.ReadAllText(path)); } } else diff --git a/src/Hocon/Hocon.csproj b/src/Hocon/Hocon.csproj index c0a1d14b..8b72f660 100644 --- a/src/Hocon/Hocon.csproj +++ b/src/Hocon/Hocon.csproj @@ -3,5 +3,6 @@ $(NetFrameworkVersion);$(NetStandardLibVersion) true HOCON (Human-Optimized Config Object Notation) core API implementation. For full access inside your application, install the Hocon.Configuration package. + $(NoWarn);NU1506 \ No newline at end of file diff --git a/src/Hocon/HoconParser.cs b/src/Hocon/HoconParser.cs index be6d7488..7147bfff 100644 --- a/src/Hocon/HoconParser.cs +++ b/src/Hocon/HoconParser.cs @@ -12,8 +12,63 @@ namespace Hocon { + + /// + /// The source of a hocon document along with optionally a source that + /// indicates where the document came from. + /// + public class HoconDocument + { + private readonly string text; + private readonly HoconDocumentSource source; + + public HoconDocument(string text, HoconDocumentSource source) + { + this.text = text; + this.source = source; + } + + public string Text => text; + + public HoconDocumentSource Source => source; + + public static HoconDocument FromFile(string path, string contents) => + new( + text: contents, + source: new HoconDocumentSource(HoconCallbackType.File, path) + ); + + public static HoconDocument FromString(string contents) => + new(text: contents, source: null); + + } + + /// + /// A description of where a hocon document came from or, in the case of + /// includes, where to load it from. + /// + public class HoconDocumentSource + { + private readonly HoconCallbackType type; + private readonly string what; + + public HoconDocumentSource(HoconCallbackType type, string what) + { + this.type = type; + this.what = what; + } + + public HoconCallbackType Type => type; + + public string What => what; + } + public delegate Task HoconIncludeCallbackAsync(HoconCallbackType callbackType, string value); + public delegate Task HoconIncludeDocumentCallbackAsync( + HoconDocumentSource include, + HoconDocument document); + /// /// This class contains methods used to parse HOCON (Human-Optimized Config Object Notation) /// configuration strings. @@ -21,7 +76,8 @@ namespace Hocon public sealed class HoconParser { private readonly List _substitutions = new List(); - private HoconIncludeCallbackAsync _includeCallback = (type, value) => Task.FromResult("{}"); + private HoconIncludeDocumentCallbackAsync _includeCallback = (i, p) => Task.FromResult("{}"); + private HoconDocument _document = default; private HoconValue _root; private HoconTokenizerResult _tokens; @@ -31,20 +87,37 @@ public sealed class HoconParser /// /// Parses the supplied HOCON configuration string into a root element. /// - /// The string that contains a HOCON configuration string. + /// The document that contains a HOCON configuration string. /// Callback used to resolve includes /// The root element created from the supplied HOCON configuration string. /// /// This exception is thrown when an unresolved substitution is encountered. /// It also occurs when any error is encountered while tokenizing or parsing the configuration string. /// - public static HoconRoot Parse(string text, HoconIncludeCallbackAsync includeCallback = null) + public static HoconRoot Parse(HoconDocument doc, HoconIncludeDocumentCallbackAsync includeCallback = null) { - return new HoconParser().ParseText(text, true, includeCallback).Normalize(); + return new HoconParser().ParseText(doc, true, includeCallback).Normalize(); } - private HoconRoot ParseText(string text, bool resolveSubstitutions, HoconIncludeCallbackAsync includeCallback) + /// + /// Parses the supplied HOCON configuration string into a root element. + /// + /// The string that contains a HOCON configuration string. + /// Callback used to resolve includes + /// The root element created from the supplied HOCON configuration string. + /// + /// This exception is thrown when an unresolved substitution is encountered. + /// It also occurs when any error is encountered while tokenizing or parsing the configuration string. + /// + public static HoconRoot Parse(string text, HoconIncludeCallbackAsync includeCallback = null) + => Parse(new HoconDocument(text, null), OldToNewCallback(includeCallback)); + + private static HoconIncludeDocumentCallbackAsync OldToNewCallback(HoconIncludeCallbackAsync old) => + (old is null) ? null : (inc, _) => old.Invoke(inc.Type, inc.What); + + private HoconRoot ParseText(HoconDocument doc, bool resolveSubstitutions, HoconIncludeDocumentCallbackAsync includeCallback) { + string text = doc.Text; if (string.IsNullOrWhiteSpace(text)) throw new HoconParserException( $"Parameter {nameof(text)} is null or empty.\n" + @@ -52,6 +125,7 @@ private HoconRoot ParseText(string text, bool resolveSubstitutions, HoconInclude if (includeCallback != null) _includeCallback = includeCallback; + _document = doc; try { @@ -409,7 +483,9 @@ private HoconValue ParseInclude() // Consume the last token _tokens.ToNextSignificant(); - var includeHocon = _includeCallback(callbackType, fileName).ConfigureAwait(false).GetAwaiter().GetResult(); + var include = new HoconDocumentSource(callbackType, fileName); + + var includeHocon = _includeCallback(include, _document).ConfigureAwait(false).GetAwaiter().GetResult(); if (string.IsNullOrWhiteSpace(includeHocon)) { @@ -419,7 +495,8 @@ private HoconValue ParseInclude() return new HoconEmptyValue(null); } - var includeRoot = new HoconParser().ParseText(includeHocon, false, _includeCallback); + var includeDoc = new HoconDocument(includeHocon, include); + var includeRoot = new HoconParser().ParseText(includeDoc, false, _includeCallback); /* if (owner != null && owner.Type != HoconType.Empty && owner.Type != includeRoot.Value.Type) throw HoconParserException.Create(includeToken, Path, From 4a8a354f9695dc0a2c9d0c372d448f990e4d891d Mon Sep 17 00:00:00 2001 From: Christian Plesner Hansen Date: Tue, 19 Aug 2025 10:54:54 +0200 Subject: [PATCH 3/3] Add support for custom env resolution and default env values --- .../HoconConfigurationFactory.cs | 4 +- src/Hocon/HoconParser.cs | 42 ++++++++++++++----- src/Hocon/Impl/HoconSubstitution.cs | 7 +++- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/Hocon.Configuration/HoconConfigurationFactory.cs b/src/Hocon.Configuration/HoconConfigurationFactory.cs index 4ce6a589..27d2de46 100644 --- a/src/Hocon.Configuration/HoconConfigurationFactory.cs +++ b/src/Hocon.Configuration/HoconConfigurationFactory.cs @@ -48,9 +48,9 @@ public static Config ParseString(string hocon, HoconIncludeCallbackAsync include /// A document that contains configuration options to use. /// callback used to resolve includes /// The configuration defined in the supplied HOCON string. - public static Config ParseDocument(HoconDocument document, HoconIncludeDocumentCallbackAsync includeCallback) + public static Config ParseDocument(HoconDocument document, HoconIncludeDocumentCallbackAsync includeCallback, HoconEnvironmentGetCallback envGetCallback = null) { - HoconRoot res = HoconParser.Parse(document, includeCallback); + HoconRoot res = HoconParser.Parse(document, includeCallback, envGetCallback); return new Config(res); } diff --git a/src/Hocon/HoconParser.cs b/src/Hocon/HoconParser.cs index 7147bfff..a30fdcfe 100644 --- a/src/Hocon/HoconParser.cs +++ b/src/Hocon/HoconParser.cs @@ -69,6 +69,8 @@ public delegate Task HoconIncludeDocumentCallbackAsync( HoconDocumentSource include, HoconDocument document); + public delegate string HoconEnvironmentGetCallback(string key, string defaultValue); + /// /// This class contains methods used to parse HOCON (Human-Optimized Config Object Notation) /// configuration strings. @@ -77,6 +79,7 @@ public sealed class HoconParser { private readonly List _substitutions = new List(); private HoconIncludeDocumentCallbackAsync _includeCallback = (i, p) => Task.FromResult("{}"); + private HoconEnvironmentGetCallback _envGetCallback = (k, d) => Environment.GetEnvironmentVariable(k); private HoconDocument _document = default; private HoconValue _root; @@ -94,9 +97,9 @@ public sealed class HoconParser /// This exception is thrown when an unresolved substitution is encountered. /// It also occurs when any error is encountered while tokenizing or parsing the configuration string. /// - public static HoconRoot Parse(HoconDocument doc, HoconIncludeDocumentCallbackAsync includeCallback = null) + public static HoconRoot Parse(HoconDocument doc, HoconIncludeDocumentCallbackAsync includeCallback = null, HoconEnvironmentGetCallback envGetCallback = null) { - return new HoconParser().ParseText(doc, true, includeCallback).Normalize(); + return new HoconParser().ParseText(doc, true, includeCallback, envGetCallback).Normalize(); } /// @@ -115,7 +118,11 @@ public static HoconRoot Parse(string text, HoconIncludeCallbackAsync includeCall private static HoconIncludeDocumentCallbackAsync OldToNewCallback(HoconIncludeCallbackAsync old) => (old is null) ? null : (inc, _) => old.Invoke(inc.Type, inc.What); - private HoconRoot ParseText(HoconDocument doc, bool resolveSubstitutions, HoconIncludeDocumentCallbackAsync includeCallback) + private HoconRoot ParseText( + HoconDocument doc, + bool resolveSubstitutions, + HoconIncludeDocumentCallbackAsync includeCallback, + HoconEnvironmentGetCallback envGetCallback) { string text = doc.Text; if (string.IsNullOrWhiteSpace(text)) @@ -125,6 +132,8 @@ private HoconRoot ParseText(HoconDocument doc, bool resolveSubstitutions, HoconI if (includeCallback != null) _includeCallback = includeCallback; + if (envGetCallback != null) + _envGetCallback = envGetCallback; _document = doc; try @@ -179,7 +188,7 @@ private void ResolveSubstitutions() string envValue = null; try { - envValue = Environment.GetEnvironmentVariable(sub.Path.Value); + envValue = _envGetCallback(sub.Path.Value, sub.DefaultValue); } catch (Exception) { @@ -259,7 +268,7 @@ private HoconValue ResolveSubstitution(HoconSubstitution sub) private bool IsValueCyclic(HoconField field, HoconSubstitution sub) { var pendingValues = new Stack(); - var visitedFields = new List {field}; + var visitedFields = new List { field }; var pendingSubs = new Stack(); pendingSubs.Push(sub); @@ -496,7 +505,7 @@ private HoconValue ParseInclude() } var includeDoc = new HoconDocument(includeHocon, include); - var includeRoot = new HoconParser().ParseText(includeDoc, false, _includeCallback); + var includeRoot = new HoconParser().ParseText(includeDoc, false, _includeCallback, _envGetCallback); /* if (owner != null && owner.Type != HoconType.Empty && owner.Type != includeRoot.Value.Type) throw HoconParserException.Create(includeToken, Path, @@ -768,9 +777,22 @@ private HoconValue ParseValue(IHoconElement owner) if (value == null) value = new HoconValue(owner); - var pointerPath = HoconPath.Parse(_tokens.Current.Value); + HoconPath pointerPath; + string defaultValue; + string refStr = _tokens.Current.Value; + var colonIndex = refStr.IndexOf(":-"); + if (colonIndex == -1) + { + pointerPath = HoconPath.Parse(refStr); + defaultValue = null; + } + else + { + pointerPath = HoconPath.Parse(refStr.Substring(0, colonIndex)); + defaultValue = refStr.Substring(colonIndex + 2); + } var sub = new HoconSubstitution(value, pointerPath, _tokens.Current, - _tokens.Current.Type == TokenType.SubstituteRequired); + _tokens.Current.Type == TokenType.SubstituteRequired, defaultValue); _substitutions.Add(sub); _tokens.Next(); value.Add(sub); @@ -780,7 +802,7 @@ private HoconValue ParseValue(IHoconElement owner) if (value == null) value = new HoconValue(owner); - var subAssign = new HoconSubstitution(value, new HoconPath(Path), _tokens.Current, false); + var subAssign = new HoconSubstitution(value, new HoconPath(Path), _tokens.Current, false, null); _substitutions.Add(subAssign); value.Add(subAssign); value.Add(ParsePlusEqualAssignArray(value)); @@ -851,7 +873,7 @@ private HoconArray ParsePlusEqualAssignArray(IHoconElement owner) "Invalid Hocon include. Hocon config substitution type must be the same as the field it's merged into. " + $"Expected type: `{currentArray.Type}`, type returned by include callback: `{includeValue.Type}`"); - currentArray.Add((HoconValue) includeValue.Clone(currentArray)); + currentArray.Add((HoconValue)includeValue.Clone(currentArray)); break; case TokenType.StartOfArray: diff --git a/src/Hocon/Impl/HoconSubstitution.cs b/src/Hocon/Impl/HoconSubstitution.cs index 88a01085..d41cdbe7 100644 --- a/src/Hocon/Impl/HoconSubstitution.cs +++ b/src/Hocon/Impl/HoconSubstitution.cs @@ -35,7 +35,7 @@ public sealed class HoconSubstitution : IHoconElement, IHoconLineInfo /// Marks wether this substitution uses the ${? notation or not. /// /// /// The of this substitution, used for exception generation purposes. - internal HoconSubstitution(IHoconElement parent, HoconPath path, IHoconLineInfo lineInfo, bool required) + internal HoconSubstitution(IHoconElement parent, HoconPath path, IHoconLineInfo lineInfo, bool required, string defaultValue) { if (parent == null) throw new ArgumentNullException(nameof(parent), "HoconSubstitution parent can not be null."); @@ -48,7 +48,8 @@ internal HoconSubstitution(IHoconElement parent, HoconPath path, IHoconLineInfo LineNumber = lineInfo.LineNumber; Required = required; Path = path; - + DefaultValue = defaultValue; + _parentsToResolveFor.Add(Parent as HoconValue); } @@ -72,6 +73,8 @@ internal HoconField ParentField /// public HoconPath Path { get; } + public string DefaultValue { get; } + /// /// The evaluated value from the Path property ///