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
@namereads; 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
-
#any_importable_models? ⇒ Boolean
Check if there are any importable models.
-
#attribute(name, type, options = {}) ⇒ Attribute
Define an attribute for the model.
- #compile_state_defaults!(method_name, register_id = nil) ⇒ Object
-
#compiled_state_defaults_name!(register_id) ⇒ Object
Define attribute methods on the model class.
- #define_attribute_methods(attr, register = nil) ⇒ Object
-
#define_reference_methods(name, register) ⇒ Object
Define reference-type attribute methods.
-
#define_reflective_attribute_methods(name, attr) ⇒ Object
Historical getter shape for punctuation-named attributes and any name the compiled form cannot express.
- #define_regular_attribute_methods(name, attr) ⇒ Object
- #invalidate_state_defaults! ⇒ Object
-
#restrict(name, options = {}) ⇒ Symbol
Restrict options on an existing attribute.
-
#validate_attribute_options!(name, options) ⇒ Object
Validate attribute options.
Instance Method Details
#any_importable_models? ⇒ Boolean
Check if there are any importable models
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
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, = {}) type, = process_type_hash(type, ) if type.is_a?(::Hash) if type.is_a?(::Array) = .merge(union_member_types: type) type = Lutaml::Model::Type::Union end # Handle direct method option in options hash if [:method] [:method_name] = .delete(:method) end attr = Attribute.new(name, type, ) @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.
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.[:values], collection: attr.[: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.[:collection] Utils.pluralize(attr.[:ref_key_attribute].to_s) else attr.[: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.[: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
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, = {}) register_id = .delete(:register) || Lutaml::Model::Config.default_register if !@attributes.key?(name) && !register_record(register_id)&.dig( :attributes, name ) return restrict_attributes[name] = if any_importable_models? raise Lutaml::Model::UndefinedAttributeError.new(name, self) end (name, ) attr = attributes(register_id)[name] attr..merge!() attr. name end |
#validate_attribute_options!(name, options) ⇒ Object
Validate attribute options
376 377 378 379 380 381 382 |
# File 'lib/lutaml/model/serialize/attribute_definition.rb', line 376 def (name, ) invalid_opts = .keys - Attribute::ALLOWED_OPTIONS return if invalid_opts.empty? raise Lutaml::Model::InvalidAttributeOptionsError.new(name, invalid_opts) end |