Module: Lutaml::Model::Serialize::AttributeDefinition

Included in:
ClassMethods
Defined in:
lib/lutaml/model/serialize/attribute_definition.rb

Overview

Handles attribute definition methods for Serialize::ClassMethods

Extracted from serialize.rb to improve code organization. Provides methods for defining and validating model attributes.

Constant Summary collapse

PLAIN_NAME =

Define regular (non-reference, non-enum) attribute methods

Plain identifier names compile to @name reads; punctuation names (mixed?) keep the reflective path (@name is not valid Ruby for them).

/\A[a-zA-Z_][a-zA-Z0-9_]*\z/

Instance Method Summary collapse

Instance Method Details

#any_importable_models?Boolean

Check if there are any importable models



389
390
391
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 389

def any_importable_models?
  importable_choices.any? || importable_models.any?
end

#attribute(name, type, options = {}) ⇒ Attribute

Define an attribute for the model



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 341

def attribute(name, type, options = {})
  type, options = process_type_hash(type, options) if type.is_a?(::Hash)

  if type.is_a?(::Array)
    options = options.merge(union_member_types: type)
    type = Lutaml::Model::Type::Union
  end

  # Handle direct method option in options hash
  if options[:method]
    options[:method_name] = options.delete(:method)
  end

  attr = Attribute.new(name, type, options)
  @attributes[name] = attr
  @merged_attributes_cache = nil
  invalidate_state_defaults!
  define_attribute_methods(attr)

  attr
end

#compile_state_defaults!(method_name, register_id = nil) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 37

