spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.5k stars 40.53k forks source link

Allow binder to compose collections from multiple sources #41830

Open philwebb opened 1 month ago

philwebb commented 1 month ago

Currently the Binder class has a "first wins" policy for binding lists. From the docs:

When lists are configured in more than one place, overriding works by replacing the entire list.

This policy has worked well, however, it also causes problems such as #41669 where users really want to merge lists together.

I think it would be a mistake to attempt to compose lists from elements in different property sources, however, we might be able to offer a feature where distinct lists could be merged together.

Proposal

When property values are defined, the property name could include an additional indicator that is used to determine how the values should be merged. The indicator would be (+) for addAll and (-) for removeAll:

This example shows an auto-configure exclude:

.properties

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientAutoConfiguration
#---
spring.config.activate.on-profile: noredis
spring.autoconfigure.exclude(+)=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

.yaml

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientAutoConfiguration
---
spring:
  config:
    activate:
      on-profile: noredis
  autoconfigure:
    exclude(+): org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

More complex examples are:

.properties

my.list-of-names(+)[0].first=Spring
my.list-of-names(+)[0].last=Boot
my.list-of-names(+)[1].first=Spring
my.list-of-names(+)[1].last=Framework

.yaml

my:
  list(-):
    - remove
    - stuff

Limitations

It will be hard to support this syntax with environment variables.

philwebb commented 3 weeks ago

Perhaps without the brackets. And perhaps only + for now.

tmoschou commented 3 weeks ago

I'ld be interested to know how this might work for collections of complex types (which might have sub-collections). E.g.

my-collection:
  - name: foo
    prop: x 
    sub-collection:
      - item1
      - item2
  - name: bar
    prop: y
    sub-collection:
      - item3
      - item4

Removing an item from my-collection or setting a property of an item, without knowing its index might be tricky. E.g

'my-collection[name == "bar"].prop': "z"

We generally in our team have an convention that if

then its probably better modelled as map using the sub-property as a key - and in most other cases we don't actually need to mutate existing collections and content with "first wins" policy.

my-collection:
  foo:
    prop: x 
    sub-collection:
      - item1
      - item2
  bar: 
    prop: y
    sub-collection:
      - item3
      - item4