Skip to content

Commit 06f9c9e

Browse files
authored
Resolve hocon paths (#5644)
* WIP fix unresolved Paths in HOCON.md * Deleted section not supported * Fix linting * Remove unsupported hocn test specs * Removed duplicate test
1 parent c85cbf2 commit 06f9c9e

File tree

3 files changed

+44
-181
lines changed

3 files changed

+44
-181
lines changed

docs/articles/configuration/hocon.md

Lines changed: 4 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ Substitution processing is performed as the last parsing step, so a substitution
253253

254254
If a key has been specified more than once, the substitution will always evaluate to its latest-assigned value (that is, it will evaluate to the merged object, or the last non-object value that was set, in the entire document being parsed including all included files).
255255

256-
If a substitution does not match any value present in the configuration and is not resolved by an external source, then it is undefined. An undefined _required substitution_ is invalid and will generate an error.
256+
If a substitution does not match any value present in the configuration, then it is undefined. An undefined _required substitution_ is invalid and will generate an error.
257257

258-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=UnsolvableSubstitutionWillThrowSample)]
258+
[!code-csharp[SerializationSetupDocSpec](../../../src/core/Akka.Docs.Tests/Configuration/SerializationSetupDocSpec.cs?name=UnsolvableSubstitutionWillThrowSample)]
259259

260260
If an _optional substitution_ is undefined:
261261

