Module: Lutaml::Model::Serialize::FormatConversion
- Included in:
- ClassMethods
- Defined in:
- lib/lutaml/model/serialize/format_conversion.rb
Overview
Handles format conversion methods for Serialize::ClassMethods
Extracted from serialize.rb to improve code organization. Provides methods for serializing/deserializing between formats.
Instance Method Summary collapse
-
#array_passthrough_format?(format) ⇒ Boolean
Whether this format+model combination requires the parsed array to pass through to the transformer as a whole (not split per element).
-
#as(format, instance, options = {}) ⇒ Object
Convert a model instance to format-specific data structure.
- #declares?(document, predicate) ⇒ Boolean
-
#default_mappings(format) ⇒ Mapping
Generate default mappings for a format.
-
#format_error_types ⇒ Array<Class>
Get list of error types that can be raised during format parsing.
-
#forward_options(document, generator_state, options) ⇒ Object
Main's behaviour differs per ADAPTER, not per option, so this follows the adapter rather than trying to translate option names: stdlib honours script_safe / ascii_only / pretty -> give it the state Oj ignores them all and uses its own escape_mode -> give it none others reach the stdlib generator underneath -> give them the options.
-
#from(format, data, options = {}) ⇒ Object
Deserialize from a format.
-
#key_value(&block) ⇒ Object
Define key-value mappings for multiple formats.
-
#mappings_for(format, register = nil) ⇒ Mapping?
Get resolved mapping for a format.
-
#of(format, doc, options = {}) ⇒ Object
Create a model instance from a parsed document.
-
#post_process_mapping(_format) ⇒ Object
Hook for format-specific post-processing after mapping DSL evaluation.
-
#pre_deserialize_hook(_format, _register) ⇒ Object
Hook for format-specific pre-deserialization logic.
-
#pre_serialize_hook(_format, _register) ⇒ Object
Hook for format-specific pre-serialization logic.
-
#prepare_to_options(_format, _instance, options) ⇒ Hash
Hook for format-specific options preparation before serialization.
-
#process_mapping(format, *_args, &block) ⇒ Object
Process mapping DSL for a format.
-
#rdf(&block) ⇒ Object
Define RDF mappings for multiple formats (Turtle, JSON-LD, etc.).
-
#reset_format_error_types_cache! ⇒ Object
Reset cached error types (for test isolation).
-
#to(format, instance, options = {}) ⇒ String
Serialize a model instance to a format.
-
#validate_document(_format, _doc, _options, _register) ⇒ Object
Hook for format-specific document validation.
-
#xml_plan_fast_path(data, options) ⇒ Object
Whole-document native materialization (Phase 5): compile the mapping into a Leptris descriptor plan and hydrate from one plan walk.
-
#xml_plan_fast_serialize(instance, options) ⇒ Object
Serialize-side plan fast path: same compiled plan, direct leptris construction.
Instance Method Details
#array_passthrough_format?(format) ⇒ Boolean
Whether this format+model combination requires the parsed array to pass through to the transformer as a whole (not split per element). YAMLS with sequence definitions needs the full document array.
235 236 237 238 239 240 241 242 243 244 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 235 def array_passthrough_format?(format) return true if format == :jsonl if format == :yamls mapping = mappings[format] return true if mapping.is_a?(Lutaml::Yamls::Adapter::Mapping) && mapping.yamls_sequence end false end |
#as(format, instance, options = {}) ⇒ Object
Convert a model instance to format-specific data structure
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 334 def as(format, instance, = {}) if instance.is_a?(Array) return instance.map { |item| public_send(:"as_#{format}", item) } end unless instance.is_a?(model) msg = "argument is a '#{instance.class}' but should be a '#{model}'" raise Lutaml::Model::IncorrectModelError, msg end # Resolve imports at the start of serialization register = [:register] || Lutaml::Model::Config.default_register # Hook for format-specific pre-serialization (e.g., XML mapping import resolution) pre_serialize_hook(format, register) # Recursively resolve child model imports ensure_child_imports_resolved!(register) transformer = Lutaml::Model::Config.transformer_for(format) transformer.model_to_data(self, instance, format, ) end |
#declares?(document, predicate) ⇒ Boolean
313 314 315 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 313 def declares?(document, predicate) document.respond_to?(predicate) && document.public_send(predicate) end |
#default_mappings(format) ⇒ Mapping
Generate default mappings for a format
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 420 def default_mappings(format) klass = ::Lutaml::Model::Config.mappings_class_for(format) mappings = klass.new mappings.tap do |mapping| attributes&.each_key do |name| mapping.map_element( name.to_s, to: name, ) end # DO NOT auto-generate root element for XML # Models without an explicit xml block should be type-only models # If a root element is needed, declare it explicitly in xml block end end |
#format_error_types ⇒ Array<Class>
Get list of error types that can be raised during format parsing. Core errors are always included; format-specific errors come from FormatRegistry registrations.
Performance: Cached base types + lazy TOML lookup. Tomlib::ParseError is lazily loaded, so we check for it on each call rather than caching a stale nil reference.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 151 def format_error_types @format_error_types_base ||= begin errors = [ Lutaml::Model::RuntimeCompatibility.safe_constantize("Psych::SyntaxError"), Lutaml::Model::RuntimeCompatibility.safe_constantize("JSON::ParserError"), NoMethodError, TypeError, ArgumentError, ] # Collect format-specific error types from FormatRegistry compatibility = Lutaml::Model::RuntimeCompatibility FormatRegistry.all.each_value do |info| next unless info[:error_types] info[:error_types].each do |error_class| cls = if error_class.is_a?(String) compatibility.safe_constantize(error_class) else error_class end errors << cls end end errors.compact.freeze end # Legacy TOML error types are lazy, so check them on each call. compatibility = Lutaml::Model::RuntimeCompatibility toml_errors = compatibility.safe_constantize("TomlRB::ParseError") toml_errors = Array(toml_errors) tomllib_err = compatibility.safe_constantize("Tomlib::ParseError") toml_errors << tomllib_err if tomllib_err teptris_err = compatibility.safe_constantize("Teptris::ParseError") toml_errors << teptris_err if teptris_err @format_error_types_base + toml_errors end |
#forward_options(document, generator_state, options) ⇒ Object
Main's behaviour differs per ADAPTER, not per option, so this follows the adapter rather than trying to translate option names:
stdlib honours script_safe / ascii_only / pretty -> give it the state
Oj ignores them all and uses its own escape_mode -> give it none
others reach the stdlib generator underneath -> give them the
299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 299 def (document, generator_state, ) return if generator_state.nil? if declares?(document, :accepts_generator_state?) generator_state elsif declares?(document, :ignores_generator_options?) elsif generator_state.respond_to?(:to_h) .merge(generator_state.to_h) else end end |
#from(format, data, options = {}) ⇒ Object
Deserialize from a format
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 47 def from(format, data, = {}) if format == :xml && Lutaml::Model::Config.instance.xml_plan_fast_path fast = xml_plan_fast_path(data, ) return fast if fast end # The rescue sits on the parse block, not the method, so cache # store failures propagate instead of becoming InvalidFormatError. with_conversion_cache(:from, format, data, ) do Instrumentation.instrument(:from, model: name, format: format) do adapter = resolve_adapter(format, .delete(:adapter)) raise Lutaml::Model::FormatAdapterNotSpecifiedError.new(format) if adapter.nil? # Resolve imports at the entry point of deserialization register = [:register] || Lutaml::Model::Config.default_register # Hook for format-specific pre-deserialization (e.g., XML mapping import resolution) pre_deserialize_hook(format, register) # Recursively resolve child model imports # This ensures the entire model tree is finalized before parsing ensure_child_imports_resolved!(register) doc = adapter.parse(data, ) send("of_#{format}", doc, ) end rescue *format_error_types => e raise Lutaml::Model::InvalidFormatError.new(format, e.) end end |
#key_value(&block) ⇒ Object
Define key-value mappings for multiple formats. Uses FormatRegistry to discover key-value formats dynamically, falling back to Config::KEY_VALUE_FORMATS for bootstrap.
371 372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 371 def key_value(&block) formats = if FormatRegistry.formats.any? FormatRegistry.key_value_formats else Lutaml::Model::Config::KEY_VALUE_FORMATS end formats.each do |format| mappings[format] ||= Lutaml::KeyValue::Mapping.new(format) mappings[format].instance_eval(&block) mappings[format].finalize(self) end end |
#mappings_for(format, register = nil) ⇒ Mapping?
Get resolved mapping for a format
Delegates to TransformationRegistry for centralized caching (Single Source of Truth - Phase 11.5).
Register resolution: If the caller passes a parent register (e.g., :default)
but this class declares its own lutaml_default_register, the child's
register takes precedence. This ensures mappings are resolved in the
correct context for cross-register embedding.
409 410 411 412 413 414 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 409 def mappings_for(format, register = nil) resolved_register = Lutaml::Model::Register.resolve_for_child(self, register) TransformationRegistry.instance.get_or_build_mapping(self, format, resolved_register) end |
#of(format, doc, options = {}) ⇒ Object
Create a model instance from a parsed document
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 202 def of(format, doc, = {}) if doc.is_a?(Array) && !array_passthrough_format?(format) return doc.map { |item| send(:"of_#{format}", item) } end register = extract_register_id([:register]) # Hook for format-specific document validation (e.g., XML root/encoding/doctype) validate_document(format, doc, , register) [:register] = register transformer = Lutaml::Model::Config.transformer_for(format) transformer.data_to_model(self, doc, format, ) end |
#post_process_mapping(_format) ⇒ Object
Hook for format-specific post-processing after mapping DSL evaluation. XML overrides this to call check_sort_configs!.
37 38 39 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 37 def post_process_mapping(_format) # No-op by default; XML overrides via prepend end |
#pre_deserialize_hook(_format, _register) ⇒ Object
Hook for format-specific pre-deserialization logic. XML overrides to resolve XML mapping imports.
84 85 86 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 84 def pre_deserialize_hook(_format, _register) # No-op by default; XML overrides via prepend end |
#pre_serialize_hook(_format, _register) ⇒ Object
Hook for format-specific pre-serialization logic. XML overrides to resolve XML mapping imports.
362 363 364 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 362 def pre_serialize_hook(_format, _register) # No-op by default; XML overrides via prepend end |
#prepare_to_options(_format, _instance, options) ⇒ Hash
Hook for format-specific options preparation before serialization. XML overrides to handle prefix, namespace overrides, declaration plan.
324 325 326 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 324 def (_format, _instance, ) end |
#process_mapping(format, *_args, &block) ⇒ Object
Process mapping DSL for a format
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 16 def process_mapping(format, *_args, &block) klass = ::Lutaml::Model::Config.mappings_class_for(format) existing = mappings[format] mappings[format] = if existing.nil? || !existing.is_a?(klass) klass.new else existing end mappings[format].instance_eval(&block) if mappings[format].is_a?(Lutaml::Xml::Mapping) mappings[format].finalize(self) end post_process_mapping(format) end |
#rdf(&block) ⇒ Object
Define RDF mappings for multiple formats (Turtle, JSON-LD, etc.). Discovers RDF formats dynamically from FormatRegistry.
389 390 391 392 393 394 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 389 def rdf(&block) Lutaml::Model::FormatRegistry.rdf_formats.each do |format| mappings[format] = Lutaml::Rdf::Mapping.new mappings[format].instance_eval(&block) end end |
#reset_format_error_types_cache! ⇒ Object
Reset cached error types (for test isolation)
192 193 194 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 192 def reset_format_error_types_cache! @format_error_types_base = nil end |
#to(format, instance, options = {}) ⇒ String
Serialize a model instance to a format
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 259 def to(format, instance, = {}) # The rescue sits on the parse block, not the method, so cache # store failures propagate instead of becoming InvalidFormatError. with_conversion_cache(:to, format, instance, ) do # Ruby's JSON generator hands #to_json its own JSON::State rather # than an options hash. It carries no LutaML options, but it does # carry the surrounding indent context, so it is forwarded to the # adapter unchanged instead of being read like a Hash -- json 3.0 # removed JSON::State#[]. = Serialize.wrap_generator_state() generator_state = .delete(Serialize::GENERATOR_STATE_KEY) Instrumentation.instrument(:to, model: name, format: format) do adapter_override = .delete(:adapter) [:_adapter_override] = true if adapter_override if format == :xml && Lutaml::Model::Config.instance.xml_plan_fast_path fast = xml_plan_fast_serialize(instance, ) return fast if fast end value = public_send(:"as_#{format}", instance, ) adapter = resolve_adapter(format, adapter_override) # Hook for format-specific options preparation (e.g., XML prefix/namespace/declaration) = (format, instance, ) document = adapter.new(value, register: [:register]) document.public_send( :"to_#{format}", (document, generator_state, ), ) end end end |
#validate_document(_format, _doc, _options, _register) ⇒ Object
Hook for format-specific document validation. XML overrides to validate root mapping and extract encoding/doctype.
225 226 227 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 225 def validate_document(_format, _doc, , _register) # No-op by default; XML overrides via prepend end |
#xml_plan_fast_path(data, options) ⇒ Object
Whole-document native materialization (Phase 5): compile the mapping into a Leptris descriptor plan and hydrate from one plan walk. Only when the resolved XML adapter is leptris- backed, the model fully compiles, and no path-affecting options are present; anything else falls back to the interpretive pipeline.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 117 def xml_plan_fast_path(data, ) return nil unless defined?(::Leptris::XML::Descriptor) return nil if .key?(:adapter) || .key?(:only) || .key?(:except) || .key?(:mappings) || .key?(:register) adapter_name = Lutaml::Model::Config.adapter_for(:xml) adapter_name = adapter_name&.name return nil unless adapter_name.to_s.end_with?("LeptrisAdapter") register = Lutaml::Model::Config.default_register plan = Lutaml::Xml::PlanCompiler.compile(self, register) return nil unless plan root = ::Leptris::XML.parse(data.to_s).root return nil if root.nil? return nil unless root.name == plan[:tree][:name] Lutaml::Xml::PlanHydrator.call(self, plan, plan[:descriptor].walk(root), node: root) rescue ::Leptris::XML::ParseError => e raise Lutaml::Model::InvalidFormatError.new(:xml, e.) end |
#xml_plan_fast_serialize(instance, options) ⇒ Object
Serialize-side plan fast path: same compiled plan, direct leptris construction. Serialize-shaped plans only — custom methods, polymorphism, unions, and spellings keep the interpretive serializer.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/lutaml/model/serialize/format_conversion.rb', line 92 def xml_plan_fast_serialize(instance, ) return nil unless defined?(::Leptris::XML::Document) return nil if .key?(:only) || .key?(:except) || .key?(:mappings) || .key?(:adapter) || .key?(:_adapter_override) || .key?(:indent) || .key?(:xml_declaration) || .key?(:declaration) || .key?(:doctype) adapter_name = Lutaml::Model::Config.adapter_for(:xml) adapter_name &&= adapter_name.name return nil unless adapter_name.to_s.end_with?("LeptrisAdapter") register = Lutaml::Model::Config.default_register plan = Lutaml::Xml::PlanCompiler.compile(self, register) return nil unless plan && Lutaml::Xml::PlanSerializer.serializable?(plan) Lutaml::Xml::PlanSerializer.call(instance, plan) end |