yaml-merge 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 39fe59fba26814f205fb26d2d3c0e09ec1a34a308bfe59c0c6ce74f046468711
4
+ data.tar.gz: dc2cdbb601a492791ba3da595aa9ec814d543428bddcbb1e0d2bdaa5cc93070c
5
+ SHA512:
6
+ metadata.gz: e87f09daeb658603e7727073b4bc6d7b8ca5b7d1de4b8e4af26ed6a9c7dc65b52d08541db2c50c53bab791e01021d14b04a1ae95dd2e573428211b08e6d279cc
7
+ data.tar.gz: 7c0d7b2f31a4a721385218ada4ad9fe915e041fd03e4f1e9777d07a38f3ebbe1592ee01b61e666a5805ca250440284845b68007de7c780b05e520d67f3c99ef4
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yaml
4
+ module Merge
5
+ module Version
6
+ VERSION = "7.0.0"
7
+ end
8
+
9
+ VERSION = Version::VERSION
10
+ end
11
+ end
data/lib/yaml/merge.rb ADDED
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "tree_haver"
6
+
7
+ module Yaml
8
+ module Merge
9
+ PACKAGE_NAME = "yaml-merge"
10
+ DESTINATION_WINS_ARRAY_POLICY = {
11
+ surface: "array",
12
+ name: "destination_wins_array"
13
+ }.freeze
14
+ BACKEND_REFERENCE = TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
15
+
16
+ module_function
17
+
18
+ def yaml_feature_profile
19
+ {
20
+ family: "yaml",
21
+ supported_dialects: ["yaml"],
22
+ supported_policies: [DESTINATION_WINS_ARRAY_POLICY]
23
+ }
24
+ end
25
+
26
+ def available_yaml_backends
27
+ [BACKEND_REFERENCE]
28
+ end
29
+
30
+ def yaml_backend_feature_profile(backend: nil)
31
+ resolved_backend = resolve_backend(backend)
32
+ return unsupported_feature_result("Unsupported YAML backend #{resolved_backend}.") unless resolved_backend == BACKEND_REFERENCE.id
33
+
34
+ yaml_feature_profile.merge(
35
+ backend: BACKEND_REFERENCE.id,
36
+ backend_ref: BACKEND_REFERENCE.to_h
37
+ )
38
+ end
39
+
40
+ def yaml_plan_context(backend: nil)
41
+ profile = yaml_backend_feature_profile(backend: backend)
42
+ return profile if profile[:ok] == false
43
+
44
+ {
45
+ family_profile: yaml_feature_profile,
46
+ feature_profile: {
47
+ backend: profile[:backend],
48
+ supports_dialects: false,
49
+ supported_policies: profile[:supported_policies]
50
+ }
51
+ }
52
+ end
53
+
54
+ def parse_yaml(source, dialect, backend: nil)
55
+ return unsupported_feature_parse_result("Unsupported YAML dialect #{dialect}.") unless dialect == "yaml"
56
+
57
+ resolved_backend = resolve_backend(backend)
58
+ return unsupported_feature_parse_result("Unsupported YAML backend #{resolved_backend}.") unless resolved_backend == BACKEND_REFERENCE.id
59
+
60
+ syntax_result = TreeHaver.parse_with_language_pack(
61
+ TreeHaver::ParserRequest.new(source: source, language: "yaml")
62
+ )
63
+ return { ok: false, diagnostics: syntax_result[:diagnostics], policies: [] } unless syntax_result[:ok]
64
+
65
+ parsed = YAML.safe_load(source, permitted_classes: [], aliases: false)
66
+ analyze_yaml_document(parsed, dialect)
67
+ rescue StandardError => e
68
+ parse_error_result(e.message)
69
+ end
70
+
71
+ def analyze_yaml_document(parsed, dialect)
72
+ return unsupported_feature_parse_result("Unsupported YAML dialect #{dialect}.") unless dialect == "yaml"
73
+ return parse_error_result("YAML documents must parse to a mapping root.") unless parsed.is_a?(Hash)
74
+
75
+ validated = validate_yaml_node(parsed, "")
76
+ return { ok: false, diagnostics: [validated[:diagnostic]], policies: [] } unless validated[:ok]
77
+
78
+ {
79
+ ok: true,
80
+ diagnostics: [],
81
+ analysis: {
82
+ kind: "yaml",
83
+ dialect: "yaml",
84
+ normalized_source: canonical_yaml(validated[:value]),
85
+ root_kind: "mapping",
86
+ owners: collect_yaml_owners(validated[:value])
87
+ },
88
+ policies: []
89
+ }
90
+ end
91
+
92
+ def match_yaml_owners(template, destination)
93
+ destination_paths = destination[:owners].to_h { |owner| [owner[:path], true] }
94
+ template_paths = template[:owners].to_h { |owner| [owner[:path], true] }
95
+
96
+ {
97
+ matched: template[:owners]
98
+ .filter { |owner| destination_paths[owner[:path]] }
99
+ .map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
100
+ unmatched_template: template[:owners].map { |owner| owner[:path] }.reject { |path| destination_paths[path] },
101
+ unmatched_destination: destination[:owners].map { |owner| owner[:path] }.reject { |path| template_paths[path] }
102
+ }
103
+ end
104
+
105
+ def merge_yaml(template_source, destination_source, dialect, backend: nil)
106
+ resolved_backend = resolve_backend(backend)
107
+ return unsupported_feature_merge_result("Unsupported YAML backend #{resolved_backend}.") unless resolved_backend == BACKEND_REFERENCE.id
108
+
109
+ merge_yaml_with_parser(template_source, destination_source, dialect) do |source, parse_dialect|
110
+ parse_yaml(source, parse_dialect, backend: resolved_backend)
111
+ end
112
+ end
113
+
114
+ def merge_yaml_with_parser(template_source, destination_source, dialect)
115
+ template = yield(template_source, dialect)
116
+ return { ok: false, diagnostics: template[:diagnostics], policies: [] } unless template[:ok]
117
+
118
+ destination = yield(destination_source, dialect)
119
+ unless destination[:ok]
120
+ return {
121
+ ok: false,
122
+ diagnostics: destination[:diagnostics].map do |diagnostic|
123
+ diagnostic[:category] == "parse_error" ? diagnostic.merge(category: "destination_parse_error") : diagnostic
124
+ end,
125
+ policies: []
126
+ }
127
+ end
128
+
129
+ template_document = YAML.safe_load(template.dig(:analysis, :normalized_source), permitted_classes: [], aliases: false)
130
+ destination_document = YAML.safe_load(destination.dig(:analysis, :normalized_source), permitted_classes: [], aliases: false)
131
+ unless template_document.is_a?(Hash) && destination_document.is_a?(Hash)
132
+ return parse_error_merge_result("YAML documents must parse to a mapping root.")
133
+ end
134
+
135
+ {
136
+ ok: true,
137
+ diagnostics: [],
138
+ output: canonical_yaml(merge_yaml_mappings(template_document, destination_document)),
139
+ policies: [DESTINATION_WINS_ARRAY_POLICY]
140
+ }
141
+ rescue StandardError => e
142
+ {
143
+ ok: false,
144
+ diagnostics: [{ severity: "error", category: "destination_parse_error", message: e.message }],
145
+ policies: []
146
+ }
147
+ end
148
+
149
+ def resolve_backend(backend)
150
+ backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
151
+ end
152
+ private_class_method :resolve_backend
153
+
154
+ def validate_yaml_node(value, path)
155
+ if scalar?(value)
156
+ { ok: true, value: value }
157
+ elsif value.is_a?(Array)
158
+ value.each_with_index.each_with_object({ ok: true, value: [] }) do |(item, index), memo|
159
+ validated = validate_yaml_node(item, "#{path}/#{index}")
160
+ return validated unless validated[:ok]
161
+
162
+ memo[:value] << validated[:value]
163
+ end
164
+ elsif value.is_a?(Hash)
165
+ value.keys.each_with_object({ ok: true, value: {} }) do |key, memo|
166
+ validated = validate_yaml_node(value[key], "#{path}/#{key}")
167
+ return validated unless validated[:ok]
168
+
169
+ memo[:value][key] = validated[:value]
170
+ end
171
+ else
172
+ unsupported_feature_result("Unsupported YAML value at #{display_path(path)}. Only mappings, scalar values, and sequences are supported.")
173
+ end
174
+ end
175
+ private_class_method :validate_yaml_node
176
+
177
+ def scalar?(value)
178
+ value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
179
+ end
180
+ private_class_method :scalar?
181
+
182
+ def display_path(path)
183
+ path.empty? ? "/" : path
184
+ end
185
+ private_class_method :display_path
186
+
187
+ def render_yaml_scalar(value)
188
+ if value.nil?
189
+ ""
190
+ elsif value.is_a?(String)
191
+ value.match?(/\A[A-Za-z0-9_.-]+\z/) ? value : JSON.generate(value)
192
+ elsif value == true || value == false
193
+ value ? "true" : "false"
194
+ else
195
+ value.to_s
196
+ end
197
+ end
198
+ private_class_method :render_yaml_scalar
199
+
200
+ def render_yaml_node(key, value, indent)
201
+ prefix = " " * indent
202
+ if value.is_a?(Array)
203
+ ["#{prefix}#{key}:"] + render_yaml_sequence(value, indent + 2)
204
+ elsif value.is_a?(Hash)
205
+ ["#{prefix}#{key}:"] + render_yaml_mapping(value, indent + 2)
206
+ elsif value.nil?
207
+ ["#{prefix}#{key}:"]
208
+ else
209
+ ["#{prefix}#{key}: #{render_yaml_scalar(value)}"]
210
+ end
211
+ end
212
+ private_class_method :render_yaml_node
213
+
214
+ def render_yaml_mapping(mapping, indent = 0)
215
+ mapping.keys.flat_map do |key|
216
+ render_yaml_node(key, mapping[key], indent)
217
+ end
218
+ end
219
+ private_class_method :render_yaml_mapping
220
+
221
+ def render_yaml_sequence(sequence, indent)
222
+ prefix = " " * indent
223
+ sequence.flat_map do |item|
224
+ if scalar?(item)
225
+ ["#{prefix}- #{render_yaml_scalar(item)}"]
226
+ elsif item.is_a?(Hash)
227
+ ["#{prefix}-"] + render_yaml_mapping(item, indent + 2)
228
+ elsif item.is_a?(Array)
229
+ ["#{prefix}-"] + render_yaml_sequence(item, indent + 2)
230
+ else
231
+ ["#{prefix}- #{render_yaml_scalar(item)}"]
232
+ end
233
+ end
234
+ end
235
+ private_class_method :render_yaml_sequence
236
+
237
+ def canonical_yaml(mapping)
238
+ "#{render_yaml_mapping(mapping).join("\n")}\n"
239
+ end
240
+ private_class_method :canonical_yaml
241
+
242
+ def collect_yaml_owners(mapping, prefix = "")
243
+ mapping.keys.sort.flat_map do |key|
244
+ path = "#{prefix}/#{key}"
245
+ value = mapping[key]
246
+ if value.is_a?(Array)
247
+ [{ path: path, owner_kind: "key_value", match_key: key }] +
248
+ value.each_with_index.flat_map do |item, index|
249
+ item_path = "#{path}/#{index}"
250
+ nested = item.is_a?(Hash) ? collect_yaml_owners(item, item_path) : []
251
+ [{ path: item_path, owner_kind: "sequence_item" }] + nested
252
+ end
253
+ elsif value.is_a?(Hash)
254
+ [{ path: path, owner_kind: "mapping", match_key: key }] + collect_yaml_owners(value, path)
255
+ else
256
+ [{ path: path, owner_kind: "key_value", match_key: key }]
257
+ end
258
+ end
259
+ end
260
+ private_class_method :collect_yaml_owners
261
+
262
+ def merge_yaml_mappings(template, destination)
263
+ ordered_merge_keys(template, destination).each_with_object({}) do |key, merged|
264
+ if !template.key?(key)
265
+ merged[key] = destination[key]
266
+ elsif !destination.key?(key)
267
+ merged[key] = template[key]
268
+ elsif template[key].is_a?(Hash) && destination[key].is_a?(Hash)
269
+ merged[key] = merge_yaml_mappings(template[key], destination[key])
270
+ else
271
+ merged[key] = destination[key]
272
+ end
273
+ end
274
+ end
275
+ private_class_method :merge_yaml_mappings
276
+
277
+ def ordered_merge_keys(template, destination)
278
+ template.keys + destination.keys.reject { |key| template.key?(key) }
279
+ end
280
+ private_class_method :ordered_merge_keys
281
+
282
+ def parse_error_result(message)
283
+ { ok: false, diagnostics: [{ severity: "error", category: "parse_error", message: message }], policies: [] }
284
+ end
285
+ private_class_method :parse_error_result
286
+
287
+ def unsupported_feature_parse_result(message)
288
+ { ok: false, diagnostics: [{ severity: "error", category: "unsupported_feature", message: message }], policies: [] }
289
+ end
290
+ private_class_method :unsupported_feature_parse_result
291
+
292
+ def unsupported_feature_merge_result(message)
293
+ { ok: false, diagnostics: [{ severity: "error", category: "unsupported_feature", message: message }], policies: [] }
294
+ end
295
+ private_class_method :unsupported_feature_merge_result
296
+
297
+ def parse_error_merge_result(message)
298
+ { ok: false, diagnostics: [{ severity: "error", category: "parse_error", message: message }], policies: [] }
299
+ end
300
+ private_class_method :parse_error_merge_result
301
+
302
+ def unsupported_feature_result(message)
303
+ { ok: false, diagnostic: { severity: "error", category: "unsupported_feature", message: message } }
304
+ end
305
+ private_class_method :unsupported_feature_result
306
+ end
307
+ end
data/lib/yaml-merge.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "yaml/merge"
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yaml-merge
3
+ version: !ruby/object:Gem::Version
4
+ version: 7.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter H. Boling
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
13
+ ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
14
+ A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
15
+ DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
16
+ LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
17
+ uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
18
+ LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
19
+ mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
20
+ coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
21
+ FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
22
+ yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
23
+ to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
24
+ qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
25
+ fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
26
+ HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
27
+ A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
28
+ ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
29
+ wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
30
+ L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
31
+ GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
32
+ kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
33
+ QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
34
+ 0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
35
+ DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
36
+ L9nRqA==
37
+ -----END CERTIFICATE-----
38
+ date: 1980-01-02 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: ast-merge
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 7.0.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 7.0.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: tree_haver
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - '='
59
+ - !ruby/object:Gem::Version
60
+ version: 7.0.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '='
66
+ - !ruby/object:Gem::Version
67
+ version: 7.0.0
68
+ description: Portable YAML analysis, owner matching, and merge behavior for Structured
69
+ Merge.
70
+ email:
71
+ - info@structuredmerge.org
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/yaml-merge.rb
77
+ - lib/yaml/merge.rb
78
+ - lib/yaml/merge/version.rb
79
+ homepage: https://github.com/structuredmerge/structuredmerge-ruby
80
+ licenses:
81
+ - AGPL-3.0-only
82
+ - PolyForm-Small-Business-1.0.0
83
+ metadata:
84
+ homepage_uri: https://structuredmerge.org
85
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.0.0
86
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.0.0/CHANGELOG.md
87
+ bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
88
+ documentation_uri: https://www.rubydoc.info/gems/yaml-merge/7.0.0
89
+ funding_uri: https://github.com/sponsors/pboling
90
+ wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
91
+ discord_uri: https://discord.gg/3qme4XHNKN
92
+ rubygems_mfa_required: 'true'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 4.0.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 4.0.10
108
+ specification_version: 4
109
+ summary: Structured Merge YAML analysis and merge for Ruby
110
+ test_files: []
metadata.gz.sig ADDED
Binary file