Module: Lutaml::Model::Serialize

Includes:
ComparableModel, Liquefiable, Registrable, Builder, Validation
Included in:
Serializable
Defined in:
lib/lutaml/model/serialize.rb,
lib/lutaml/model/serialize/builder.rb,
lib/lutaml/model/serialize/model_import.rb,
lib/lutaml/model/serialize/enum_handling.rb,
lib/lutaml/model/serialize/value_mapping.rb,
lib/lutaml/model/serialize/initialization.rb,
lib/lutaml/model/serialize/format_conversion.rb,
lib/lutaml/model/serialize/conversion_caching.rb,
lib/lutaml/model/serialize/attribute_definition.rb,
lib/lutaml/model/serialize/transformation_builder.rb,
lib/lutaml/model/serialize/deserialization_context.rb

Defined Under Namespace

Modules: AttributeDefinition, Builder, ClassMethods, ConversionCaching, EnumHandling, FormatConversion, Initialization, ModelImport, TransformationBuilder, ValueMapping Classes: DeserializationContext

Constant Summary collapse

DEFAULT_VALUE_MAP =

Performance: Pre-computed default value map to avoid per-call allocations

{
  omitted: :nil,
  nil: :nil,
  empty: :empty,
}.freeze
LAZY_EMPTY_COLLECTION =

Shared frozen sentinel for lazy collection initialization. The getter materializes a real Array on first access.

[].freeze
LAZY_IVAR =

Ivar symbols for lazy collections, memoized per attribute name: the "@name" interpolation allocated twice per collection read (491k on the ISO-13849 serialize profile).

{}.compare_by_identity
NO_ARG =

Sentinel distinguishing "getter called with no argument" from builder-syntax g.attr(value) where value may be nil. Compiled getters default to it instead of a *args splat.

Object.new.freeze
INTERNAL_ATTRIBUTES =
%i[@using_default @lutaml_register @lutaml_parent @lutaml_root
@register_records].freeze
GENERATOR_STATE_KEY =

Ruby's JSON generator hands #to_json its own JSON::State instead of an options hash. It carries no LutaML options, but it does carry the surrounding indent context, so it travels IN BAND under a private key rather than replacing the options hash. Wrapping rather than returning early keeps every later step -- register propagation, root-mapping validation, Collection's collection: true merge -- working on a real Hash. json 3.0 removed JSON::State#[] and rejects unknown keys in State#merge, so nothing may treat it as a Hash.

:_generator_state

Constants included from Validation

Validation::VALIDATING_KEY

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Liquefiable

#to_liquid

Methods included from Validation

#collect_validation_errors, #element_order, #format_element_sequences, new_registry, #order_names, #validate, validate, validate!, #validate!, #validate_helper, #validate_sequence!, visiting?

Methods included from ComparableModel

#already_compared?, #attributes_hash, #calculate_hash, #comparison_key, #eql?, #hash, #same_class?

Methods included from Builder

#mixed_content?, #order_tracking_enabled?, #ordered?, #record_mutation, #record_mutation_collection

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/lutaml/model/serialize.rb', line 229

def method_missing(method_name, *args, &block)
  if method_name.to_s.end_with?("=") && attribute_exist?(method_name)
    define_singleton_method(method_name) do |value|
      instance_variable_set(:"@#{method_name.to_s.chomp('=')}", value)
    end
    send(method_name, *args)
  elsif ::Lutaml::Model::Attribute.default_evaluation? &&
      self.class.respond_to?(method_name)
    self.class.public_send(method_name, *args, &block)
  else
    super
  end
end

Instance Attribute Details

#lutaml_parentObject

Returns the value of attribute lutaml_parent.



143
144
145
# File 'lib/lutaml/model/serialize.rb', line 143

def lutaml_parent
  @lutaml_parent
end

#lutaml_registerObject

Returns the value of attribute lutaml_register.



143
144
145
# File 'lib/lutaml/model/serialize.rb', line 143

