Skip to content

Commit 1550057

Browse files
authored
add support for custom merge strategy (#104)
1 parent 6fa86f4 commit 1550057

File tree

5 files changed

+100
-13
lines changed

5 files changed

+100
-13
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ usage: himl [-h] [--output-file OUTPUT_FILE] [--format OUTPUT_FORMAT]
136136
[--skip-interpolation-validation]
137137
[--skip-interpolation-resolving] [--enclosing-key ENCLOSING_KEY]
138138
[--cwd CWD]
139+
[--list-merge-strategy {append,override,prepend,append_unique}]
139140
path
140141
```
141142

@@ -352,3 +353,38 @@ VA7: !include configs/env=int/region=va7/kafka-brokers.yaml regionBrokers.VA
352353
```
353354

354355
This will replace the value after interpolation with the value of the regionBrokers.VA7 found under the configs/env=int/region=va7/kafka-brokers.yaml path.
356+
357+
## Custom merge strategy
358+
An optional parameter `type_strategies` can be passed into ConfigProcessor to define custom merging behavior. It could be custom functions that fit your needs.
359+
Your function should take the arguments of (config, path, base, nxt) and return the merged result.
360+
361+
Example:
362+
```py
363+
from himl import ConfigProcessor
364+
365+
def strategy_merge_override(config, path, base, nxt):
366+
"""merge list of dicts. if objects have same id, nxt replaces base."""
367+
"""if remove flag is present in nxt item, remove base and not add nxt"""
368+
result = deepcopy(base)
369+
for nxto in nxt:
370+
for baseo in result:
371+
# if list is not a list of dicts, bail out and let the next strategy to execute
372+
if not isinstance(baseo,dict) or not isinstance(nxto,dict):
373+
return STRATEGY_END
374+
if 'id' in baseo and 'id' in nxto and baseo['id'] == nxto['id']:
375+
result.remove(baseo) #same id, remove previous item
376+
if 'remove' not in nxto:
377+
result.append(nxto)
378+
return result
379+
380+
config_processor = ConfigProcessor()
381+
path = "examples/simple/production"
382+
filters = () # can choose to output only specific keys
383+
exclude_keys = () # can choose to remove specific keys
384+
output_format = "yaml" # yaml/json
385+
386+
config_processor.process(path=path, filters=filters, exclude_keys=exclude_keys,
387+
output_format=output_format, print_data=True,
388+
type_strategies= [(list, [strategy_merge_override,'append']), (dict, ["merge"])] ))
389+
390+
```

examples/complex/default.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,23 @@ cluster_info:
88
# Interpolation example
99
description: "This is cluster: {{cluster}}. It is using {{cluster_info.node_type}} instance type."
1010
node_type: c3.2xlarge # default value, which can be overridden by each cluster
11-
11+
cluster_metrics:
12+
- id: 1
13+
metric: cpu
14+
value: 90
15+
- id: 2
16+
metric: memory
17+
value: 90
18+
- id: 3
19+
metric: disk
20+
value: 90
21+
metrics:
22+
- cpu
23+
- memory
24+
- disk
25+
myList:
26+
- id1
27+
- id4
1228
# Fetching the secret value at runtime, from a secrets store (in this case AWS SSM).
1329
# passphrase: "{{ssm.path(/key/coming/from/aws/secrets/store/manager).aws_profile(myprofile)}}"
1430

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
11
cluster: cluster2
2+
cluster_metrics:
3+
- id: 1
4+
metric: cpu
5+
value: 95
6+
- id: 2
7+
metric: memory
8+
value: 95
9+
- id: 3
10+
metric: disk
11+
remove: True
12+
- metric: exec
13+
value: 5
14+
metrics:
15+
- cpu
16+
- exec
17+
myList:
18+
- id1
19+
- id2
20+
- id3
21+

himl/config_generator.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
class ConfigProcessor(object):
2929

3030
def process(self, cwd=None, path=None, filters=(), exclude_keys=(), enclosing_key=None, remove_enclosing_key=None, output_format="yaml",
31-
print_data=False, output_file=None, skip_interpolations=False, skip_interpolation_validation=False, skip_secrets=False, multi_line_string=False):
31+
print_data=False, output_file=None, skip_interpolations=False, skip_interpolation_validation=False, skip_secrets=False, multi_line_string=False,
32+
type_strategies = [(list, ["append"]), (dict, ["merge"])], fallback_strategies = ["override"], type_conflict_strategies = ["override"]):
3233

3334
path = self.get_relative_path(path)
3435

