lutaml-model 0.8.0 → 0.8.2
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 +4 -4
- data/.github/workflows/dependent-repos.json +9 -0
- data/.github/workflows/downstream-performance.yml +0 -3
- data/.rubocop_todo.yml +18 -186
- data/README.adoc +212 -15
- data/bench/bench_xmi.rb +6 -6
- data/bench/gate_config.rb +2 -9
- data/docs/_pages/configuration.adoc +155 -41
- data/docs/_pages/serialization_adapters.adoc +65 -14
- data/docs/index.adoc +3 -1
- data/docs/yamls_sequence.adoc +335 -0
- data/lib/lutaml/hash_format.rb +4 -0
- data/lib/lutaml/json/adapter/multi_json_adapter.rb +4 -2
- data/lib/lutaml/json/adapter/oj_adapter.rb +4 -2
- data/lib/lutaml/json.rb +4 -0
- data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +4 -2
- data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +4 -2
- data/lib/lutaml/model/adapter_resolver.rb +410 -0
- data/lib/lutaml/model/adapter_scope.rb +64 -0
- data/lib/lutaml/model/config.rb +84 -21
- data/lib/lutaml/model/configuration.rb +17 -249
- data/lib/lutaml/model/format_registry.rb +44 -117
- data/lib/lutaml/model/mapping/listener.rb +4 -2
- data/lib/lutaml/model/serialize/format_conversion.rb +42 -3
- data/lib/lutaml/model/serialize.rb +4 -2
- data/lib/lutaml/model/services/base.rb +4 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +2 -0
- data/lib/lutaml/toml.rb +10 -3
- data/lib/lutaml/xml/serialization/instance_methods.rb +6 -0
- data/lib/lutaml/xml.rb +3 -4
- data/lib/lutaml/yaml.rb +4 -0
- data/lib/lutaml/yamls/adapter/mapping.rb +7 -0
- data/lib/lutaml/yamls/adapter/standard_adapter.rb +23 -2
- data/lib/lutaml/yamls/adapter/transform.rb +105 -7
- data/lib/lutaml/yamls/adapter/yamls_sequence.rb +20 -0
- data/lib/lutaml/yamls/adapter/yamls_sequence_rule.rb +48 -0
- data/lib/lutaml/yamls/adapter.rb +2 -0
- data/spec/fixtures/geolexica_v2_concept.rb +136 -0
- data/spec/fixtures/geolexica_v2_sample.yaml +36 -0
- data/spec/fixtures/geolexica_v2_sample2.yaml +38 -0
- data/spec/fixtures/yamls_range_concept.rb +139 -0
- data/spec/lutaml/model/xml_decoupling_spec.rb +5 -4
- data/spec/lutaml/model/yamls_range_spec.rb +393 -0
- data/spec/lutaml/model/yamls_sequence_spec.rb +245 -0
- data/spec/spec_helper.rb +5 -0
- metadata +13 -3
- data/bench/bench_uniword.rb +0 -69
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
# Single authority for adapter resolution: (format, type_name) → adapter class.
|
|
6
|
+
#
|
|
7
|
+
# Consolidates adapter metadata, loading, validation, and auto-detection
|
|
8
|
+
# that was previously spread across Config, Configuration, and FormatRegistry.
|
|
9
|
+
#
|
|
10
|
+
# Resolution chain (in order):
|
|
11
|
+
# 1. Thread-local AdapterScope override
|
|
12
|
+
# 2. Explicitly configured type (from Config.xml_adapter_type =)
|
|
13
|
+
# 3. Lazy auto-detected type (cached after first probe)
|
|
14
|
+
# 4. Default from format metadata
|
|
15
|
+
# 5. Raise FormatAdapterNotSpecifiedError
|
|
16
|
+
#
|
|
17
|
+
# @example Configure an adapter
|
|
18
|
+
# AdapterResolver.set_adapter_type(:xml, :nokogiri)
|
|
19
|
+
#
|
|
20
|
+
# @example Resolve an adapter
|
|
21
|
+
# adapter_class = AdapterResolver.adapter_for(:xml)
|
|
22
|
+
#
|
|
23
|
+
# @example Per-operation override
|
|
24
|
+
# adapter_class = AdapterResolver.resolved_adapter_class(:xml, :ox)
|
|
25
|
+
#
|
|
26
|
+
class AdapterResolver
|
|
27
|
+
class << self
|
|
28
|
+
# --- Resolution ---
|
|
29
|
+
|
|
30
|
+
# Resolve the adapter class for a format using the full resolution chain.
|
|
31
|
+
#
|
|
32
|
+
# @param format [Symbol] the format name (:xml, :json, etc.)
|
|
33
|
+
# @return [Class, nil] the adapter class or nil
|
|
34
|
+
def adapter_for(format)
|
|
35
|
+
# 1. Thread-local scope override
|
|
36
|
+
scope_override = AdapterScope.override_for(format)
|
|
37
|
+
if scope_override
|
|
38
|
+
return resolved_adapter_class(format, scope_override)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 2. Explicitly configured type
|
|
42
|
+
configured = configured_type(format)
|
|
43
|
+
if configured
|
|
44
|
+
adapter = resolved[format]
|
|
45
|
+
return adapter if adapter
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 3. Lazy auto-detection (cached)
|
|
49
|
+
detected = detected_types[format]
|
|
50
|
+
if detected.nil? && !detected_formats.key?(format)
|
|
51
|
+
result = detect_adapter_for(format)
|
|
52
|
+
if result
|
|
53
|
+
detected_formats[format] = true
|
|
54
|
+
detected_types[format] = result
|
|
55
|
+
return load_and_cache(format, result)
|
|
56
|
+
end
|
|
57
|
+
detected_formats[format] = true
|
|
58
|
+
elsif detected
|
|
59
|
+
return load_and_cache(format, detected)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 4. Default from metadata
|
|
63
|
+
default = metadata.dig(format, :default)
|
|
64
|
+
if default
|
|
65
|
+
return load_and_cache(format, default)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 5. No adapter available
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Set the adapter type for a format (validates and loads).
|
|
73
|
+
#
|
|
74
|
+
# @param format [Symbol] the format name
|
|
75
|
+
# @param type_name [Symbol] the adapter type name (:nokogiri, :ox, etc.)
|
|
76
|
+
# @return [Class] the loaded adapter class
|
|
77
|
+
def set_adapter_type(format, type_name)
|
|
78
|
+
type_name = type_name.to_sym
|
|
79
|
+
validate!(format, type_name)
|
|
80
|
+
configured_types[format] = type_name
|
|
81
|
+
resolved[format] = load_adapter(format, type_name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Store a pre-resolved adapter class for a format.
|
|
85
|
+
#
|
|
86
|
+
# Used by FormatRegistry.register for key-value formats that have
|
|
87
|
+
# a fixed adapter class (not selectable by type name).
|
|
88
|
+
#
|
|
89
|
+
# @param format [Symbol] the format name
|
|
90
|
+
# @param adapter_class [Class] the adapter class
|
|
91
|
+
# @return [void]
|
|
92
|
+
def set_adapter_class(format, adapter_class)
|
|
93
|
+
resolved[format] = adapter_class
|
|
94
|
+
configured_types[format] = :__fixed__
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Load and resolve an adapter class by type name.
|
|
98
|
+
# Used for per-operation overrides (from_xml adapter: :ox).
|
|
99
|
+
#
|
|
100
|
+
# @param format [Symbol] the format name
|
|
101
|
+
# @param type_name [Symbol, String] the adapter type name
|
|
102
|
+
# @return [Class] the adapter class
|
|
103
|
+
def resolved_adapter_class(format, type_name)
|
|
104
|
+
type_name = type_name.to_sym
|
|
105
|
+
validate!(format, type_name)
|
|
106
|
+
load_adapter(format, type_name)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# --- Metadata Registration ---
|
|
110
|
+
|
|
111
|
+
# Register adapter metadata for a format (available adapters, default).
|
|
112
|
+
#
|
|
113
|
+
# Called by FormatRegistry.register for formats with selectable adapters.
|
|
114
|
+
#
|
|
115
|
+
# @param format [Symbol] the format name
|
|
116
|
+
# @param options [Hash] { available: [...], default: :name }
|
|
117
|
+
# @return [void]
|
|
118
|
+
def register_metadata(format, options)
|
|
119
|
+
metadata[format] = options
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Register a fixed adapter class for a format with a single adapter.
|
|
123
|
+
#
|
|
124
|
+
# Used for key-value formats (json, yaml, hash, etc.) that have
|
|
125
|
+
# exactly one adapter. Creates metadata so validation works.
|
|
126
|
+
#
|
|
127
|
+
# @param format [Symbol] the format name
|
|
128
|
+
# @param adapter_class [Class] the adapter class
|
|
129
|
+
# @param adapter_name [Symbol] the adapter type name (e.g., :standard)
|
|
130
|
+
# @return [void]
|
|
131
|
+
def register_fixed(format, adapter_class, adapter_name)
|
|
132
|
+
metadata[format] = {
|
|
133
|
+
available: [adapter_name],
|
|
134
|
+
default: adapter_name,
|
|
135
|
+
}
|
|
136
|
+
resolved[format] = adapter_class
|
|
137
|
+
configured_types[format] = adapter_name
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get the configured type name for a format.
|
|
141
|
+
#
|
|
142
|
+
# @param format [Symbol] the format name
|
|
143
|
+
# @return [Symbol, nil] the type name or nil
|
|
144
|
+
def configured_type(format)
|
|
145
|
+
configured_types[format]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# --- Reset ---
|
|
149
|
+
|
|
150
|
+
# Clear all state (for testing reset).
|
|
151
|
+
#
|
|
152
|
+
# @return [void]
|
|
153
|
+
def reset!
|
|
154
|
+
@metadata = nil
|
|
155
|
+
@configured_types = nil
|
|
156
|
+
@resolved = nil
|
|
157
|
+
@detected_types = nil
|
|
158
|
+
@detected_formats = nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# Internal state accessors
|
|
164
|
+
|
|
165
|
+
def metadata
|
|
166
|
+
@metadata ||= {}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def configured_types
|
|
170
|
+
@configured_types ||= {}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def resolved
|
|
174
|
+
@resolved ||= {}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def detected_types
|
|
178
|
+
@detected_types ||= {}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def detected_formats
|
|
182
|
+
@detected_formats ||= {}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Load adapter and cache the result.
|
|
186
|
+
#
|
|
187
|
+
# @param format [Symbol] the format name
|
|
188
|
+
# @param type_name [Symbol] the adapter type name
|
|
189
|
+
# @return [Class] the adapter class
|
|
190
|
+
def load_and_cache(format, type_name)
|
|
191
|
+
resolved[format] ||= load_adapter(format, type_name)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Load an adapter file and resolve to its class.
|
|
195
|
+
#
|
|
196
|
+
# @param format [Symbol] the format name
|
|
197
|
+
# @param type_name [Symbol] the adapter type name
|
|
198
|
+
# @return [Class] the adapter class
|
|
199
|
+
def load_adapter(format, type_name)
|
|
200
|
+
adapter = format.to_s
|
|
201
|
+
type = normalize_type_name(type_name, format)
|
|
202
|
+
|
|
203
|
+
load_adapter_file(adapter, type)
|
|
204
|
+
load_moxml_adapter(type_name, format)
|
|
205
|
+
|
|
206
|
+
class_for(adapter, type)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Normalize type name to file/class name pattern.
|
|
210
|
+
#
|
|
211
|
+
# @param type_name [Symbol] raw type name
|
|
212
|
+
# @param format [Symbol] format name
|
|
213
|
+
# @return [String] normalized type name (e.g., "nokogiri_adapter")
|
|
214
|
+
def normalize_type_name(type_name, format)
|
|
215
|
+
if type_name.to_s.start_with?("multi_json")
|
|
216
|
+
"multi_json_adapter"
|
|
217
|
+
else
|
|
218
|
+
"#{type_name.to_s.gsub("_#{format}", '')}_adapter"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Load the adapter file.
|
|
223
|
+
#
|
|
224
|
+
# @param adapter [String] format name as string
|
|
225
|
+
# @param type [String] normalized type name
|
|
226
|
+
def load_adapter_file(adapter, type)
|
|
227
|
+
loader = FormatRegistry.adapter_loader_for(adapter.to_sym)
|
|
228
|
+
if loader.respond_to?(:load_adapter_file)
|
|
229
|
+
loader.load_adapter_file(adapter, type)
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Default key-value adapter loading
|
|
234
|
+
adapter_path = if RuntimeCompatibility.opal?
|
|
235
|
+
"lutaml/key_value/adapter/#{adapter}/#{type}"
|
|
236
|
+
else
|
|
237
|
+
File.join(File.dirname(__FILE__), "../key_value/adapter",
|
|
238
|
+
adapter, type)
|
|
239
|
+
end
|
|
240
|
+
require adapter_path
|
|
241
|
+
rescue LoadError
|
|
242
|
+
raise UnknownAdapterTypeError.new(adapter, type), cause: nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Load the Moxml adapter for XML and similar formats.
|
|
246
|
+
#
|
|
247
|
+
# @param type_name [Symbol] raw type name
|
|
248
|
+
# @param format [Symbol] format name
|
|
249
|
+
def load_moxml_adapter(type_name, format)
|
|
250
|
+
loader = FormatRegistry.adapter_loader_for(format)
|
|
251
|
+
if loader.respond_to?(:load_moxml_adapter)
|
|
252
|
+
loader.load_moxml_adapter(type_name,
|
|
253
|
+
format)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Resolve the adapter class from the type name.
|
|
258
|
+
#
|
|
259
|
+
# @param adapter [String] format name as string
|
|
260
|
+
# @param type [String] normalized type name
|
|
261
|
+
# @return [Class] the adapter class
|
|
262
|
+
def class_for(adapter, type)
|
|
263
|
+
loader = FormatRegistry.adapter_loader_for(adapter.to_sym)
|
|
264
|
+
if loader.respond_to?(:class_for)
|
|
265
|
+
return loader.class_for(adapter, type)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Default key-value adapter class resolution
|
|
269
|
+
KeyValue::Adapter.const_get(to_class_name(adapter))
|
|
270
|
+
.const_get(to_class_name(type))
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Validate that the adapter type is available for the format.
|
|
274
|
+
#
|
|
275
|
+
# @param format [Symbol] the format name
|
|
276
|
+
# @param type_name [Symbol] the adapter type name
|
|
277
|
+
# @raise [ArgumentError] if format or adapter is unknown
|
|
278
|
+
def validate!(format, type_name)
|
|
279
|
+
adapter_config = metadata[format]
|
|
280
|
+
|
|
281
|
+
unless adapter_config
|
|
282
|
+
all_formats = metadata.keys
|
|
283
|
+
available_formats = all_formats.map { |f| "`:#{f}`" }.join(", ")
|
|
284
|
+
raise ArgumentError,
|
|
285
|
+
"Unknown format: `:#{format}`. Available formats: #{available_formats}."
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
if format == :toml && type_name == :tomlib && RuntimeCompatibility.windows?
|
|
289
|
+
raise ArgumentError,
|
|
290
|
+
"The `:tomlib` adapter is not supported on Windows due to " \
|
|
291
|
+
"segmentation fault issues. Please use `:toml_rb` instead."
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
available = adapter_config[:available]
|
|
295
|
+
return unless available
|
|
296
|
+
return if available.include?(type_name)
|
|
297
|
+
|
|
298
|
+
available_list = available.map { |a| "`:#{a}`" }.join(", ")
|
|
299
|
+
closest = find_suggestion(type_name.to_s, available.map(&:to_s))
|
|
300
|
+
|
|
301
|
+
msg = "Unknown adapter: `:#{type_name}` for `:#{format}` format. " \
|
|
302
|
+
"Available adapters: #{available_list}."
|
|
303
|
+
msg += " Did you mean `:#{closest}`?" if closest
|
|
304
|
+
|
|
305
|
+
raise ArgumentError, msg
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Find closest string match for suggestions.
|
|
309
|
+
#
|
|
310
|
+
# @param input [String]
|
|
311
|
+
# @param candidates [Array<String>]
|
|
312
|
+
# @return [String, nil]
|
|
313
|
+
def find_suggestion(input, candidates)
|
|
314
|
+
return nil if input.nil? || input.empty?
|
|
315
|
+
|
|
316
|
+
candidates.min_by do |candidate|
|
|
317
|
+
levenshtein_distance(input.downcase, candidate.downcase)
|
|
318
|
+
end.tap do |closest|
|
|
319
|
+
max_dist = ([input.length, closest.length].max / 2) + 1
|
|
320
|
+
return nil if closest && levenshtein_distance(input.downcase,
|
|
321
|
+
closest.downcase) > max_dist
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Calculate Levenshtein distance.
|
|
326
|
+
#
|
|
327
|
+
# @param lhs [String]
|
|
328
|
+
# @param rhs [String]
|
|
329
|
+
# @return [Integer]
|
|
330
|
+
def levenshtein_distance(lhs, rhs)
|
|
331
|
+
return lhs.length if rhs.empty?
|
|
332
|
+
return rhs.length if lhs.empty?
|
|
333
|
+
|
|
334
|
+
matrix = Array.new(lhs.length + 1) do |i|
|
|
335
|
+
Array.new(rhs.length + 1) do |j|
|
|
336
|
+
if i.zero?
|
|
337
|
+
j
|
|
338
|
+
else
|
|
339
|
+
(j.zero? ? i : 0)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
(1..lhs.length).each do |i|
|
|
345
|
+
(1..rhs.length).each do |j|
|
|
346
|
+
cost = lhs[i - 1] == rhs[j - 1] ? 0 : 1
|
|
347
|
+
matrix[i][j] = [
|
|
348
|
+
matrix[i - 1][j] + 1,
|
|
349
|
+
matrix[i][j - 1] + 1,
|
|
350
|
+
matrix[i - 1][j - 1] + cost,
|
|
351
|
+
].min
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
matrix[lhs.length][rhs.length]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Auto-detect available adapter for a format.
|
|
359
|
+
#
|
|
360
|
+
# @param format [Symbol] the format name
|
|
361
|
+
# @return [Symbol, nil] the detected adapter type name
|
|
362
|
+
def detect_adapter_for(format)
|
|
363
|
+
case format
|
|
364
|
+
when :xml
|
|
365
|
+
detect_xml_adapter
|
|
366
|
+
when :toml
|
|
367
|
+
detect_toml_adapter
|
|
368
|
+
else
|
|
369
|
+
metadata.dig(format, :default)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Detect available XML adapter.
|
|
374
|
+
#
|
|
375
|
+
# @return [Symbol, nil] :nokogiri, :ox, :oga, :rexml, or nil
|
|
376
|
+
def detect_xml_adapter
|
|
377
|
+
return :oga if RuntimeCompatibility.opal?
|
|
378
|
+
return :nokogiri if Utils.safe_load("nokogiri", :Nokogiri)
|
|
379
|
+
return :ox if Utils.safe_load("ox", :Ox)
|
|
380
|
+
return :oga if Utils.safe_load("oga", :Oga)
|
|
381
|
+
return :rexml if Utils.safe_load("rexml", :REXML)
|
|
382
|
+
|
|
383
|
+
nil
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Detect available TOML adapter.
|
|
387
|
+
#
|
|
388
|
+
# @return [Symbol, nil] :tomlib, :toml_rb, or nil
|
|
389
|
+
def detect_toml_adapter
|
|
390
|
+
return nil if RuntimeCompatibility.opal?
|
|
391
|
+
|
|
392
|
+
if RuntimeCompatibility.windows?
|
|
393
|
+
return :toml_rb if Utils.safe_load("toml-rb", :TomlRb)
|
|
394
|
+
|
|
395
|
+
return nil
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
return :tomlib if Utils.safe_load("tomlib", :Tomlib)
|
|
399
|
+
return :toml_rb if Utils.safe_load("toml-rb", :TomlRb)
|
|
400
|
+
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def to_class_name(str)
|
|
405
|
+
str.to_s.split("_").map(&:capitalize).join
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
# Thread-local scoped adapter override stack.
|
|
6
|
+
#
|
|
7
|
+
# Provides block-scoped adapter overrides for testing and library stacking.
|
|
8
|
+
# Each thread has its own stack — no mutex needed.
|
|
9
|
+
#
|
|
10
|
+
# @example Testing with a specific adapter
|
|
11
|
+
# Lutaml::Model::Config.with_adapter(xml: :ox) do
|
|
12
|
+
# MyClass.from_xml(xml) # Uses Ox
|
|
13
|
+
# end
|
|
14
|
+
# # Outside the block, reverts to configured default
|
|
15
|
+
#
|
|
16
|
+
# @example Library stacking
|
|
17
|
+
# # Library A guarantees Ox internally
|
|
18
|
+
# def self.parse(data)
|
|
19
|
+
# Config.with_adapter(xml: :nokogiri) { MyModel.from_xml(data) }
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class AdapterScope
|
|
23
|
+
STACK_KEY = :__lutaml_adapter_scope_stack
|
|
24
|
+
EMPTY = {}.freeze
|
|
25
|
+
|
|
26
|
+
# Push adapter overrides and yield. Restores previous scope on exit.
|
|
27
|
+
#
|
|
28
|
+
# @param overrides [Hash{Symbol => Symbol}] format => adapter type name
|
|
29
|
+
# e.g., { xml: :ox, json: :oj }
|
|
30
|
+
# @yield block within which overrides are active
|
|
31
|
+
# @return [Object] the block's return value
|
|
32
|
+
def self.with(overrides)
|
|
33
|
+
stack = Thread.current[STACK_KEY] ||= []
|
|
34
|
+
stack.push(overrides)
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
stack.pop
|
|
38
|
+
Thread.current[STACK_KEY] = nil if stack.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return the current scope's overrides hash.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash{Symbol => Symbol}] current overrides or empty hash
|
|
44
|
+
def self.current
|
|
45
|
+
Thread.current[STACK_KEY]&.last || EMPTY
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return the override for a specific format from the current scope.
|
|
49
|
+
#
|
|
50
|
+
# @param format [Symbol] the format name (:xml, :json, etc.)
|
|
51
|
+
# @return [Symbol, nil] the adapter type name or nil
|
|
52
|
+
def self.override_for(format)
|
|
53
|
+
current[format]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Clear all scope state (for testing reset).
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
59
|
+
def self.reset!
|
|
60
|
+
Thread.current[STACK_KEY] = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/lutaml/model/config.rb
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
|
-
# Configuration module - single entry point for all configuration
|
|
5
|
+
# Configuration module - single entry point for all configuration.
|
|
6
|
+
#
|
|
7
|
+
# Delegates adapter resolution to AdapterResolver and scoped overrides
|
|
8
|
+
# to AdapterScope. Keeps the configure block API for backward compatibility.
|
|
6
9
|
module Config
|
|
7
10
|
extend self
|
|
8
11
|
|
|
9
|
-
# Bootstrap format lists for key-value adapter method definition.
|
|
10
|
-
# At runtime, prefer FormatRegistry.formats for dynamic format discovery.
|
|
11
|
-
# NOTE: XML is NOT listed here — it registers dynamically via FormatRegistry
|
|
12
|
-
# when `require "lutaml/xml"` is called.
|
|
13
|
-
AVAILABLE_FORMATS = %i[json jsonl yaml toml hash].freeze
|
|
14
|
-
KEY_VALUE_FORMATS = AVAILABLE_FORMATS
|
|
15
|
-
|
|
16
12
|
# Dynamic format discovery from FormatRegistry
|
|
17
13
|
def available_formats
|
|
18
14
|
FormatRegistry.formats
|
|
@@ -28,9 +24,26 @@ module Lutaml
|
|
|
28
24
|
@instance ||= Configuration.new
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
# Block-scoped adapter override (thread-safe).
|
|
28
|
+
#
|
|
29
|
+
# Pushes adapter overrides onto a thread-local stack for the duration
|
|
30
|
+
# of the block. Restores previous state on exit.
|
|
31
|
+
#
|
|
32
|
+
# @param overrides [Hash{Symbol => Symbol}] format => adapter type name
|
|
33
|
+
# @yield block within which overrides are active
|
|
34
|
+
# @return [Object] the block's return value
|
|
35
|
+
#
|
|
36
|
+
# @example Testing with a specific adapter
|
|
37
|
+
# Config.with_adapter(xml: :ox) do
|
|
38
|
+
# MyClass.from_xml(xml) # Uses Ox
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# @example Library stacking
|
|
42
|
+
# Config.with_adapter(xml: :nokogiri, toml: :tomlib) do
|
|
43
|
+
# MyModel.from_xml(data)
|
|
44
|
+
# end
|
|
45
|
+
def with_adapter(**overrides, &)
|
|
46
|
+
AdapterScope.with(overrides, &)
|
|
34
47
|
end
|
|
35
48
|
|
|
36
49
|
# Delegate configure to Configuration
|
|
@@ -39,24 +52,43 @@ module Lutaml
|
|
|
39
52
|
self
|
|
40
53
|
end
|
|
41
54
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
# Get adapter class for a format using the full resolution chain.
|
|
56
|
+
#
|
|
57
|
+
# @param format [Symbol] the format name
|
|
58
|
+
# @return [Class, nil] the adapter class or nil
|
|
59
|
+
def adapter_for(format)
|
|
60
|
+
AdapterResolver.adapter_for(format)
|
|
45
61
|
end
|
|
46
62
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
# Store a pre-resolved adapter class for a format.
|
|
64
|
+
#
|
|
65
|
+
# @param format [Symbol] the format name
|
|
66
|
+
# @param adapter [Class] the adapter class
|
|
67
|
+
def set_adapter_for(format, adapter)
|
|
68
|
+
AdapterResolver.set_adapter_class(format, adapter)
|
|
50
69
|
end
|
|
51
70
|
|
|
52
|
-
#
|
|
71
|
+
# Dynamic adapter type accessors for boot-time formats.
|
|
72
|
+
# Additional formats (xml, etc.) get their accessors registered
|
|
73
|
+
# via FormatRegistry.register which calls define_adapter_type_methods.
|
|
74
|
+
AVAILABLE_FORMATS = %i[json jsonl yaml toml hash yamls].freeze
|
|
75
|
+
KEY_VALUE_FORMATS = AVAILABLE_FORMATS
|
|
76
|
+
|
|
53
77
|
AVAILABLE_FORMATS.each do |format|
|
|
78
|
+
define_method(:"#{format}_adapter") do
|
|
79
|
+
AdapterResolver.adapter_for(format)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
define_method(:"#{format}_adapter=") do |adapter_klass|
|
|
83
|
+
AdapterResolver.set_adapter_class(format, adapter_klass)
|
|
84
|
+
end
|
|
85
|
+
|
|
54
86
|
define_method(:"#{format}_adapter_type=") do |type_name|
|
|
55
|
-
|
|
87
|
+
AdapterResolver.set_adapter_type(format, type_name)
|
|
56
88
|
end
|
|
57
89
|
|
|
58
90
|
define_method(:"#{format}_adapter_type") do
|
|
59
|
-
|
|
91
|
+
AdapterResolver.configured_type(format)
|
|
60
92
|
end
|
|
61
93
|
end
|
|
62
94
|
|
|
@@ -84,7 +116,38 @@ module Lutaml
|
|
|
84
116
|
instance.default_context_id = value
|
|
85
117
|
end
|
|
86
118
|
|
|
87
|
-
#
|
|
119
|
+
# Define dynamic adapter type accessor methods for a format.
|
|
120
|
+
# Called by FormatRegistry.register when a new format is registered.
|
|
121
|
+
#
|
|
122
|
+
# Defines two pairs of methods:
|
|
123
|
+
# - #{format}_adapter / #{format}_adapter= — adapter class getter/setter
|
|
124
|
+
# - #{format}_adapter_type / #{format}_adapter_type= — type name getter/setter
|
|
125
|
+
#
|
|
126
|
+
# @param format [Symbol] the format name
|
|
127
|
+
def define_adapter_type_methods(format)
|
|
128
|
+
return if respond_to?(:"#{format}_adapter_type=")
|
|
129
|
+
|
|
130
|
+
# Adapter class getter (returns Class)
|
|
131
|
+
define_method(:"#{format}_adapter") do
|
|
132
|
+
AdapterResolver.adapter_for(format)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Adapter class setter (accepts Class)
|
|
136
|
+
define_method(:"#{format}_adapter=") do |adapter_klass|
|
|
137
|
+
AdapterResolver.set_adapter_class(format, adapter_klass)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Adapter type name setter (accepts Symbol like :nokogiri)
|
|
141
|
+
define_method(:"#{format}_adapter_type=") do |type_name|
|
|
142
|
+
AdapterResolver.set_adapter_type(format, type_name)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Adapter type name getter (returns Symbol or nil)
|
|
146
|
+
define_method(:"#{format}_adapter_type") do
|
|
147
|
+
AdapterResolver.configured_type(format)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
88
151
|
def to_class_name(str)
|
|
89
152
|
str.to_s.split("_").map(&:capitalize).join
|
|
90
153
|
end
|