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

Returns:

  • (Boolean)

    True if there are pending imports



367
368
369
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 367

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

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

Define an attribute for the model

Parameters:

  • name (Symbol)

    The attribute name

  • type (Class, Symbol, Hash)

    The attribute type

  • options (Hash) (defaults to: {})

    Attribute options

Returns:



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 319

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(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
    def #{method_name}
    #{lines}
    end
  RUBY
end

#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.

Parameters:

  • attr (Attribute)

    The attribute to define methods for

  • register (Symbol, nil)

    The register for type resolution



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.

Parameters:

  • name (Symbol)

    The attribute name

  • register (Symbol)

    The register ID



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
# 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(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
      def #{name}(arg = Lutaml::Model::Serialize::NO_ARG)
        if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
          materialize_lazy_collection(:#{name})
        else
          current = @#{name} || []
          new_value = current.is_a?(Array) ? current + [arg] : arg
          @#{name} = new_value
          record_mutation(:#{name}, arg)
          arg
        end
      end
    RUBY
  else
    class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
      def #{name}(arg = Lutaml::Model::Serialize::NO_ARG)
        if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
          @#{name}
        else
          public_send(:"#{name}=", arg)
          arg
        end
      end
    RUBY
  end

  enum_shorthand_names = instance_variable_get(:@__enum_shorthand_names__) || Set.new
  # Opal's Module#method_defined? only accepts 1 arg (MRI accepts
  # the optional `inherit` flag). Skip the flag there — the
  # difference only matters for inherited-method filtering.
  setter_defined = if Lutaml::Model.opal?
                     method_defined?(:"#{name}=")
                   else
                     method_defined?(:"#{name}=", false)
                   end
  return if setter_defined && !enum_shorthand_names.include?(name.to_s)

  # class_eval'd bodies cannot close over locals; a hidden
  # define_method accessor holds the Attribute handle.
  attr_reader_method = :"__attribute_definition_#{name}"
  reader_defined = if Lutaml::Model.opal?
                     method_defined?(attr_reader_method)
                   else
                     method_defined?(attr_reader_method, false)
                   end
  define_method(attr_reader_method) { attr } unless reader_defined

  if attr.collection?
    # class_eval interpolates, e.g.:
    #   def items=(value)
    #     value_set_for(:items)
    #     value = ATTR.cast_value(value, lutaml_register)
    #     current = @items
    #     ... sentinel preservation ...
    #     record_mutation_collection(:items, value)
    #   end
    # The compiled body writes @items directly: the define_method
    # form interpolated "@items" name strings per call — one per
    # setter invocation on instance-heavy parses (TODO.max-perf/06).
    class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
      def #{name}=(value)
        value_set_for(:#{name})
        value = __attribute_definition_#{name}.cast_value(value, lutaml_register)
        current = @#{name}
        if current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
            (value.nil? || Lutaml::Model::Utils.uninitialized?(value))
          # Sentinel stays — no allocation for truly empty collections
        else
          @#{name} = value
        end
        record_mutation_collection(:#{name}, value)
      end
    RUBY
  else
    class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
      def #{name}=(value)
        value_set_for(:#{name})
        value = __attribute_definition_#{name}.cast_value(value, lutaml_register)
        @#{name} = value
        record_mutation(:#{name}, value)
      end
    RUBY
  end
end

#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

Parameters:

  • name (Symbol)

    The attribute name to restrict

  • options (Hash) (defaults to: {})

    New options to merge

Returns:

  • (Symbol)

    The attribute name



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

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

Parameters:

  • name (Symbol)

    The attribute name

  • options (Hash)

    The options to validate

Raises:



376
377
378
379
380
381
382
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 376

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