lutaml-model 0.8.34 → 0.8.35
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/.rspec +1 -0
- data/lib/lutaml/model/attribute.rb +6 -4
- data/lib/lutaml/model/register.rb +12 -5
- data/lib/lutaml/model/transformation_registry.rb +13 -7
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter_element.rb +17 -4
- data/spec/benchmarks/xml_parsing_benchmark_spec.rb +70 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a5f36f080c15bb2639142a36b424853d26458732c867963dbf7a99d6a1cd189
|
|
4
|
+
data.tar.gz: 3159ba755e2cc317e5e9b3785b2be1cffb08e424b2bd792bca34f20c853d25be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e83080b05e3277310d103e84c5f63fda7e963e25bb1cdc764934a1d931e388f212b51f97c5a02182966b3e376a97d00fc30fa396568a644100b5745064d1fd1a
|
|
7
|
+
data.tar.gz: f59d08e3915dacf3a7da3c0bfbc2e9b300cb7c66177c6dd4455870349fc4f2cb892d8d34a1028b74f2660b87e21980b44a4e941329fcf8a69687fde9a7d336f0
|
data/.rspec
CHANGED
|
@@ -52,11 +52,13 @@ module Lutaml
|
|
|
52
52
|
# Public reader for compiled rule plans: does this type class carry a
|
|
53
53
|
# custom from_xml/from_json (answer static, probed once)?
|
|
54
54
|
def custom_from_probe?(type)
|
|
55
|
-
TypeProbeCache.cache
|
|
56
|
-
|
|
55
|
+
cache = TypeProbeCache.cache
|
|
56
|
+
return cache[type] if cache.key?(type)
|
|
57
|
+
|
|
58
|
+
base = Lutaml::Model::Type::Value.singleton_class
|
|
59
|
+
cache[type] =
|
|
57
60
|
type.method(:from_xml).owner != base ||
|
|
58
|
-
|
|
59
|
-
end
|
|
61
|
+
type.method(:from_json).owner != base
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
|
|
@@ -80,15 +80,22 @@ module Lutaml
|
|
|
80
80
|
# @param parent_register [Symbol, String, nil] The parent's register/context ID
|
|
81
81
|
# @return [Symbol, nil] The register ID to use for the child
|
|
82
82
|
def self.resolve_for_child(child_class, parent_register)
|
|
83
|
-
cache_key = [child_class.object_id, parent_register]
|
|
84
|
-
|
|
85
83
|
if Lutaml::Model.opal?
|
|
86
|
-
|
|
84
|
+
per_class = RESOLVE_CACHE[child_class] # rubocop:todo Style/IdenticalConditionalBranches
|
|
85
|
+
return per_class[parent_register] if per_class&.key?(parent_register)
|
|
87
86
|
|
|
88
|
-
RESOLVE_CACHE[
|
|
87
|
+
(RESOLVE_CACHE[child_class] ||= {})[parent_register] =
|
|
89
88
|
_resolve_for_child_uncached(child_class, parent_register)
|
|
90
89
|
else
|
|
91
|
-
|
|
90
|
+
# Nested maps: class → register → result. Every hop keys on a
|
|
91
|
+
# stable object (Class / Symbol), so the hot lookup allocates
|
|
92
|
+
# nothing — the flat [object_id, register] array key cost one
|
|
93
|
+
# Array per call, six per parsed element.
|
|
94
|
+
per_class = RESOLVE_CACHE[child_class] # rubocop:todo Style/IdenticalConditionalBranches
|
|
95
|
+
per_class ||= RESOLVE_CACHE.compute_if_absent(child_class) do
|
|
96
|
+
Concurrent::Map.new
|
|
97
|
+
end
|
|
98
|
+
per_class.compute_if_absent(parent_register) do
|
|
92
99
|
_resolve_for_child_uncached(child_class, parent_register)
|
|
93
100
|
end
|
|
94
101
|
end
|
|
@@ -174,25 +174,31 @@ module Lutaml
|
|
|
174
174
|
# @param register [Symbol, Register, nil] The register
|
|
175
175
|
# @return [Mapping] The resolved mapping
|
|
176
176
|
def get_or_build_mapping(model_class, format, register)
|
|
177
|
-
# Performance:
|
|
177
|
+
# Performance: nested class → format → register maps keyed on
|
|
178
|
+
# stable objects; the flat array key allocated per call.
|
|
178
179
|
register_id = extract_register_id(register)
|
|
179
|
-
key = [model_class.object_id, format, register_id]
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
per_class = @mappings[model_class]
|
|
182
|
+
per_format = per_class&.[](format)
|
|
183
|
+
cached = per_format&.[](register_id)
|
|
183
184
|
return cached if cached
|
|
184
185
|
|
|
185
|
-
# Build mapping OUTSIDE any lock to avoid deadlock
|
|
186
|
-
# (ensure_mappings_imported! may recursively call get_or_build_mapping)
|
|
187
186
|
mapping = model_class.mappings[format]
|
|
188
187
|
mapping = mapping || model_class.default_mappings(format)
|
|
189
188
|
|
|
190
189
|
mapping.ensure_mappings_imported!(register_id)
|
|
191
190
|
|
|
192
|
-
@mappings[
|
|
191
|
+
per_class = (@mappings[model_class] ||= new_inner_map)
|
|
192
|
+
(per_class[format] ||= new_inner_map)[register_id] = mapping
|
|
193
193
|
mapping
|
|
194
194
|
end
|
|
195
195
|
|
|
196
|
+
# Inner cache level container matching the outer map's engine
|
|
197
|
+
# (Concurrent::Map under GVL-threaded MRI, plain Hash under Opal).
|
|
198
|
+
def new_inner_map
|
|
199
|
+
Lutaml::Model.opal? ? {} : ::Concurrent::Map.new
|
|
200
|
+
end
|
|
201
|
+
|
|
196
202
|
# Clear all cached data (transformations and mappings).
|
|
197
203
|
#
|
|
198
204
|
# This is useful for testing or when configuration changes dynamically.
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -64,8 +64,8 @@ module Lutaml
|
|
|
64
64
|
name = adapter_class.name_of(node)
|
|
65
65
|
super(
|
|
66
66
|
node,
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
attributes || NO_ATTRIBUTES,
|
|
68
|
+
children || NO_CHILDREN,
|
|
69
69
|
text,
|
|
70
70
|
name: name,
|
|
71
71
|
parent_document: parent,
|
|
@@ -146,8 +146,19 @@ module Lutaml
|
|
|
146
146
|
end
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
+
# Shared frozen empties: the parse path hands these straight into
|
|
150
|
+
# XmlElement (which never mutates the containers it is given), so
|
|
151
|
+
# attribute-less elements — the majority in real documents — build
|
|
152
|
+
# nothing here.
|
|
153
|
+
NO_ATTRIBUTES = {}.freeze # rubocop:todo Lint/UselessConstantScoping
|
|
154
|
+
NO_CHILDREN = [].freeze # rubocop:todo Lint/UselessConstantScoping
|
|
155
|
+
EMPTY_ATTRIBUTES = [{}, nil].freeze # rubocop:todo Lint/UselessConstantScoping
|
|
156
|
+
|
|
149
157
|
def node_attributes_with_order(node)
|
|
150
|
-
return
|
|
158
|
+
return EMPTY_ATTRIBUTES unless node.is_a?(Moxml::Element)
|
|
159
|
+
|
|
160
|
+
attrs = node.attributes
|
|
161
|
+
return EMPTY_ATTRIBUTES if attrs.respond_to?(:empty?) && attrs.empty?
|
|
151
162
|
|
|
152
163
|
order = []
|
|
153
164
|
hash = node.attributes.each_with_object({}) do |attr, h|
|
|
@@ -174,7 +185,7 @@ module Lutaml
|
|
|
174
185
|
end
|
|
175
186
|
|
|
176
187
|
def parse_children(node, default_namespace: nil)
|
|
177
|
-
return
|
|
188
|
+
return NO_CHILDREN unless node.children
|
|
178
189
|
|
|
179
190
|
# Non-element children (text/cdata/comment/PI) stay RAW moxml
|
|
180
191
|
# nodes in the list — the hot scalar parse path reads text off
|
|
@@ -197,6 +208,8 @@ module Lutaml
|
|
|
197
208
|
public
|
|
198
209
|
|
|
199
210
|
def children
|
|
211
|
+
return @children if @children.empty?
|
|
212
|
+
|
|
200
213
|
unless @non_element_children_wrapped
|
|
201
214
|
@children.map! do |child|
|
|
202
215
|
if child.is_a?(Moxml::Node) && !child.is_a?(Moxml::Element)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "benchmark/ips"
|
|
5
|
+
|
|
6
|
+
# Adapter-comparison benchmark, runnable on demand:
|
|
7
|
+
# bundle exec rspec --tag benchmark spec/benchmarks/
|
|
8
|
+
# Tagged out of the default suite (.rsex --tag ~benchmark); asserts
|
|
9
|
+
# nothing about absolute speed — machines differ — but prints i/s and
|
|
10
|
+
# allocations/op so regressions are visible when run deliberately.
|
|
11
|
+
RSpec.describe "XML adapter benchmark", :benchmark do
|
|
12
|
+
Item = Class.new(Lutaml::Model::Serializable) do # rubocop:todo Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
13
|
+
attribute :id, :integer
|
|
14
|
+
attribute :name, :string
|
|
15
|
+
attribute :tags, :string, collection: true
|
|
16
|
+
|
|
17
|
+
xml do
|
|
18
|
+
element "item"
|
|
19
|
+
map_attribute "id", to: :id
|
|
20
|
+
map_element "name", to: :name
|
|
21
|
+
map_element "tag", to: :tags
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Root = Class.new(Lutaml::Model::Serializable) do # rubocop:todo Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
26
|
+
attribute :item, Item, collection: true
|
|
27
|
+
|
|
28
|
+
xml do
|
|
29
|
+
element "root"
|
|
30
|
+
map_element "item", to: :item
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
XML = +"<root>" # rubocop:todo Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
35
|
+
1_000.times { |i| XML << %(<item id="#{i}"><name>Item #{i}</name><tag>a</tag><tag>b</tag></item>) }
|
|
36
|
+
XML << "</root>"
|
|
37
|
+
|
|
38
|
+
def allocations_of
|
|
39
|
+
GC.start
|
|
40
|
+
before = GC.stat(:total_allocated_objects)
|
|
41
|
+
yield
|
|
42
|
+
GC.stat(:total_allocated_objects) - before
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
%i[nokogiri ox oga rexml leptris].each do |adapter|
|
|
46
|
+
it "#{adapter}: from_xml/to_xml i/s and allocations" do
|
|
47
|
+
model = nil
|
|
48
|
+
Lutaml::Model::Config.with_adapter(xml: adapter) do
|
|
49
|
+
begin
|
|
50
|
+
Lutaml::Model::Config.adapter_for(:xml)
|
|
51
|
+
rescue Lutaml::Model::UnknownAdapterTypeError
|
|
52
|
+
skip "#{adapter} adapter unavailable on this platform"
|
|
53
|
+
end
|
|
54
|
+
report = Benchmark.ips(time: 2, warmup: 1, quiet: true) do |x|
|
|
55
|
+
x.report("parse") { model = Root.from_xml(XML) }
|
|
56
|
+
x.report("serialize") { model.to_xml }
|
|
57
|
+
end
|
|
58
|
+
parse_ips = report.entries.first.ips
|
|
59
|
+
serialize_ips = report.entries.last.ips
|
|
60
|
+
|
|
61
|
+
allocs = allocations_of { Root.from_xml(XML) }
|
|
62
|
+
puts format( # rubocop:todo RSpec/Output
|
|
63
|
+
"%<a>-9s parse %<p>7.1f i/s serialize %<s>7.1f i/s %<al>8d allocs/op",
|
|
64
|
+
a: adapter, p: parse_ips, s: serialize_ips, al: allocs,
|
|
65
|
+
)
|
|
66
|
+
expect(model.item.size).to eq(1_000)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.35
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -971,6 +971,7 @@ files:
|
|
|
971
971
|
- sig/lutaml/model.rbs
|
|
972
972
|
- spec/address_person_spec.rb
|
|
973
973
|
- spec/address_spec.rb
|
|
974
|
+
- spec/benchmarks/xml_parsing_benchmark_spec.rb
|
|
974
975
|
- spec/ceramic_spec.rb
|
|
975
976
|
- spec/fixtures/address.rb
|
|
976
977
|
- spec/fixtures/ceramic.rb
|