lutaml-model 0.8.56 → 0.8.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/scripts/test_installed_gem.rb +58 -0
- data/.github/workflows/downstream-performance.yml +28 -3
- data/.github/workflows/js-pr-check.yml +4 -0
- data/.github/workflows/js-sync.yml +4 -0
- data/.github/workflows/opal.yml +9 -0
- data/.github/workflows/rake.yml +6 -41
- data/.github/workflows/release.yml +41 -0
- data/Gemfile +17 -3
- data/bench/bench_alloc_audit.rb +56 -0
- data/bench/bench_common.rb +11 -0
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/gem-integration.adoc +85 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +8 -1
- data/docs/_guides/native-engines.adoc +23 -5
- data/docs/_guides/schema-generation.adoc +25 -0
- data/docs/_guides/type-registers-contexts.adoc +126 -0
- data/lib/compat/opal/lutaml_model_boot.rb +7 -5
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
- data/lib/lutaml/key_value/data_model/element.rb +5 -1
- data/lib/lutaml/key_value/mapping_rule.rb +7 -0
- data/lib/lutaml/key_value/transform.rb +29 -24
- data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
- data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
- data/lib/lutaml/key_value/transformation.rb +64 -116
- data/lib/lutaml/model/adapter_resolver.rb +7 -3
- data/lib/lutaml/model/attribute.rb +74 -15
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
- data/lib/lutaml/model/render_policy.rb +26 -8
- data/lib/lutaml/model/schema/base_schema.rb +15 -0
- data/lib/lutaml/model/schema/generator/definition.rb +52 -8
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
- data/lib/lutaml/model/schema.rb +4 -2
- data/lib/lutaml/model/serializable.rb +33 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
- data/lib/lutaml/model/serialize/initialization.rb +54 -7
- data/lib/lutaml/model/serialize/model_import.rb +60 -0
- data/lib/lutaml/model/serialize.rb +14 -6
- data/lib/lutaml/model/type/time.rb +6 -0
- data/lib/lutaml/model/type/value.rb +9 -7
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/utils.rb +18 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
- data/lib/lutaml/xml/adapter_loader.rb +5 -2
- data/lib/lutaml/xml/format.rb +18 -16
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
- data/lib/lutaml/xml/leptris.rb +46 -0
- data/lib/lutaml/xml/mapping.rb +71 -18
- data/lib/lutaml/xml/model_transform.rb +231 -8
- data/lib/lutaml/xml/namespace.rb +18 -8
- data/lib/lutaml/xml/namespace_collector.rb +17 -1
- data/lib/lutaml/xml/namespace_needs.rb +77 -49
- data/lib/lutaml/xml/parse_session.rb +7 -19
- data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
- data/lib/lutaml/xml/transformation.rb +15 -1
- data/lib/lutaml/xml/type/serializers.rb +8 -1
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/lutaml-model.gemspec +7 -4
- data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
- data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
- data/spec/lutaml/key_value/transformation_spec.rb +87 -21
- data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
- data/spec/lutaml/model/collection_validation_spec.rb +265 -0
- data/spec/lutaml/model/desc_spec.rb +50 -0
- data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
- data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
- data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
- data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
- data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
- data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
- data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
- data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
- data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
- data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
- data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
- data/spec/person_spec.rb +8 -0
- metadata +48 -17
- data/lib/lutaml/xml/plan_compiler.rb +0 -332
- data/lib/lutaml/xml/plan_hydrator.rb +0 -367
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -252
- data/lib/lutaml/xml/plan_walk.rb +0 -30
|
@@ -12,6 +12,14 @@ module Lutaml
|
|
|
12
12
|
# options are normalized into value_map entries at MappingRule construction
|
|
13
13
|
# time, so downstream code never needs to re-interpret them.
|
|
14
14
|
module RenderPolicy
|
|
15
|
+
# Isolated holder: host transformations freeze after compile; an
|
|
16
|
+
# ivar cache on them raises FrozenError (learned the hard way).
|
|
17
|
+
SKIP_PLANS = ::Class.new do
|
|
18
|
+
def self.cache
|
|
19
|
+
@cache ||= {}.compare_by_identity
|
|
20
|
+
end
|
|
21
|
+
end.cache
|
|
22
|
+
|
|
15
23
|
def self.derived_attribute_for?(context_obj, attr_name)
|
|
16
24
|
return false unless context_obj.is_a?(Lutaml::Model::Serialize) &&
|
|
17
25
|
context_obj.class.is_a?(Class) &&
|
|
@@ -52,16 +60,26 @@ module Lutaml
|
|
|
52
60
|
# @param context_obj [Object] The context object (model_instance or delegate_obj)
|
|
53
61
|
# @return [Boolean] true if should skip
|
|
54
62
|
def check_skip_logic?(value, rule, context_obj)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
# Rule-constant parts (attribute name, :to value map) are frozen
|
|
64
|
+
# per rule; recomputing them per value showed in the to_yaml
|
|
65
|
+
# wall profile. The host transformation freezes itself after
|
|
66
|
+
# compile, so the cache lives in an isolated holder (TypeProbeCache
|
|
67
|
+
# precedent): host object -> identity-keyed rule plans.
|
|
68
|
+
per_host = (SKIP_PLANS[self] ||= {}.compare_by_identity)
|
|
69
|
+
skip_plan = (per_host[rule] ||= [extract_attribute_name(rule), to_value_map(rule)])
|
|
70
|
+
attr_name, to_map = skip_plan
|
|
71
|
+
|
|
72
|
+
# Plain conditionals, not `when ->(v)` literals: each lambda
|
|
73
|
+
# literal in a case/when allocates on every call — two per
|
|
74
|
+
# value here, 16% of to_yaml's allocations in the object-mode
|
|
75
|
+
# profile.
|
|
76
|
+
if value.nil?
|
|
61
77
|
return to_map[:nil] == :omitted
|
|
62
|
-
|
|
78
|
+
end
|
|
79
|
+
if Lutaml::Model::Utils.empty?(value)
|
|
63
80
|
return to_map[:empty] == :omitted
|
|
64
|
-
|
|
81
|
+
end
|
|
82
|
+
if Lutaml::Model::Utils.uninitialized?(value)
|
|
65
83
|
return to_map[:omitted] == :omitted || to_map[:omitted].nil?
|
|
66
84
|
end
|
|
67
85
|
|
|
@@ -16,6 +16,21 @@ module Lutaml
|
|
|
16
16
|
|
|
17
17
|
format_schema(schema, options)
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
# Generate one schema document covering several model classes.
|
|
21
|
+
# The roots share a single $defs map and no top-level $ref is
|
|
22
|
+
# emitted; each root stays addressable under $defs by its class
|
|
23
|
+
# name. Use generate for a single root with a top-level $ref.
|
|
24
|
+
def generate_many(klasses, options = {})
|
|
25
|
+
schema = {
|
|
26
|
+
"$schema" => options[:schema],
|
|
27
|
+
"$id" => options[:id],
|
|
28
|
+
"description" => options[:description],
|
|
29
|
+
"$defs" => Generator::DefinitionsCollection.from_classes(klasses).to_schema,
|
|
30
|
+
}.compact
|
|
31
|
+
|
|
32
|
+
format_schema(schema, options)
|
|
33
|
+
end
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
attr_reader :schema, :klass
|
|
@@ -23,9 +23,23 @@ module Lutaml
|
|
|
23
23
|
},
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
#
|
|
26
|
+
# Choice validation follows the serializer's contract: unset
|
|
27
|
+
# or empty members are omitted from output, and instances that
|
|
28
|
+
# leave a whole group empty are accepted (#869). Members are
|
|
29
|
+
# therefore never `required`; a max:1 group contributes an
|
|
30
|
+
# at-most-one constraint over its members, and independent
|
|
31
|
+
# groups combine conjunctively under allOf. Groups whose range
|
|
32
|
+
# admits several members impose no serialization constraint.
|
|
27
33
|
if type.choice_attributes.any?
|
|
28
|
-
|
|
34
|
+
groups = type.choice_attributes
|
|
35
|
+
.select { |choice| choice.max == 1 }
|
|
36
|
+
.map { |choice| { "oneOf" => exclusive_branches(choice) } }
|
|
37
|
+
|
|
38
|
+
if groups.one?
|
|
39
|
+
@schema[name].merge!(groups.first)
|
|
40
|
+
elsif groups.any?
|
|
41
|
+
@schema[name]["allOf"] = groups
|
|
42
|
+
end
|
|
29
43
|
end
|
|
30
44
|
|
|
31
45
|
@schema
|
|
@@ -33,15 +47,45 @@ module Lutaml
|
|
|
33
47
|
|
|
34
48
|
private
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
# Leaves of the group (nested choices collapse into it), as
|
|
51
|
+
# [name, attribute] pairs.
|
|
52
|
+
def choice_leaves(choice)
|
|
53
|
+
choice.attributes.flat_map do |member|
|
|
54
|
+
if member.is_a?(Lutaml::Model::Choice)
|
|
55
|
+
choice_leaves(member)
|
|
56
|
+
else
|
|
57
|
+
[[member.name.to_s, member]]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Exactly-one-present construction over the leaves: no leaf, or
|
|
63
|
+
# exactly one leaf present without the others. Accepts the
|
|
64
|
+
# all-empty document the serializer emits; rejects documents
|
|
65
|
+
# with two members of the group at once (max:1).
|
|
66
|
+
def exclusive_branches(choice)
|
|
67
|
+
leaves = choice_leaves(choice)
|
|
68
|
+
names = leaves.map(&:first)
|
|
69
|
+
none = {
|
|
70
|
+
"not" => {
|
|
71
|
+
"anyOf" => names.map { |n| { "required" => [n] } },
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
singles = leaves.map do |name, attribute|
|
|
75
|
+
others = names - [name]
|
|
38
76
|
{
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
77
|
+
"allOf" => (
|
|
78
|
+
[{
|
|
79
|
+
"type" => "object",
|
|
80
|
+
"properties" => PropertiesCollection.from_attributes(
|
|
81
|
+
[attribute], extract_register_from(type)
|
|
82
|
+
).to_schema,
|
|
83
|
+
"required" => [name],
|
|
84
|
+
}] + others.map { |o| { "not" => { "required" => [o] } } }
|
|
85
|
+
),
|
|
43
86
|
}
|
|
44
87
|
end
|
|
88
|
+
[none] + singles
|
|
45
89
|
end
|
|
46
90
|
|
|
47
91
|
def properties_to_schema(type)
|
|
@@ -9,20 +9,28 @@ module Lutaml
|
|
|
9
9
|
include SharedMethods
|
|
10
10
|
|
|
11
11
|
def from_class(klass)
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
collection = new
|
|
13
|
+
register_definition(collection, klass)
|
|
14
|
+
collection
|
|
15
|
+
end
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
# Build one collection covering several roots. The seen-set
|
|
18
|
+
# is shared across roots so a class referenced by more than
|
|
19
|
+
# one of them registers once.
|
|
20
|
+
def from_classes(klasses)
|
|
21
|
+
collection = new
|
|
22
|
+
seen = {}
|
|
23
|
+
klasses.each { |klass| register_definition(collection, klass, seen) }
|
|
24
|
+
collection
|
|
17
25
|
end
|
|
18
26
|
|
|
19
|
-
def process_attributes(collection, klass)
|
|
27
|
+
def process_attributes(collection, klass, seen = {})
|
|
20
28
|
register = extract_register_from(klass)
|
|
21
29
|
klass.attributes.each_value do |attribute|
|
|
22
30
|
if attribute.union?
|
|
23
|
-
process_union_members(collection, attribute)
|
|
31
|
+
process_union_members(collection, attribute, seen)
|
|
24
32
|
elsif attribute.serializable?(register)
|
|
25
|
-
process_attribute(collection, attribute, register)
|
|
33
|
+
process_attribute(collection, attribute, register, seen)
|
|
26
34
|
end
|
|
27
35
|
end
|
|
28
36
|
end
|
|
@@ -30,31 +38,44 @@ module Lutaml
|
|
|
30
38
|
# A union attribute's type is Type::Union, not a model, so collect
|
|
31
39
|
# definitions for each of its Serializable members directly (a
|
|
32
40
|
# union schema emits a $ref per model member).
|
|
33
|
-
def process_union_members(collection, attribute)
|
|
41
|
+
def process_union_members(collection, attribute, seen = {})
|
|
34
42
|
attribute.union_member_types.each do |member|
|
|
35
43
|
next unless member.is_a?(::Class) &&
|
|
36
44
|
member.include?(Lutaml::Model::Serialize)
|
|
37
45
|
|
|
38
|
-
collection
|
|
46
|
+
register_definition(collection, member, seen)
|
|
39
47
|
end
|
|
40
48
|
end
|
|
41
49
|
|
|
42
|
-
def process_attribute(collection, attribute, register)
|
|
50
|
+
def process_attribute(collection, attribute, register, seen = {})
|
|
43
51
|
attr_type = Lutaml::Model::GlobalContext.resolve_type(
|
|
44
52
|
attribute.type, register
|
|
45
53
|
)
|
|
46
|
-
collection
|
|
54
|
+
register_definition(collection, attr_type, seen)
|
|
47
55
|
|
|
48
|
-
process_polymorphic_types(collection, attribute)
|
|
56
|
+
process_polymorphic_types(collection, attribute, seen)
|
|
49
57
|
end
|
|
50
58
|
|
|
51
|
-
def process_polymorphic_types(collection, attribute)
|
|
59
|
+
def process_polymorphic_types(collection, attribute, seen = {})
|
|
52
60
|
return unless attribute.options&.[](:polymorphic)
|
|
53
61
|
|
|
54
62
|
attribute.options[:polymorphic].each do |child|
|
|
55
|
-
collection
|
|
63
|
+
register_definition(collection, child, seen)
|
|
56
64
|
end
|
|
57
65
|
end
|
|
66
|
+
|
|
67
|
+
# Recursive models (Node#child → Node, or longer include
|
|
68
|
+
# cycles) are valid; the seen-set registers each class once
|
|
69
|
+
# and lets property references point at the shared $defs
|
|
70
|
+
# entry instead of recursing (#863).
|
|
71
|
+
def register_definition(collection, klass, seen = {})
|
|
72
|
+
return collection if seen[klass]
|
|
73
|
+
|
|
74
|
+
seen[klass] = true
|
|
75
|
+
collection << Definition.new(klass)
|
|
76
|
+
process_attributes(collection, klass, seen)
|
|
77
|
+
collection
|
|
78
|
+
end
|
|
58
79
|
end
|
|
59
80
|
|
|
60
81
|
attr_reader :definitions
|
data/lib/lutaml/model/schema.rb
CHANGED
|
@@ -52,11 +52,13 @@ module Lutaml
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def self.to_json(klass, options = {})
|
|
55
|
-
|
|
55
|
+
# The generator signatures are keyword-only; splat so callers may
|
|
56
|
+
# pass an options hash positionally (#866).
|
|
57
|
+
JsonSchema.generate(klass, **options)
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def self.to_yaml(klass, options = {})
|
|
59
|
-
YamlSchema.generate(klass, options)
|
|
61
|
+
YamlSchema.generate(klass, **options)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
# XML-specific methods (to_xsd, to_relaxng, from_xml) are registered
|
|
@@ -8,6 +8,39 @@ module Lutaml
|
|
|
8
8
|
# model classes actually inherit from (lutaml-model#18).
|
|
9
9
|
include Serialize
|
|
10
10
|
extend ComparableModel::ClassMethods
|
|
11
|
+
|
|
12
|
+
# Ivars that must never take part in inspect output: they either
|
|
13
|
+
# walk the whole graph (@lutaml_parent climbs to the root, and
|
|
14
|
+
# parent-child mutual recursion never terminates under the
|
|
15
|
+
# default Object#inspect) or are bulky parse bookkeeping.
|
|
16
|
+
INSPECT_EXCLUDED_IVARS = %i[
|
|
17
|
+
@lutaml_parent @lutaml_root @lutaml_register @using_default
|
|
18
|
+
@element_order @attribute_order @encoding
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
# Bounded, Struct-like inspect: scalars by value, collections by
|
|
22
|
+
# size, child models by class name. The default Object#inspect
|
|
23
|
+
# recurses through @lutaml_parent and re-enters children forever
|
|
24
|
+
# on real parsed documents.
|
|
25
|
+
def inspect
|
|
26
|
+
parts = (instance_variables - INSPECT_EXCLUDED_IVARS).map do |iv|
|
|
27
|
+
"#{iv}=#{self.class.inspect_value(instance_variable_get(iv))}"
|
|
28
|
+
end
|
|
29
|
+
attrs = " #{parts.join(', ')}" unless parts.empty?
|
|
30
|
+
"#<#{self.class.name}#{attrs}>"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def inspect_value(value)
|
|
35
|
+
case value
|
|
36
|
+
when nil, true, false, Numeric, Symbol then value.inspect
|
|
37
|
+
when String then value.inspect.length > 48 ? "#{value.inspect[0, 45]}...".inspect : value.inspect
|
|
38
|
+
when Array then "(#{value.size} items)"
|
|
39
|
+
when Hash then "{#{value.size} keys}"
|
|
40
|
+
else "##{value.class.name}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
11
44
|
end
|
|
12
45
|
end
|
|
13
46
|
end
|
|
@@ -8,6 +8,24 @@ module Lutaml
|
|
|
8
8
|
# Extracted from serialize.rb to improve code organization.
|
|
9
9
|
# Provides methods for defining and validating model attributes.
|
|
10
10
|
module AttributeDefinition
|
|
11
|
+
# Whether the per-class accessors and the state-defaults seed are
|
|
12
|
+
# compiled from Ruby source with a String class_eval (the fast
|
|
13
|
+
# path). Opal has no runtime compiler unless opal-parser is
|
|
14
|
+
# bundled, so there every such method is defined from a block
|
|
15
|
+
# with define_method instead.
|
|
16
|
+
# Core dispatch for the block-form methods, bound per call so an
|
|
17
|
+
# attribute or method a model defines with one of these names
|
|
18
|
+
# (send, __send__, instance_variable_get, const_get, ...) is never invoked;
|
|
19
|
+
# the compiled form reads ivars and calls methods directly.
|
|
20
|
+
IVAR_GET = ::Kernel.instance_method(:instance_variable_get)
|
|
21
|
+
IVAR_SET = ::Kernel.instance_method(:instance_variable_set)
|
|
22
|
+
SEND = ::BasicObject.instance_method(:__send__)
|
|
23
|
+
CONST_GET = ::Module.instance_method(:const_get)
|
|
24
|
+
|
|
25
|
+
def self.source_compilation?
|
|
26
|
+
!Lutaml::Model.opal?
|
|
27
|
+
end
|
|
28
|
+
|
|
11
29
|
# Define attribute methods on the model class
|
|
12
30
|
#
|
|
13
31
|
# @param attr [Attribute] The attribute to define methods for
|
|
@@ -44,6 +62,26 @@ module Lutaml
|
|
|
44
62
|
return
|
|
45
63
|
end
|
|
46
64
|
|
|
65
|
+
unless AttributeDefinition.source_compilation?
|
|
66
|
+
seeds = attrs.map do |name, attr|
|
|
67
|
+
sentinel = if attr.collection?
|
|
68
|
+
Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION
|
|
69
|
+
else
|
|
70
|
+
Lutaml::Model::UninitializedClass.instance
|
|
71
|
+
end
|
|
72
|
+
[:"@#{name}", sentinel]
|
|
73
|
+
end
|
|
74
|
+
# Raw readers for every attribute the register sees, as the
|
|
75
|
+
# compiled form below does (#933).
|
|
76
|
+
attrs.each_key { |name| define_raw_state_reader(name) }
|
|
77
|
+
define_method(method_name) do
|
|
78
|
+
# Returns the last seeded value, as the compiled form's final
|
|
79
|
+
# assignment does.
|
|
80
|
+
seeds.reduce(nil) { |_, (ivar, sentinel)| IVAR_SET.bind_call(self, ivar, sentinel) }
|
|
81
|
+
end
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
47
85
|
lines = attrs.map do |name, attr|
|
|
48
86
|
if attr.collection?
|
|
49
87
|
"@#{name} = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION"
|
|
@@ -62,6 +100,32 @@ module Lutaml
|
|
|
62
100
|
#{lines}
|
|
63
101
|
end
|
|
64
102
|
RUBY
|
|
103
|
+
|
|
104
|
+
# Raw readers compiled alongside the seeding so every
|
|
105
|
+
# attribute the register can see — including late-merged
|
|
106
|
+
# imports that bypass define_attribute_methods — carries
|
|
107
|
+
# one (#933). Idempotent with the declaration-time
|
|
108
|
+
# definition.
|
|
109
|
+
raw_lines = attrs.filter_map do |name, _attr|
|
|
110
|
+
next unless name.to_s.match?(PLAIN_NAME)
|
|
111
|
+
|
|
112
|
+
<<~RUBY
|
|
113
|
+
def __lutaml_raw_#{name}
|
|
114
|
+
@#{name}
|
|
115
|
+
end
|
|
116
|
+
RUBY
|
|
117
|
+
end.join
|
|
118
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
119
|
+
#{raw_lines}
|
|
120
|
+
RUBY
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Block-form raw-state reader (no String eval), defined on self.
|
|
124
|
+
def define_raw_state_reader(name)
|
|
125
|
+
ivar = :"@#{name}"
|
|
126
|
+
define_method(:"__lutaml_raw_#{name}") do
|
|
127
|
+
IVAR_GET.bind_call(self, ivar)
|
|
128
|
+
end
|
|
65
129
|
end
|
|
66
130
|
|
|
67
131
|
# Historical getter shape for punctuation-named attributes and
|
|
@@ -104,12 +168,46 @@ module Lutaml
|
|
|
104
168
|
end
|
|
105
169
|
remove_method(compiled) if defined_now
|
|
106
170
|
end
|
|
171
|
+
# The names cache must go with the methods: the memoized
|
|
172
|
+
# lookup answers stale names for removed methods otherwise.
|
|
173
|
+
@state_defaults_names.clear
|
|
107
174
|
end
|
|
108
175
|
|
|
109
176
|
def define_attribute_methods(attr, register = nil)
|
|
110
177
|
name = attr.name
|
|
111
178
|
register_id = extract_register_id(register)
|
|
112
179
|
|
|
180
|
+
# lutaml-model#922: raw-state reader for the deserializer's
|
|
181
|
+
# unmatched-rule handling. A custom writer that assigns a
|
|
182
|
+
# *different* attribute mid-parse (relaton's ICS Isoics
|
|
183
|
+
# fallback; any derived-value pattern) is invisible to the
|
|
184
|
+
# value_set tracker -- public setters don't mark it -- so the
|
|
185
|
+
# tracked "still default" state lies. The deserializer needs
|
|
186
|
+
# the raw stored state to know whether an attribute was
|
|
187
|
+
# writer-populated: still holding the initialized marker
|
|
188
|
+
# (sentinel / lazy collection) means unset; anything else was
|
|
189
|
+
# written since init and must never be overwritten by an
|
|
190
|
+
# unmatched rule's default assignment. Declared here (not in
|
|
191
|
+
# the state-defaults compilation) so late declarations get it
|
|
192
|
+
# too -- XMI's extension loader declares attributes after
|
|
193
|
+
# first parse.
|
|
194
|
+
# Without a source compiler, compile_state_defaults! defines
|
|
195
|
+
# its raw readers from blocks; define one for every name here
|
|
196
|
+
# too (define_method takes any name, not only plain ones).
|
|
197
|
+
# Both forms define on self: a bare `model` receiver resolves
|
|
198
|
+
# through the host class's own namespace (omml's Base answers
|
|
199
|
+
# a Store model) and defined the reader on the wrong object
|
|
200
|
+
# (#933).
|
|
201
|
+
if !AttributeDefinition.source_compilation?
|
|
202
|
+
define_raw_state_reader(name)
|
|
203
|
+
elsif name.to_s.match?(PLAIN_NAME)
|
|
204
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
205
|
+
def __lutaml_raw_#{name}
|
|
206
|
+
@#{name}
|
|
207
|
+
end
|
|
208
|
+
RUBY
|
|
209
|
+
end
|
|
210
|
+
|
|
113
211
|
if attr.enum?
|
|
114
212
|
add_enum_methods_to_model(
|
|
115
213
|
model,
|
|
@@ -216,7 +314,9 @@ module Lutaml
|
|
|
216
314
|
# Array on EVERY read — the largest per-call allocation source
|
|
217
315
|
# on instance-heavy parses (TODO.max-perf/06). The optional-arg
|
|
218
316
|
# form allocates nothing, and the read is a direct @ivar.
|
|
219
|
-
if
|
|
317
|
+
if !AttributeDefinition.source_compilation?
|
|
318
|
+
define_block_attribute_getter(name, attr)
|
|
319
|
+
elsif attr.collection?
|
|
220
320
|
# class_eval interpolates, e.g.:
|
|
221
321
|
# def items(arg = Lutaml::Model::Serialize::NO_ARG)
|
|
222
322
|
# if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
|
|
@@ -272,7 +372,9 @@ module Lutaml
|
|
|
272
372
|
end
|
|
273
373
|
define_method(attr_reader_method) { attr } unless reader_defined
|
|
274
374
|
|
|
275
|
-
if
|
|
375
|
+
if !AttributeDefinition.source_compilation?
|
|
376
|
+
define_block_attribute_setters(name, attr)
|
|
377
|
+
elsif attr.collection?
|
|
276
378
|
# class_eval interpolates, e.g.:
|
|
277
379
|
# def items=(value)
|
|
278
380
|
# value_set_for(:items)
|
|
@@ -332,6 +434,93 @@ module Lutaml
|
|
|
332
434
|
end
|
|
333
435
|
end
|
|
334
436
|
|
|
437
|
+
# Block form of the compiled getter in define_regular_attribute_methods,
|
|
438
|
+
# for runtimes without a source compiler (see source_compilation?).
|
|
439
|
+
def define_block_attribute_getter(name, attr)
|
|
440
|
+
name = name.to_sym # the compiled form passes :name literals
|
|
441
|
+
ivar = :"@#{name}"
|
|
442
|
+
if attr.collection?
|
|
443
|
+
# The compiled getter resolves Array in the model class's scope.
|
|
444
|
+
scope = self
|
|
445
|
+
define_method(name) do |arg = Lutaml::Model::Serialize::NO_ARG|
|
|
446
|
+
if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
|
|
447
|
+
materialize_lazy_collection(name)
|
|
448
|
+
else
|
|
449
|
+
current = IVAR_GET.bind_call(self, ivar) || []
|
|
450
|
+
new_value = current.is_a?(CONST_GET.bind_call(scope, :Array)) ? current + [arg] : arg
|
|
451
|
+
IVAR_SET.bind_call(self, ivar, new_value)
|
|
452
|
+
record_mutation(name, arg)
|
|
453
|
+
arg
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
else
|
|
457
|
+
define_method(name) do |arg = Lutaml::Model::Serialize::NO_ARG|
|
|
458
|
+
if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
|
|
459
|
+
IVAR_GET.bind_call(self, ivar)
|
|
460
|
+
else
|
|
461
|
+
public_send(:"#{name}=", arg)
|
|
462
|
+
arg
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Block form of the compiled setters in define_regular_attribute_methods.
|
|
469
|
+
# The Attribute is read through the same hidden accessor the
|
|
470
|
+
# compiled form calls, not captured: a setter redefined over an
|
|
471
|
+
# enum shorthand keeps casting with the accessor's Attribute.
|
|
472
|
+
def define_block_attribute_setters(name, attr)
|
|
473
|
+
name = name.to_sym # the compiled form passes :name literals
|
|
474
|
+
ivar = :"@#{name}"
|
|
475
|
+
handle = :"__attribute_definition_#{name}"
|
|
476
|
+
if attr.collection?
|
|
477
|
+
assign = lambda do |model, value|
|
|
478
|
+
current = IVAR_GET.bind_call(model, ivar)
|
|
479
|
+
unless current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
|
|
480
|
+
(value.nil? || Lutaml::Model::Utils.uninitialized?(value))
|
|
481
|
+
IVAR_SET.bind_call(model, ivar, value)
|
|
482
|
+
end
|
|
483
|
+
SEND.bind_call(model, :record_mutation_collection, name, value)
|
|
484
|
+
end
|
|
485
|
+
define_method(:"#{name}=") do |value|
|
|
486
|
+
value_set_for(name)
|
|
487
|
+
assign.call(self, SEND.bind_call(self, handle).cast_value(value, lutaml_register))
|
|
488
|
+
end
|
|
489
|
+
define_method(:"__assign_parsed_#{name}=") do |value|
|
|
490
|
+
value_set_for(name)
|
|
491
|
+
assign.call(self, value)
|
|
492
|
+
end
|
|
493
|
+
else
|
|
494
|
+
define_method(:"#{name}=") do |value|
|
|
495
|
+
value_set_for(name)
|
|
496
|
+
value = SEND.bind_call(self, handle).cast_value(value, lutaml_register)
|
|
497
|
+
IVAR_SET.bind_call(self, ivar, value)
|
|
498
|
+
record_mutation(name, value)
|
|
499
|
+
end
|
|
500
|
+
define_method(:"__assign_parsed_#{name}=") do |value|
|
|
501
|
+
value_set_for(name)
|
|
502
|
+
IVAR_SET.bind_call(self, ivar, value)
|
|
503
|
+
record_mutation(name, value)
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Class-level documentation metadata (#356): `desc "text"` on
|
|
509
|
+
# the model class; a zero-argument call reads it back.
|
|
510
|
+
# Subclasses inherit the nearest ancestor's description — an
|
|
511
|
+
# explicit `desc` overrides. Consumed by documentation and
|
|
512
|
+
# schema tooling; the serialization paths never read it.
|
|
513
|
+
def desc(text = nil)
|
|
514
|
+
return @lutaml_desc = text if text
|
|
515
|
+
|
|
516
|
+
return @lutaml_desc if defined?(@lutaml_desc)
|
|
517
|
+
|
|
518
|
+
if superclass.is_a?(::Class) &&
|
|
519
|
+
superclass < ::Lutaml::Model::Serializable
|
|
520
|
+
superclass.desc
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
335
524
|
# Define an attribute for the model
|
|
336
525
|
#
|
|
337
526
|
# @param name [Symbol] The attribute name
|
|
@@ -354,6 +543,11 @@ module Lutaml
|
|
|
354
543
|
attr = Attribute.new(name, type, options)
|
|
355
544
|
@attributes[name] = attr
|
|
356
545
|
@merged_attributes_cache = nil
|
|
546
|
+
# Attributes can be declared after the model has been
|
|
547
|
+
# instantiated (forward references, post-hoc declarations);
|
|
548
|
+
# the interned writer table from the first instantiation must
|
|
549
|
+
# not outlive the attribute set it was built from.
|
|
550
|
+
@instantiate_writers = nil
|
|
357
551
|
invalidate_state_defaults!
|
|
358
552
|
define_attribute_methods(attr)
|
|
359
553
|
|
|
@@ -84,6 +84,26 @@ module Lutaml
|
|
|
84
84
|
@__order_tracking__ ? true : false
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Whether any writer setter ran on this instance. Parsed
|
|
88
|
+
# instances start pristine — every set element value is covered
|
|
89
|
+
# by the parse-built element_order, so ordered serialization
|
|
90
|
+
# can skip order reconciliation; the first writer-set marks the
|
|
91
|
+
# instance and reconciliation engages (writer-added values sit
|
|
92
|
+
# outside the parse order).
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def mutated_after_parse?
|
|
95
|
+
@__mutated_after_parse__ ? true : false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Construction-time reset: hydration routes every attribute
|
|
99
|
+
# through the generated writers (which mark mutations); a
|
|
100
|
+
# freshly built instance is definitionally not
|
|
101
|
+
# mutated-after-parse.
|
|
102
|
+
# @return [void]
|
|
103
|
+
def clear_mutated_after_parse!
|
|
104
|
+
@__mutated_after_parse__ = false
|
|
105
|
+
end
|
|
106
|
+
|
|
87
107
|
# Record a singular attribute mutation in element_order.
|
|
88
108
|
#
|
|
89
109
|
# No-op unless order tracking is enabled (i.e. the instance was
|
|
@@ -102,6 +122,7 @@ module Lutaml
|
|
|
102
122
|
# content for content-mapped attributes, ignored otherwise
|
|
103
123
|
# @return [Object] the passed value
|
|
104
124
|
def record_mutation(attribute_name, value = nil)
|
|
125
|
+
@__mutated_after_parse__ = true
|
|
105
126
|
return value unless @__order_tracking__
|
|
106
127
|
|
|
107
128
|
track_order(attribute_name, value, nil)
|
|
@@ -122,6 +143,7 @@ module Lutaml
|
|
|
122
143
|
# @param value [Object, nil] The value assigned to the collection
|
|
123
144
|
# @return [Object] the passed value
|
|
124
145
|
def record_mutation_collection(attribute_name, value)
|
|
146
|
+
@__mutated_after_parse__ = true
|
|
125
147
|
return value unless @__order_tracking__
|
|
126
148
|
return value if value.nil? || Lutaml::Model::Utils.uninitialized?(value)
|
|
127
149
|
|
|
@@ -19,6 +19,11 @@ module Lutaml
|
|
|
19
19
|
# not emit Instrumentation events — the work did not happen.
|
|
20
20
|
#
|
|
21
21
|
# Semantics (deliberate):
|
|
22
|
+
# - Growth is unbounded by default: every distinct input digs its
|
|
23
|
+
# own entry, and nothing evicts until the store does. Bound the
|
|
24
|
+
# store — lutaml-store's memory adapter accepts max_entries —
|
|
25
|
+
# via Config.conversion_cache before enabling this in
|
|
26
|
+
# long-lived batch processes.
|
|
22
27
|
# - `from_*` hits return the same cached instance for identical
|
|
23
28
|
# input — across callers and threads. Treat results as read-only;
|
|
24
29
|
# classes whose callers mutate parse results must not opt in.
|