@@ -271,168 +271,5 @@ A substitution is replaced with any value type (number, object, string, array, t
271271

272272
Examples:
273273

274-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=StringSubstitutionSample)]
275-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=ArraySubstitutionSample)]
276-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=ObjectMergeSubstitutionSample)]
277-
278-
#### Using Substitution to Access Environment Variables
279-
280-
For substitutions which are not found in the configuration tree, it will be resolved by looking at system environment variables.
281-
282-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=EnvironmentVariableSample)]
283-
284-
An application can explicitly block looking up a substitution in the environment by setting a value in the configuration, with the same name as the environment variable. You could set `HOME : null` in your root object to avoid expanding `${HOME}` from the environment, for example:
285-
286-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=BlockedEnvironmentVariableSample)]
287-
288-
It's recommended that HOCON keys always use lowercase, because environment variables generally are capitalized. This avoids naming collisions between environment variables and configuration properties. (While on Windows `Environment.GetEnvironmentVariable()` is generally not case-sensitive, the lookup will be case sensitive all the way until the env variable fallback lookup is reached).
289-
290-
Environment variables are interpreted as follows:
291-
292-
* Env variables set to the empty string are kept as such (set to empty string, rather than undefined)
293-
* If `Environment.GetEnvironmentVariable()` throws SecurityException, then it is treated as not present
294-
* Encoding is handled by C# (`Environment.GetEnvironmentVariable()` already returns a Unicode string)
295-
* Environment variables always become a string value, though if an app asks for another type automatic type conversion would kick in
296-
297-
##### Note on Windows and Case Sensitivity of Environment Variables
298-
299-
HOCON's lookup of environment variable values is always case sensitive, but Linux and Windows differ in their handling of case.
300-
301-
Linux allows one to define multiple environment variables with the same name but with different case; so both "PATH" and "Path" may be defined simultaneously. HOCON's access to these environment variables on Linux is straightforward; ie just make sure you define all your vars with the required case.
302-
303-
Windows is more confusing. Windows environment variables names may contain a mix of upper and lowercase characters, eg "Path", however Windows does not allow one to define multiple instances of the same name but differing in case.
304-
Whilst accessing env vars in Windows is case insensitive, accessing env vars in HOCON is case sensitive.
305-
306-
So if you know that you HOCON needs "PATH" then you must ensure that the variable is defined as "PATH" rather than some other name such as "Path" or "path".
307-
However, Windows does not allow us to change the case of an existing env var; we can't simply redefine the var with an upper case name.
308-
The only way to ensure that your environment variables have the desired case is to first undefine all the env vars that you will depend on then redefine them with the required case.
309-
310-
For example, the the ambient environment might have this definition ...
311-
312-
```cmd
313-
set Path=A;B;C
314-
```
315-
316-
... we just don't know. But if the HOCON needs "PATH", then the start script must take a precautionary approach and enforce the necessary case as follows ...
317-
318-
```cmd
319-
set OLDPATH=%PATH%
320-
set PATH=
321-
set PATH=%OLDPATH%
322-
```
323-
324-
You cannot know what ambient environment variables might exist in the ambient environment when your program is invoked, nor what case those definitions might have. Therefore the only safe thing to do is redefine all the vars you rely on as shown above.
325-
326-
#### Self-Referential Substitutions
327-
328-
The idea of self-referential substitution is to allow a new value for a field to be based on the older value.
329-
330-
```text
331-
path : "a:b:c"
332-
path : ${path}":d"
333-
```
334-
335-
is equal to:
336-
337-
```text
338-
path : "a:b:c:d"
339-
```
340-
341-
Examples of self-referential fields:
342-
343-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=SelfReferencingSubstitutionWithString)]
344-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=SelfReferencingSubstitutionWithArray)]
345-
346-
Note that an object or array with a substitution inside it is **not** considered self-referential for this purpose. The self-referential rules do **not** apply to:
347-
348-
* `a : { b : ${a} }`
349-
* `a : [${a}]`
350-
351-
These cases are unbreakable cycles that generate an error.
352-
353-
Examples:
354-
355-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=CircularReferenceSubstitutionError)]
356-
357-
#### The `+=` Field Separator
358-
359-
Fields may have `+=` as a separator rather than `:` or `=`. A field with `+=` transforms into a self-referential array
360-
concatenation, like this:
361-
362-
a += b
363-
364-
becomes:
365-
366-
a = ${?a} [b]
367-
368-
`+=` appends an element to a previous array. If the previous value was not an array, an error will result just as it would in the long form `a = ${?a} [b]`. Note that the previous value is optional (`${?a}` not `${a}`), which allows `a += b` to be the first mention of `a` in the file (it is not necessary to have `a = []` first).
369-
370-
Example:
371-
372-
[!code-csharp[ConfigurationSample](../../../src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs?name=PlusEqualOperatorSample)]
373-
374-
#### Examples of Self-Referential Substitutions
375-
376-
In isolation (with no merges involved), a self-referential field is an error because the substitution cannot be resolved:
377-
378-
foo : ${foo} // an error
379-
380-
When `foo : ${foo}` is merged with an earlier value for `foo`, however, the substitution can be resolved to that earlier value. When merging two objects, the self-reference in the overriding field refers to the overridden field. Say you have:
381-
382-
foo : { a : 1 }
383-
foo : ${foo}
384-
385-
Then `${foo}` resolves to `{ a : 1 }`, the value of the overridden field.
386-
387-
It would be an error if these two fields were reversed, so:
388-
389-
foo : ${foo}
390-
foo : { a : 1 }
391-
392-
Here the `${foo}` self-reference comes before `foo` has a value, so it is undefined, exactly as if the substitution referenced a path not found in the document.
393-
394-
Because `foo : ${foo}` conceptually looks to previous definitions of `foo` for a value, the optional substitution syntax `${?foo}` does not create a cycle:
395-
396-
foo : ${?foo} // this field just disappears silently
397-
398-
If a substitution is hidden by a value that could not be merged with it (by a non-object value) then it is never evaluated and no error will be reported. So for example:
399-
400-
foo : ${does-not-exist}
401-
foo : 42
402-
403-
In this case, no matter what `${does-not-exist}` resolves to, we know `foo` is `42`, so `${does-not-exist}` is never evaluated and there is no error. The same is true for cycles like `foo : ${foo}, foo : 42`, where the initial self-reference are simply ignored.
404-
405-
A self-reference resolves to the value "below" even if it's part of a path expression. So for example:
406-
407-
foo : { a : { c : 1 } }
408-
foo : ${foo.a}
409-
foo : { a : 2 }
410-
411-
Here, `${foo.a}` would refer to `{ c : 1 }` rather than `2` and so the final merge would be `{ a : 2, c : 1 }`.
412-
413-
Recall that for a field to be self-referential, it must have a substitution or value concatenation as its value. If a field has an object or array value, for example, then it is not self-referential even if there is a reference to the field itself inside that object or array.
414-
415-
Substitution can refer to paths within themselves, for example:
416-
417-
bar : { foo : 42,
418-
baz : ${bar.foo}
419-
}
420-
421-
Because there is no inherent cycle here, the substitution will "look forward" (including looking at the field currently being defined). To make this clearer, in the example below, `bar.baz` would be `43`:
422-
423-
bar : { foo : 42,
424-
baz : ${bar.foo}
425-
}
426-
bar : { foo : 43 }
427-
428-
Mutually-referring objects would also work, and are not self-referential (so they look forward):
429-
430-
// bar.a will end up as 4 and foo.c will end up as 3
431-
bar : { a : ${foo.d}, b : 1 }
432-
bar.b = 3
433-
foo : { c : ${bar.b}, d : 2 }
434-
foo.d = 4
435-
436-
One tricky case is an optional self-reference in a value concatenation, in this example `a` would be `foo` not `foofoo` because the self reference has to "look back" to an undefined `a`:
437-
438-
a = ${?a}foo
274+
[!code-csharp[SerializationSetupDocSpec](../../../src/core/Akka.Docs.Tests/Configuration/SerializationSetupDocSpec.cs?name=StringSubstitutionSample)]
275+
[!code-csharp[SerializationSetupDocSpec](../../../src/core/Akka.Docs.Tests/Configuration/SerializationSetupDocSpec.cs?name=ArraySubstitutionSample)]