def lutaml_register
  @lutaml_register
end

#lutaml_rootObject

Returns the value of attribute lutaml_root.



143
144
145
# File 'lib/lutaml/model/serialize.rb', line 143

def lutaml_root
  @lutaml_root
end

Class Method Details

.included(base) ⇒ Object



49
50
51
52
# File 'lib/lutaml/model/serialize.rb', line 49

def self.included(base)
  base.extend(ClassMethods)
  base.initialize_attrs(base)
end

.register_format_mapping_method(format) ⇒ Object



68
69
70
71
72
73
74
# File 'lib/lutaml/model/serialize.rb', line 68

def self.register_format_mapping_method(format)
  method_name = format == :hash ? :hsh : format

  ::Lutaml::Model::Serialize::ClassMethods.define_method(method_name) do |*args, &block|
    process_mapping(format, *args, &block)
  end
end

.register_from_format_method(format) ⇒ Object



76
77
78
79
80
81
82
83
84
# File 'lib/lutaml/model/serialize.rb', line 76

def self.register_from_format_method(format)
  ClassMethods.define_method(:"from_#{format}") do |data, options = {}|
    from(format, data, options)
  end

  ClassMethods.define_method(:"of_#{format}") do |doc, options = {}|
    of(format, doc, options)
  end
end

.register_stream_methods(format) ⇒ Object

lutaml-model#436: multi-document streams. from_<f>_stream parses each document separately; discriminator: receives each parsed document hash and returns the model class to build with, enabling polymorphic streams. to_<f>_stream joins instances into one stream document.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/lutaml/model/serialize.rb', line 91

def self.register_stream_methods(format)
  ClassMethods.define_method(:"from_#{format}_stream") do |data, options = {}|
    docs = Lutaml::Model::Serialize.stream_documents(data, format, options)
    discriminator = options[:discriminator]
    docs.map do |doc|
      target = discriminator ? discriminator.call(doc) : self
      target.send(:"of_#{format}", doc, options)
    end
  end

  ClassMethods.define_method(:"to_#{format}_stream") do |instances, options = {}|
    instances.map do |instance|
      instance.send(:"to_#{format}", options)
    end.join("---\n")
  end
end

.register_to_format_method(format) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/lutaml/model/serialize.rb', line 129

def self.register_to_format_method(format)
  ClassMethods.define_method(:"to_#{format}") do |instance, options = {}|
    to(format, instance, options)
  end

  ClassMethods.define_method(:"as_#{format}") do |instance, options = {}|
    as(format, instance, options)
  end

  define_method(:"to_#{format}") do |options = {}|
    to_format(format, options)
  end
end

.stream_documents(data, format, options) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/lutaml/model/serialize.rb', line 108

def self.stream_documents(data, format, options)
  require "yaml"
  if YAML.respond_to?(:load_stream, true)
    begin
      docs = YAML.load_stream(data,
                              aliases: options.fetch(:aliases, true))
    rescue ArgumentError
      # Older Psych builds without the aliases keyword: aliases are
      # on by default there.
      docs = YAML.load_stream(data)
    end
    docs.is_a?(Array) ? docs.compact : [docs].compact
  else
    data.split(/^---\s*$/).filter_map do |chunk|
      chunk.strip.empty? ? nil : YAML.safe_load(chunk)
    end
  end
rescue StandardError => e
  raise Lutaml::Model::InvalidFormatError.new(format, e.message)
end

.wrap_generator_state(options) ⇒ Object



289
290
291
292
293
# File 'lib/lutaml/model/serialize.rb', line 289

def self.wrap_generator_state(options)
  return options if options.is_a?(::Hash)

  { GENERATOR_STATE_KEY => options }
end

Instance Method Details

#attr_value(attrs, name, attribute) ⇒ Object



199
200
201
202
203
# File 'lib/lutaml/model/serialize.rb', line 199