@@ -41,7 +42,7 @@ def process(self, cwd=None, path=None, filters=(), exclude_keys=(), enclosing_ke
4142
if cwd is None:
4243
cwd = os.getcwd()
4344

44-
generator = ConfigGenerator(cwd, path, multi_line_string)
45+
generator = ConfigGenerator(cwd, path, multi_line_string, type_strategies, fallback_strategies, type_conflict_strategies)
4546
generator.generate_hierarchy()
4647
generator.process_hierarchy()
4748

@@ -120,13 +121,15 @@ class ConfigGenerator(object):
120121
will contain merged data on each layer.
121122
"""
122123

123-
def __init__(self, cwd, path, multi_line_string):
124+
def __init__(self, cwd, path, multi_line_string, type_strategies, fallback_strategies, type_conflict_strategies):
124125
self.cwd = cwd
125126
self.path = path
126127
self.hierarchy = self.generate_hierarchy()
127128
self.generated_data = OrderedDict()
128129
self.interpolation_validator = InterpolationValidator()
129-
130+
self.type_strategies = type_strategies
131+
self.fallback_strategies = fallback_strategies
132+
self.type_conflict_strategies = type_conflict_strategies
130133
if multi_line_string is True:
131134
yaml.representer.BaseRepresenter.represent_scalar = ConfigGenerator.custom_represent_scalar
132135

@@ -176,22 +179,22 @@ def yaml_get_content(yaml_file):
176179
return content if content else {}
177180

178181
@staticmethod
179-
def merge_value(reference, new_value):
180-
merger = Merger([(list, ["append"]), (dict, ["merge"])], ["override"], ["override"])
182+
def merge_value(reference, new_value, type_strategies, fallback_strategies, type_conflict_strategies):
183+
merger = Merger(type_strategies, fallback_strategies, type_conflict_strategies)
181184
if isinstance(new_value, (list, set, dict)):
182185
new_reference = merger.merge(reference, new_value)
183186
else:
184187
raise TypeError("Cannot handle merge_value of type {}".format(type(new_value)))
185188
return new_reference
186189

187190
@staticmethod
188-
def merge_yamls(values, yaml_content):
191+
def merge_yamls(values, yaml_content, type_strategies, fallback_strategies, type_conflict_strategies):
189192
for key, value in iteritems(yaml_content):
190193
if key in values and type(values[key]) != type(value):
191194
raise Exception("Failed to merge key '{}', because of mismatch in type: {} vs {}"
192195
.format(key, type(values[key]), type(value)))
193196
if key in values and not isinstance(value, primitive_types):
194-
values[key] = ConfigGenerator.merge_value(values[key], value)
197+
values[key] = ConfigGenerator.merge_value(values[key], value, type_strategies, fallback_strategies, type_conflict_strategies)
195198
else:
196199
values[key] = value
197200

@@ -224,7 +227,7 @@ def process_hierarchy(self):
224227
for yaml_files in self.hierarchy:
225228
for yaml_file in yaml_files:
226229
yaml_content = self.yaml_get_content(yaml_file)
227-
self.merge_yamls(merged_values, yaml_content)
230+
self.merge_yamls(merged_values, yaml_content, self.type_strategies, self.fallback_strategies, self.type_conflict_strategies)
228231
self.resolve_simple_interpolations(merged_values, yaml_file)
229232
self.generated_data = merged_values
230233

himl/main.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,17 @@
1010

1111
import argparse
1212
import os
13-
1413
from .config_generator import ConfigProcessor
14+
from enum import Enum
15+
16+
class ListMergeStrategy(Enum):
17+
append = 'append'
18+
override = 'override'
19+
prepend = 'prepend'
20+
append_unique = 'append_unique' #WARNING: currently this strategy does not support list of dicts, only list of str
1521

22+
def __str__(self):
23+
return self.value
1624

1725
class ConfigRunner(object):
1826

@@ -29,9 +37,11 @@ def do_run(self, opts):
2937
opts.print_data = True
3038

3139
config_processor = ConfigProcessor()
40+
3241
config_processor.process(cwd, opts.path, filters, excluded_keys, opts.enclosing_key, opts.remove_enclosing_key,
3342
opts.output_format, opts.print_data, opts.output_file, opts.skip_interpolation_resolving,
34-
opts.skip_interpolation_validation, opts.skip_secrets, opts.multi_line_string)
43+
opts.skip_interpolation_validation, opts.skip_secrets, opts.multi_line_string,
44+
type_strategies= [(list, [opts.merge_list_strategy.value]), (dict, ["merge"])] )
3545

3646
@staticmethod
3747
def get_parser(parser=None):
@@ -63,10 +73,12 @@ def get_parser(parser=None):
6373
help='the working directory')
6474
parser.add_argument('--multi-line-string', action='store_true',
6575
help='will overwrite the global yaml dumper to use block style')
76+
parser.add_argument('--list-merge-strategy', dest='merge_list_strategy', type=ListMergeStrategy, choices=list(ListMergeStrategy),
77+
default='append',
78+
help='override default merge strategy for list')
6679
parser.add_argument('--version', action='version', version='%(prog)s v{version}'.format(version="0.10.0"),
6780
help='print himl version')
6881
return parser
6982

70-
7183
def run(args=None):
7284
ConfigRunner().run(args)

0 commit comments

Comments
 (0)