src/core/Akka.Docs.Tests/Configuration/ConfigurationSample.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,5 @@
204204
// // </BlockedEnvironmentVariableSample>
205205
// }
206206

207-
// [Fact]
208-
// public void UnsolvableSubstitutionWillThrowSample()
209-
// {
210-
// // <UnsolvableSubstitutionWillThrowSample>
211-
// // This substitution will throw an exception because it is a required substitution,
212-
// // and we can not resolve it, even when checking for environment variables.
213-
// var hoconString = "from_environment = ${MY_ENV_VAR}";
214-
215-
// Assert.Throws<Exception>(() =>
216-
// {
217-
// Config config = hoconString;
218-
// }).Message.Should().StartWith("Unresolved substitution");
219-
// // </UnsolvableSubstitutionWillThrowSample>
220-
// }
221207
// }
222208
//}

src/core/Akka.Docs.Tests/Configuration/SerializationSetupDocSpec.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,45 @@ public void SerializationSetupShouldWorkAsExpected()
125125
}
126126
// </Verification>
127127
}
128+
[Fact]
129+
public void UnsolvableSubstitutionWillThrowSample()
130+
{
131+
// <UnsolvableSubstitutionWillThrowSample>
132+
// This substitution will throw an exception because it is a required substitution,
133+
// and we can not resolve it.
134+
135+
var hoconString = "from_environment = ${does-not-exist}";
136+
137+
Assert.Throws<FormatException>(() =>
138+
{
139+
Config config = hoconString;
140+
}).Message.Should().StartWith("Unresolved substitution");
141+
// </UnsolvableSubstitutionWillThrowSample>
142+
}
143+
[Fact]
144+
public void StringSubstitutionSample()
145+
{
146+
// <StringSubstitutionSample>
147+
// ${string_bar} will be substituted by 'bar' and concatenated with `foo` into `foobar`
148+
var hoconString = @"
149+
string_bar = bar
150+
string_foobar = foo${string_bar}
151+
";
152+
var config = ConfigurationFactory.ParseString(hoconString); // This config uses ConfigurationFactory as a helper
153+
config.GetString("string_foobar").Should().Be("foobar");
154+
// </StringSubstitutionSample>
155+
}
156+
[Fact]
157+
public void ArraySubstitutionSample()
158+
{
159+
// <ArraySubstitutionSample>
160+
// ${a} will be substituted by the array [1, 2] and concatenated with [3, 4] to create [1, 2, 3, 4]
161+
var hoconString = @"
162+
a = [1,2]
163+
b = ${a} [3, 4]";
164+
Config config = hoconString; // This Config uses implicit conversion from string directly into a Config object
165+
(new[] { 1, 2, 3, 4 }).Should().BeEquivalentTo(config.GetIntList("b"));
166+
// </ArraySubstitutionSample>
167+
}
128168
}
129169
}

0 commit comments

Comments
 (0)