def attr_value(attrs, name, attribute)
  value = Utils.fetch_str_or_sym(attrs, name,
                                 attribute.default(lutaml_register, self))
  attribute.cast_value(value, lutaml_register)
end

#attribute_exist?(name) ⇒ Boolean

Returns:

  • (Boolean)


248
249
250
251
252
# File 'lib/lutaml/model/serialize.rb', line 248

def attribute_exist?(name)
  name = name.to_s.chomp("=").to_sym if name.end_with?("=")

  self.class.attributes(lutaml_register).key?(name)
end

#define_singleton_attribute_methodsObject

Ensure register-specific attribute methods are defined on the class. Delegates to the class method which defines methods once per (class, register) combination instead of per-instance singleton methods.



332
333
334
# File 'lib/lutaml/model/serialize.rb', line 332

def define_singleton_attribute_methods
  self.class.ensure_register_methods_defined(lutaml_register)
end

#extract_register_id(attrs, options) ⇒ Object



181
182
183
184
# File 'lib/lutaml/model/serialize.rb', line 181

def extract_register_id(attrs, options)
  register = attrs&.dig(:lutaml_register) || options&.dig(:register)
  self.class.extract_register_id(register)
end

#finalize_deserialization(register) ⇒ Object

Complete deserialization initialization after allocation. Called by allocate_for_deserialization to set up instance state, define register-specific methods, and register in the reference store.



175
176
177
178
179
# File 'lib/lutaml/model/serialize.rb', line 175

def finalize_deserialization(register)
  init_deserialization_state(register)
  define_singleton_attribute_methods
  register_in_reference_store
end

#init_deserialization_state(register) ⇒ Object

Initialize instance state for fast deserialization path. Called by allocate_for_deserialization instead of initialize. Uses nil for @using_default to mean "all attributes use default" — no hash allocation needed until value_set_for is called.



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/lutaml/model/serialize.rb', line 160

def init_deserialization_state(register)
  @using_default = nil
  @lutaml_register = register

  # Seed every attribute through the per-(class, register) compiled
  # method: plain `@x = sentinel` writes, no per-call "@#{name}"
  # string interpolation, no instance_variable_set. The previous
  # generic walk interpolated a name string per attribute per
  # instance — 581k allocations on the ISO-13849 profile.
  public_send(self.class.compiled_state_defaults_name!(self.class.extract_register_id(register)))
end

#initialize(attrs = {}, options = {}) ⇒ Object



145
146
147
148
149
150
151
152
153
154
# File 'lib/lutaml/model/serialize.rb', line 145

def initialize(attrs = {}, options = {})
  @using_default = {}
  @lutaml_register = extract_register_id(attrs, options)
  return unless self.class.attributes(@lutaml_register)

  initialize_attributes(attrs, options)
  define_singleton_attribute_methods

  register_in_reference_store
end

#key_exist?(hash, key) ⇒ Boolean

Returns:

  • (Boolean)


260
261
262
# File 'lib/lutaml/model/serialize.rb', line 260

def key_exist?(hash, key)
  hash.key?(key.to_sym) || hash.key?(key.to_s)
end

#key_value(hash, key) ⇒ Object



264
265
266
# File 'lib/lutaml/model/serialize.rb', line 264

def key_value(hash, key)
  hash[key.to_sym] || hash[key.to_s]
end

#prepare_instance_format_options(_format, _options) ⇒ Object

Hook for format-specific instance-level options preparation. XML overrides via InstanceMethods prepend.

Parameters:

  • _format (Symbol)

    The format

  • _options (Hash)

    Options hash (modified in place)



316
317
318
# File 'lib/lutaml/model/serialize.rb', line 316

def prepare_instance_format_options(_format, _options)
  # No-op by default
end

#pretty_print_instance_variablesObject



268
269
270
271
272
273
# File 'lib/lutaml/model/serialize.rb', line 268

