jdaugherty commented on code in PR #16770:
URL: https://github.com/apache/grails-core/pull/16770#discussion_r4382487693


##########
grails-core/src/test/groovy/org/grails/config/PropertySourcesConfigSpec.groovy:
##########
@@ -116,4 +117,38 @@ class PropertySourcesConfigSpec extends Specification {
         then: 'the second value is not merged with the first value'
         cfg.foo.bar == ['two': '2']
     }
+
+    void 'a placeholder in a merged value is resolved once, for the flat key 
and the nested entry alike'() {
+        given: "a property source that answers a new value each time it is 
read"
+        def counter = new CountingPropertySource()
+        def config = new PropertySourcesConfig(counter)
+
+        when: "a map holding a placeholder is merged"
+        config.merge([app: [url: 'http://${host}']])
+
+        then: "the dotted key, the nested map and the export all read the one 
resolved value"
+        config.getProperty('app.url') == 'http://host1'
+        config['app.url'] == 'http://host1'
+        config.navigate('app', 'url') == 'http://host1'
+        config.toProperties().getProperty('app.url') == 'http://host1'

Review Comment:
   On 7.0.x this assertion already passes, and so does the `getProperty` one: 
only `config['app.url']` fails without the fix. On a fresh merge the flat 
`app.url` key is put into the root before `app`, so `toProperties()` writes the 
raw value first and then overwrites it with the nested one. The export only 
came out raw when the section already existed:
   
   ```groovy
   config.merge([app: [name: 'x']])
   config.merge([app: [url: 'http://${host}']])
   config.toProperties().getProperty('app.url')  // 'http://${host}' on 7.0.x, 
'http://host1' here
   ```
   
   Could the spec merge into an existing `app` section first, so the export 
assertion also fails on the old code? The `toProperties()` line in the 
description has the same caveat: for the example as written it was already 
`'http://example.com'`.



##########
grails-core/src/test/groovy/org/grails/config/PropertySourcesConfigSpec.groovy:
##########
@@ -116,4 +117,38 @@ class PropertySourcesConfigSpec extends Specification {
         then: 'the second value is not merged with the first value'
         cfg.foo.bar == ['two': '2']
     }
+
+    void 'a placeholder in a merged value is resolved once, for the flat key 
and the nested entry alike'() {
+        given: "a property source that answers a new value each time it is 
read"
+        def counter = new CountingPropertySource()
+        def config = new PropertySourcesConfig(counter)
+
+        when: "a map holding a placeholder is merged"
+        config.merge([app: [url: 'http://${host}']])
+
+        then: "the dotted key, the nested map and the export all read the one 
resolved value"
+        config.getProperty('app.url') == 'http://host1'
+        config['app.url'] == 'http://host1'
+        config.navigate('app', 'url') == 'http://host1'
+        config.toProperties().getProperty('app.url') == 'http://host1'
+        counter.readCount() == 1

Review Comment:
   This holds for a nested map, but not for a dotted key, the shape a flattened 
config arrives in (`ConfigObject.flatten()`, property sources). 
`config.merge(['app.url': 'http://${host}'])` resolves the placeholder twice, 
once for the root `app.url` entry and once in the nested pass, and the second 
result ends up in both places: `host2` from every read, `readCount() == 2`. So 
the two places agree, which is the point of the fix, but "resolved once" in the 
feature name and the commit message only holds for nested maps.
   
   Could the spec also cover the dotted-key shape, for example as a `where:` 
row, asserting that the four reads agree rather than counting the reads?



##########
grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy:
##########
@@ -271,10 +271,12 @@ class NavigableMap implements Map<String, Object>, 
Cloneable {
                 }
                 targetMap.remove(sourceKey)
             } else {
+                mergeMapEntry(targetMap, sourceKey, newValue)
                 if (path) {
-                    rootMap.put("${path}.${sourceKey}".toString(), newValue)
+                    // the flat key holds the value the nested entry holds, as 
the hook stored it, so a placeholder in it
+                    // is resolved once, for both
+                    rootMap.put("${path}.${sourceKey}".toString(), 
targetMap.get(sourceKey))

Review Comment:
   Not caused by this change, but it's the one shape where the flat key and the 
nested entry still disagree after it, so noting it here (the line is 162, 
outside the diff): a dotted key inside a nested map. The nested pass gets 
`pathParts.join('.')` as its path without the enclosing `path`, so for
   
   ```groovy
   config.merge([grails: ['gorm.url': 'http://${host}']])
   ```
   
   `grails.gorm.url` keeps the first resolution, the nested entry gets the 
second, and the second also lands under a stray root key:
   
   ```groovy
   config['grails.gorm.url']                // 'http://host1'
   config.navigate('grails', 'gorm', 'url') // 'http://host2'
   config.getProperty('gorm.url')           // 'http://host2'
   ```
   
   Prefixing the enclosing path at line 162 (`path ? 
"${path}.${pathParts.join('.')}".toString() : pathParts.join('.')`) makes all 
three agree and drops the stray key, and the grails-bootstrap and grails-core 
tests pass with it. 8.0.x has the same code. YAML doesn't reach this, since 
Spring flattens it before the merge; it takes a nested map holding a dotted key 
passed to `merge`. Fine as a follow-up issue if you'd rather keep this PR to 
the one line.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to