def compile_state_defaults!(method_name, register_id = nil)
  attrs = attributes(register_id)

  if attrs.empty?
    define_method(method_name) do
      # no attributes to seed
    end
    return
  end

  lines = attrs.map do |name, attr|
    if attr.collection?
      "@#{name} = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION"
    else
      "@#{name} = Lutaml::Model::UninitializedClass.instance"
    end
  end.join("\n")

  # class_eval interpolates per-attribute `@name = <sentinel>` lines:
  #   def __init_state_defaults_default
  #     @id = Lutaml::Model::UninitializedClass.instance
  #     @items = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION
  #   end
  class_eval("    def \#{method_name}\n    \#{lines}\n    end\n  RUBY\nend\n", __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition

#compiled_state_defaults_name!(register_id) ⇒ Object

Define attribute methods on the model class

Compile (per class, cached) the method that seeds every attribute with its "no data arrived" sentinel: collections share the frozen LAZY_EMPTY_COLLECTION, scalars share the UninitializedClass singleton. Compiled once instead of walked per instance — the grammars-compile-don't-interpret rule applied to object state. Compile (per class and register, on demand) the method that seeds every attribute with its "no data arrived" sentinel: collections share the frozen LAZY_EMPTY_COLLECTION, scalars share the UninitializedClass singleton. Compiled once instead of walked per instance — grammars compile, they don't interpret.



26
27
28
29
30
31
32
33
34
35
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 26

def compiled_state_defaults_name!(register_id)
  @state_defaults_names ||= {}
  name = @state_defaults_names[register_id]
  return name if name

  method_name = :"__init_state_defaults_#{register_id}"
  compile_state_defaults!(method_name, register_id)
  @state_defaults_names[register_id] = method_name
  method_name
end

#define_attribute_methods(attr, register = nil) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 109

def define_attribute_methods(attr, register = nil)
  name = attr.name
  register_id = extract_register_id(register)

  if attr.enum?
    add_enum_methods_to_model(
      model,
      name,
      attr.options[:values],
      collection: attr.options[:collection],
    )
  elsif attr.derived? && name != attr.method_name
    unless method_defined?(name, false)
      define_method(name) do
        value = public_send(attr.method_name)
        # Cast the derived value to the specified type. cast_derived,
        # not cast_element: this reader is its own casting entry point,
        # so it needs the same "nothing arrived, nothing to cast" rule
        # the writers get, and a collection has to come back as one.
        attr.cast_derived(value, register_id)
      end
    end
  elsif attr.unresolved_type == Lutaml::Model::Type::Reference
    Lutaml::Model::Store.reference_types_in_use!
    define_reference_methods(name, register_id)
  else
    define_regular_attribute_methods(name, attr)
  end
end

#define_reference_methods(name, register) ⇒ Object

Define reference-type attribute methods

Reference types store a reference key that can be resolved to the actual object.



146
147
148
149
150
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
190
191
192
193
194
195
196
197
198
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 146

def define_reference_methods(name, register)
  register_id = register
  attr = attributes[name]

  unless method_defined?(:"#{name}_ref", false)
    define_method("#{name}_ref") do
      instance_variable_get(:"@#{name}_ref")
    end
  end

  key_method_name = if attr.options[:collection]
                      Utils.pluralize(attr.options[:ref_key_attribute].to_s)
                    else
                      attr.options[:ref_key_attribute]
                    end

  unless method_defined?(:"#{name}_#{key_method_name}", false)
    define_method("#{name}_#{key_method_name}") do
      ref = instance_variable_get(:"@#{name}_ref")
      # attr.reference_key first: once a collection reader has stored
      # its resolved objects, the key has to come back off the object.
      resolve_reference_key(attr.reference_key(ref))
    end
  end

  unless method_defined?(name, false)
    if attr.options[:collection]
      define_method(name) do
        materialize_reference_collection(name)
      end
    else
      define_method(name) do
        ref = instance_variable_get(:"@#{name}_ref")
        resolve_reference_value(ref)
      end
    end
  end

  unless method_defined?(:"#{name}=", false)
    define_method(:"#{name}=") do |value|
      value_set_for(name)
      casted_value = value
      unless casted_value.is_a?(Lutaml::Model::Type::Reference)
        casted_value = attr.cast_value(value, register_id)
      end

      instance_variable_set(:"@#{name}_ref", casted_value)

      resolved_reference = resolve_reference_key(casted_value)
      instance_variable_set(:"@#{name}", resolved_reference)
    end
  end
end

#define_reflective_attribute_methods(name, attr) ⇒ Object

Historical getter shape for punctuation-named attributes and any name the compiled form cannot express.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 69

def define_reflective_attribute_methods(name, attr)
  if attr.collection?
    define_method(name) do |*args|
      if args.empty?
        materialize_lazy_collection(name)
      else
        # Builder-style: g.member(item) appends to collection
        value = args.first
        current = instance_variable_get(:"@#{name}") || []
        new_value = current.is_a?(Array) ? current + [value] : value
        instance_variable_set(:"@#{name}", new_value)
        record_mutation(name, value)
        value
      end
    end
  else
    define_method(name) do |*args|
      if args.empty?
        instance_variable_get(:"@#{name}")
      else
        public_send(:"#{name}=", args.first)
        args.first
      end
    end
  end
end

#define_regular_attribute_methods(name, attr) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 209

def define_regular_attribute_methods(name, attr)
  unless name.to_s.match?(PLAIN_NAME)
    return define_reflective_attribute_methods(name, attr)
  end

  # Getters compile with a sentinel default argument instead of a
  # `*args` splat: the splat allocated an (almost always empty)
  # Array on EVERY read — the largest per-call allocation source
  # on instance-heavy parses (TODO.max-perf/06). The optional-arg
  # form allocates nothing, and the read is a direct @ivar.
  if attr.collection?
    # class_eval interpolates, e.g.:
    #   def items(arg = Lutaml::Model::Serialize::NO_ARG)
    #     if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
    #       materialize_lazy_collection(:items)
    #     else
    #       ... builder append ...
    #     end
    #   end
    class_eval("      def \#{name}(arg = Lutaml::Model::Serialize::NO_ARG)\n        if arg.equal?(Lutaml::Model::Serialize::NO_ARG)\n          materialize_lazy_collection(:\#{name})\n        else\n          current = @\#{name} || []\n          new_value = current.is_a?(Array) ? current + [arg] : arg\n          @\#{name} = new_value\n          record_mutation(:\#{name}, arg)\n          arg\n        end\n      end\n    RUBY\n  else\n    class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition\n      def \#{name}(arg = Lutaml::Model::Serialize::NO_ARG)\n        if arg.equal?(Lutaml::Model::Serialize::NO_ARG)\n          @\#{name}\n        else\n          public_send(:\"\#{name}=\", arg)\n          arg\n        end\n      end\n    RUBY\n  end\n\n  enum_shorthand_names = instance_variable_get(:@__enum_shorthand_names__) || Set.new\n  # Opal's Module#method_defined? only accepts 1 arg (MRI accepts\n  # the optional `inherit` flag). Skip the flag there \u2014 the\n  # difference only matters for inherited-method filtering.\n  setter_defined = if Lutaml::Model.opal?\n                     method_defined?(:\"\#{name}=\")\n                   else\n                     method_defined?(:\"\#{name}=\", false)\n                   end\n  return if setter_defined && !enum_shorthand_names.include?(name.to_s)\n\n  # class_eval'd bodies cannot close over locals; a hidden\n  # define_method accessor holds the Attribute handle.\n  attr_reader_method = :\"__attribute_definition_\#{name}\"\n  reader_defined = if Lutaml::Model.opal?\n                     method_defined?(attr_reader_method)\n                   else\n                     method_defined?(attr_reader_method, false)\n                   end\n  define_method(attr_reader_method) { attr } unless reader_defined\n\n  if attr.collection?\n    # class_eval interpolates, e.g.:\n    #   def items=(value)\n    #     value_set_for(:items)\n    #     value = ATTR.cast_value(value, lutaml_register)\n    #     current = @items\n    #     ... sentinel preservation ...\n    #     record_mutation_collection(:items, value)\n    #   end\n    # The compiled body writes @items directly: the define_method\n    # form interpolated \"@items\" name strings per call \u2014 one per\n    # setter invocation on instance-heavy parses (TODO.max-perf/06).\n    class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition\n      def \#{name}=(value)\n        value_set_for(:\#{name})\n        value = __attribute_definition_\#{name}.cast_value(value, lutaml_register)\n        current = @\#{name}\n        if current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&\n            (value.nil? || Lutaml::Model::Utils.uninitialized?(value))\n          # Sentinel stays \u2014 no allocation for truly empty collections\n        else\n          @\#{name} = value\n        end\n        record_mutation_collection(:\#{name}, value)\n      end\n\n      # TODO.max-perf/31: the deserialization transforms already\n      # cast through the format-aware path; this writer skips the\n      # setter's re-cast while keeping value_set_for marking and\n      # mutation recording. Public assignment keeps the caster.\n      def __assign_parsed_\#{name}=(value)\n        value_set_for(:\#{name})\n        current = @\#{name}\n        if current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&\n            (value.nil? || Lutaml::Model::Utils.uninitialized?(value))\n        else\n          @\#{name} = value\n        end\n        record_mutation_collection(:\#{name}, value)\n      end\n    RUBY\n  else\n    class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition\n      def \#{name}=(value)\n        value_set_for(:\#{name})\n        value = __attribute_definition_\#{name}.cast_value(value, lutaml_register)\n        @\#{name} = value\n        record_mutation(:\#{name}, value)\n      end\n\n      # TODO.max-perf/31: see the collection branch above.\n      def __assign_parsed_\#{name}=(value)\n        value_set_for(:\#{name})\n        @\#{name} = value\n        record_mutation(:\#{name}, value)\n      end\n    RUBY\n  end\nend\n", __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition

#invalidate_state_defaults!Object



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 96

def invalidate_state_defaults!
  # Opal's method_defined? takes no inherit flag (see the
  # setter_defined check in define_regular_attribute_methods).
  (@state_defaults_names ||= {}).each_key do |compiled|
    defined_now = if Lutaml::Model.opal?
                    method_defined?(compiled)
                  else
                    method_defined?(compiled, false)
                  end
    remove_method(compiled) if defined_now
  end
end

#restrict(name, options = {}) ⇒ Symbol

Restrict options on an existing attribute



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 368

def restrict(name, options = {})
  register_id = options.delete(:register) || Lutaml::Model::Config.default_register

  if !@attributes.key?(name) && !register_record(register_id)&.dig(
    :attributes, name
  )
    return restrict_attributes[name] = options if any_importable_models?

    raise Lutaml::Model::UndefinedAttributeError.new(name, self)
  end

  validate_attribute_options!(name, options)
  attr = attributes(register_id)[name]
  attr.options.merge!(options)
  attr.process_options!
  name
end

#validate_attribute_options!(name, options) ⇒ Object

Validate attribute options

Raises:



398
399
400
401
402
403
404
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 398

def validate_attribute_options!(name, options)
  invalid_opts = options.keys - Attribute::ALLOWED_OPTIONS
  return if invalid_opts.empty?

  raise Lutaml::Model::InvalidAttributeOptionsError.new(name,
                                                        invalid_opts)
end