def pretty_print_instance_variables
  reference_attributes = instance_variables.select do |var|
    var.to_s.end_with?("_ref")
  end
  (instance_variables - INTERNAL_ATTRIBUTES - reference_attributes).sort
end

#register_in_reference_storeObject



336
337
338
# File 'lib/lutaml/model/serialize.rb', line 336

def register_in_reference_store
  Lutaml::Model::Store.register(self) if self.class.reference_resolvable?
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


243
244
245
246
# File 'lib/lutaml/model/serialize.rb', line 243

def respond_to_missing?(method_name, include_private = false)
  (method_name.to_s.end_with?("=") && attribute_exist?(method_name)) ||
    super
end

#to_format(format, options = {}) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/lutaml/model/serialize.rb', line 295

def to_format(format, options = {})
  options = Lutaml::Model::Serialize.wrap_generator_state(options)

  # Hook for format-specific validation (e.g., XML root mapping check)
  validate_root_mapping!(format, options)

  # Pass instance's lutaml_register if not explicitly provided
  options[:register] ||= lutaml_register if lutaml_register

  # Hook for format-specific options preparation
  # XML overrides to handle prefix, doctype, declaration, namespaces
  prepare_instance_format_options(format, options)

  self.class.to(format, self, options)
end

#to_yaml_hashObject



275
276
277
# File 'lib/lutaml/model/serialize.rb', line 275

def to_yaml_hash
  self.class.as_yaml(self)
end

#using_default?(attribute_name) ⇒ Boolean

Returns:

  • (Boolean)


222
223
224
225
226
227
# File 'lib/lutaml/model/serialize.rb', line 222

def using_default?(attribute_name)
  # nil means "all attributes using default" — return true without allocation
  return true if @using_default.nil?

  @using_default[attribute_name]
end

#using_default_for(attribute_name) ⇒ Object



205
206
207
208
# File 'lib/lutaml/model/serialize.rb', line 205

def using_default_for(attribute_name)
  @using_default ||= ::Hash.new(true)
  @using_default[attribute_name] = true
end

#validate_attribute!(attr_name) ⇒ Object



254
255
256
257
258
# File 'lib/lutaml/model/serialize.rb', line 254

def validate_attribute!(attr_name)
  attr = self.class.attributes[attr_name]
  value = instance_variable_get(:"@#{attr_name}")
  attr.validate_value!(value, lutaml_register, instance_object: self)
end

#validate_root_mapping!(_format, _options) ⇒ Object

Hook for format-specific root mapping validation. XML overrides via InstanceMethods prepend.

Parameters:

  • _format (Symbol)

    The format

  • _options (Hash)

    Options hash



325
326
327
# File 'lib/lutaml/model/serialize.rb', line 325

def validate_root_mapping!(_format, _options)
  # No-op by default
end

#value_map(options) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/lutaml/model/serialize.rb', line 186

def value_map(options)
  # Fast path: return default map if no custom options
  return DEFAULT_VALUE_MAP if options.equal?(Type::Value::EMPTY_OPTIONS)
  return DEFAULT_VALUE_MAP if options.empty?

  # Slow path: merge with custom options
  {
    omitted: options[:omitted] || :nil,
    nil: options[:nil] || :nil,
    empty: options[:empty] || :empty,
  }
end

#value_set_for(attribute_name) ⇒ Object



210
211
212
213
214
215
# File 'lib/lutaml/model/serialize.rb', line 210

def value_set_for(attribute_name)
  # Only allocate hash when transitioning from "all defaults" (nil)
  # Hash.new(true) ensures unset keys still return true
  @using_default ||= ::Hash.new(true)
  @using_default[attribute_name] = false
end

#values_set_for(attribute_names) ⇒ Object



217
218
219
220
# File 'lib/lutaml/model/serialize.rb', line 217

def values_set_for(attribute_names)
  @using_default ||= ::Hash.new(true)
  attribute_names.each { |name| @using_default[name] = false }
end