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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc1bb0f95876011fac44764d7ea55c334d24ea2a20911e46f4200448cf50cc1e
|
|
4
|
+
data.tar.gz: 23f02dbcf3dac25a19a50f4a8bf75b81d642b64514c0aeb9a820d727be63406f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d649b8eef84ef0cfa60ec5fec9e70b0fa45cdc44508325f12c7a17d66e3ca437535c3bf56bc725217f73d305046c9102d7dba067148f4efdec8996d2f2f8ec2
|
|
7
|
+
data.tar.gz: d81707bac3fb9bf9ebc3091c421253e3b9b8abd085891e03debcd19674af75ae5e6dc5f1b724fbb05e160847694dcc8752e9eb1fb17425a394c1babc67777273
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Smoke test for the INSTALLED lutaml-model gem (parsanol-ruby
|
|
4
|
+
# pattern, lutaml-model#833): runs against the gem's own load paths —
|
|
5
|
+
# never the repo — and exercises the boot surface a packaging
|
|
6
|
+
# regression would break: every format round-trip and the Opal boot
|
|
7
|
+
# manifest's autoload coverage.
|
|
8
|
+
|
|
9
|
+
gem_root = Gem::Specification.find_by_name("lutaml-model").full_gem_path
|
|
10
|
+
raise "installed lutaml-model not found" unless gem_root
|
|
11
|
+
|
|
12
|
+
$LOAD_PATH.unshift(File.join(gem_root, "lib"))
|
|
13
|
+
require "lutaml/model"
|
|
14
|
+
|
|
15
|
+
# Boot manifest: every autoload in lib/compat/opal/lutaml_model_boot.rb
|
|
16
|
+
# must resolve from the installed gem alone.
|
|
17
|
+
boot = File.join(gem_root, "lib", "compat", "opal", "lutaml_model_boot.rb")
|
|
18
|
+
if File.exist?(boot)
|
|
19
|
+
manifest = File.read(boot)
|
|
20
|
+
missing = manifest.scan(/autoload[^\n]*["']([^"']+)["']/).flatten.select do |path|
|
|
21
|
+
!File.exist?(File.join(gem_root, "lib", path))
|
|
22
|
+
end
|
|
23
|
+
raise "boot manifest references missing files: #{missing.inspect}" unless missing.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Smoke < Lutaml::Model::Serializable
|
|
27
|
+
attribute :name, :string
|
|
28
|
+
attribute :count, :integer
|
|
29
|
+
attribute :tags, :string, collection: true
|
|
30
|
+
|
|
31
|
+
xml do
|
|
32
|
+
root "smoke"
|
|
33
|
+
map_attribute "name", to: :name
|
|
34
|
+
map_element "count", to: :count
|
|
35
|
+
map_element "tag", to: :tags
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
require "tempfile"
|
|
40
|
+
model = Smoke.new(name: "smoke", count: 3, tags: %w[a b])
|
|
41
|
+
|
|
42
|
+
round_trips = {
|
|
43
|
+
xml: -> { Smoke.from_xml(model.to_xml) },
|
|
44
|
+
json: -> { Smoke.from_json(model.to_json) },
|
|
45
|
+
yaml: -> { Smoke.from_yaml(model.to_yaml) },
|
|
46
|
+
toml: -> { Smoke.from_toml(model.to_toml) },
|
|
47
|
+
hash: -> { Smoke.from_hash(model.to_hash) },
|
|
48
|
+
}
|
|
49
|
+
round_trips.each do |format, parse|
|
|
50
|
+
round = parse.call
|
|
51
|
+
raise "#{format}: name lost" if round.name != "smoke"
|
|
52
|
+
raise "#{format}: count lost" if round.count != 3
|
|
53
|
+
raise "#{format}: tags lost" if Array(round.tags).sort != %w[a b]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts "installed-gem smoke OK (#{gem_root}, " \
|
|
57
|
+
"lutaml-model #{Lutaml::Model::VERSION}, " \
|
|
58
|
+
"#{round_trips.keys.join("/")})"
|
|
@@ -17,9 +17,16 @@ permissions:
|
|
|
17
17
|
jobs:
|
|
18
18
|
benchmark:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
|
-
timeout-minutes:
|
|
20
|
+
timeout-minutes: 60
|
|
21
21
|
strategy:
|
|
22
22
|
fail-fast: false
|
|
23
|
+
# Serializes the corpus legs: concurrent legs on shared runners
|
|
24
|
+
# produce contention measurements that misfire the absolute gates
|
|
25
|
+
# (lutaml-model#834: niso 0.921s vs a 0.25s main-passing gate).
|
|
26
|
+
# max-parallel, NOT a job-level concurrency group: with a
|
|
27
|
+
# matrix, every leg lands in the same group and GitHub cancels
|
|
28
|
+
# four of the five at start.
|
|
29
|
+
max-parallel: 1
|
|
23
30
|
matrix:
|
|
24
31
|
include:
|
|
25
32
|
- downstream: xmi
|
|
@@ -109,7 +116,16 @@ jobs:
|
|
|
109
116
|
File.write("Gemfile", out.join)
|
|
110
117
|
' /tmp/main-checkout
|
|
111
118
|
bundle install --jobs=4
|
|
112
|
-
|
|
119
|
+
# lutaml-model#834: min-of-N across attempts — contention
|
|
120
|
+
# inflates single-shot timings, so run three attempts and let
|
|
121
|
+
# the merge keep the least-interfered measurement per fixture.
|
|
122
|
+
for i in 1 2 3; do
|
|
123
|
+
bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb || \
|
|
124
|
+
echo "::warning::baseline attempt $i errored (#834) — continuing with remaining attempts"
|
|
125
|
+
cp "$BENCH_JSON" "$BENCH_JSON.attempt$i" 2>/dev/null || true
|
|
126
|
+
done
|
|
127
|
+
ruby ${{ github.workspace }}/bench/bench_merge_attempts.rb \
|
|
128
|
+
"$BENCH_JSON" "$BENCH_JSON.attempt1" "$BENCH_JSON.attempt2" "$BENCH_JSON.attempt3"
|
|
113
129
|
|
|
114
130
|
- name: Run current benchmark (PR branch)
|
|
115
131
|
env:
|
|
@@ -117,6 +133,9 @@ jobs:
|
|
|
117
133
|
${{ matrix.env_key }}: ${{ github.workspace }}/downstream
|
|
118
134
|
LUTAML_MODEL_DIR: ${{ github.workspace }}
|
|
119
135
|
BENCH_JSON: /tmp/bench_current_${{ matrix.downstream }}.json
|
|
136
|
+
# TODO.perf 17: corpus opt-out histogram in the job log —
|
|
137
|
+
# which clauses (if any) keep this corpus interpretive.
|
|
138
|
+
PLAN_COMPILE_STATS: "1"
|
|
120
139
|
run: |
|
|
121
140
|
cd downstream
|
|
122
141
|
ruby -e '
|
|
@@ -134,7 +153,13 @@ jobs:
|
|
|
134
153
|
File.write("Gemfile", out.join)
|
|
135
154
|
' ${{ github.workspace }}
|
|
136
155
|
bundle install --jobs=4
|
|
137
|
-
|
|
156
|
+
for i in 1 2 3; do
|
|
157
|
+
bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb || \
|
|
158
|
+
echo "::warning::current attempt $i errored (#834) — continuing with remaining attempts"
|
|
159
|
+
cp "$BENCH_JSON" "$BENCH_JSON.attempt$i" 2>/dev/null || true
|
|
160
|
+
done
|
|
161
|
+
ruby ${{ github.workspace }}/bench/bench_merge_attempts.rb \
|
|
162
|
+
"$BENCH_JSON" "$BENCH_JSON.attempt1" "$BENCH_JSON.attempt2" "$BENCH_JSON.attempt3"
|
|
138
163
|
|
|
139
164
|
- name: Compare baseline vs current
|
|
140
165
|
run: |
|
data/.github/workflows/opal.yml
CHANGED
|
@@ -9,9 +9,18 @@ on:
|
|
|
9
9
|
permissions:
|
|
10
10
|
contents: read
|
|
11
11
|
|
|
12
|
+
concurrency:
|
|
13
|
+
group: opal-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
12
16
|
jobs:
|
|
13
17
|
test:
|
|
14
18
|
runs-on: ubuntu-latest
|
|
19
|
+
env:
|
|
20
|
+
# The Opal leg is the ONLY consumer of the vendored
|
|
21
|
+
# Opal-compatibility forks (#835 step 2): MRI legs run released
|
|
22
|
+
# oga/ruby-ll.
|
|
23
|
+
LUTAML_OPAL_FORKS: "1"
|
|
15
24
|
steps:
|
|
16
25
|
- uses: actions/checkout@v6
|
|
17
26
|
with:
|
data/.github/workflows/rake.yml
CHANGED
|
@@ -23,10 +23,13 @@ jobs:
|
|
|
23
23
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
24
24
|
ruby: ["3.3", "3.4", "4.0"]
|
|
25
25
|
experimental: [false]
|
|
26
|
-
|
|
26
|
+
# ruby head runs on ubuntu/macos only: windows-head cannot build
|
|
27
|
+
# the released ruby-ll C extension (and the vendored forks exist
|
|
28
|
+
# for Opal), so that cell was permanently red with zero signal
|
|
29
|
+
# (#832 follow-up).
|
|
30
|
+
exclude:
|
|
27
31
|
- os: windows-latest
|
|
28
32
|
ruby: head
|
|
29
|
-
experimental: true
|
|
30
33
|
steps:
|
|
31
34
|
- uses: actions/checkout@v6
|
|
32
35
|
with:
|
|
@@ -36,50 +39,12 @@ jobs:
|
|
|
36
39
|
uses: ruby/setup-ruby@v1
|
|
37
40
|
with:
|
|
38
41
|
ruby-version: ${{ matrix.ruby }}
|
|
39
|
-
# bundler-cache disabled:
|
|
40
|
-
# need ragel + ruby-ll outputs generated (rake vendor:prepare)
|
|
41
|
-
# before their C extensions can compile.
|
|
42
|
+
# bundler-cache disabled: adapter pins resolve per run.
|
|
42
43
|
bundler-cache: false
|
|
43
44
|
|
|
44
|
-
- name: Install ragel (Linux)
|
|
45
|
-
if: runner.os == 'Linux'
|
|
46
|
-
run: sudo apt-get update && sudo apt-get install -y ragel
|
|
47
|
-
|
|
48
|
-
- name: Install ragel (macOS)
|
|
49
|
-
if: runner.os == 'macOS'
|
|
50
|
-
run: brew install ragel
|
|
51
|
-
|
|
52
|
-
- name: Install ragel (Windows)
|
|
53
|
-
if: runner.os == 'Windows'
|
|
54
|
-
run: choco install -y ragel
|
|
55
|
-
continue-on-error: true
|
|
56
|
-
|
|
57
|
-
- name: Install ruby-ll (for rake vendor:prepare)
|
|
58
|
-
run: gem install ruby-ll --no-document
|
|
59
|
-
|
|
60
|
-
- name: Generate ragel / ruby-ll outputs in vendored forks
|
|
61
|
-
# Run before bundle install: the path-source oga/ruby-ll forks
|
|
62
|
-
# gitignore their generated .rb/.c files. bundle install compiles
|
|
63
|
-
# the C extensions via each fork's extconf.rb, which needs
|
|
64
|
-
# ext/c/lexer.c to exist.
|
|
65
|
-
run: rake vendor:prepare
|
|
66
|
-
|
|
67
|
-
- name: Configure bundler
|
|
68
|
-
# ruby-ll's C extension needs an older C standard on some compilers.
|
|
69
|
-
run: |
|
|
70
|
-
mkdir -p .bundle
|
|
71
|
-
echo '---' > .bundle/config
|
|
72
|
-
echo 'BUNDLE_BUILD__RUBY___LL: "--with-cflags=-std=gnu17"' >> .bundle/config
|
|
73
|
-
|
|
74
45
|
- name: Install dependencies
|
|
75
46
|
run: bundle install
|
|
76
47
|
|
|
77
|
-
- name: Compile liboga / libll native extensions
|
|
78
|
-
# Bundler does not reliably build path-source gems' native
|
|
79
|
-
# extensions; compile them explicitly so `require "oga"` (which
|
|
80
|
-
# requires libll) resolves under CRuby.
|
|
81
|
-
run: rake vendor:compile
|
|
82
|
-
|
|
83
48
|
- name: Run tests
|
|
84
49
|
run: bundle exec rake spec
|
|
85
50
|
|
|
@@ -20,6 +20,12 @@ permissions:
|
|
|
20
20
|
contents: write
|
|
21
21
|
id-token: write
|
|
22
22
|
|
|
23
|
+
# One release at a time: concurrent bumps race the version commit
|
|
24
|
+
# and half-published gems are worse than a queued release.
|
|
25
|
+
concurrency:
|
|
26
|
+
group: release
|
|
27
|
+
cancel-in-progress: false
|
|
28
|
+
|
|
23
29
|
jobs:
|
|
24
30
|
release:
|
|
25
31
|
uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
|
|
@@ -33,3 +39,38 @@ jobs:
|
|
|
33
39
|
# pat_token is optional (private-gem checkout + GH Packages). Keep if
|
|
34
40
|
# the org still uses private deps; drop once pure public.
|
|
35
41
|
pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }}
|
|
42
|
+
|
|
43
|
+
# lutaml-model#833: the reusable release pushes the gem without ever
|
|
44
|
+
# installing it — packaging regressions (gemspec globs, autoload
|
|
45
|
+
# drift, fresh-bundler resolution) pass in-repo and break consumers.
|
|
46
|
+
# Install the just-published version from rubygems.org and exercise
|
|
47
|
+
# the boot surface from the gem's own load paths.
|
|
48
|
+
smoke:
|
|
49
|
+
needs: release
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v6
|
|
53
|
+
# The release job commits the version bump to main while this job
|
|
54
|
+
# runs; a checkout of the dispatch-time HEAD reads the previous
|
|
55
|
+
# version and smoke-tests the wrong gem (0.8.60 was smoke-checked
|
|
56
|
+
# for the 0.8.61 release). Pull post-bump main before resolving.
|
|
57
|
+
with:
|
|
58
|
+
fetch-depth: 5
|
|
59
|
+
- uses: ruby/setup-ruby@v1
|
|
60
|
+
with:
|
|
61
|
+
ruby-version: "3.4"
|
|
62
|
+
- name: Resolve the released version
|
|
63
|
+
id: version
|
|
64
|
+
# From the version file, not git describe: the default checkout
|
|
65
|
+
# is shallow with no tags, so git describe failed and the empty
|
|
66
|
+
# version produced `gem install "lutaml-model:"`.
|
|
67
|
+
run: |
|
|
68
|
+
git pull --ff-only origin main
|
|
69
|
+
echo "version=$(ruby -e 'print File.read(%q{lib/lutaml/model/version.rb})[/VERSION\s*=\s*"([^"]+)"/, 1]')" >> "$GITHUB_OUTPUT"
|
|
70
|
+
- name: Install and smoke the released gem
|
|
71
|
+
run: |
|
|
72
|
+
gem install "lutaml-model:${{ steps.version.outputs.version }}" --no-document
|
|
73
|
+
# The round-trip script exercises all five formats; the TOML
|
|
74
|
+
# legs require an adapter gem to be present.
|
|
75
|
+
gem install tomlib toml-rb --no-document
|
|
76
|
+
ruby .github/scripts/test_installed_gem.rb
|
data/Gemfile
CHANGED
|
@@ -10,8 +10,17 @@ gemspec
|
|
|
10
10
|
# in lib/oga.rb / lib/ll/setup.rb that selects the pure-Ruby implementation
|
|
11
11
|
# when RUBY_PLATFORM == 'opal'. Under CRuby/JRuby the forks behave
|
|
12
12
|
# identically to upstream (the conditional falls through to liboga/libll).
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# The vendored forks are Opal-compatibility shims (pure-Ruby lexer)
|
|
14
|
+
# used by the Opal CI leg ONLY (LUTAML_OPAL_FORKS=1 in opal.yml) —
|
|
15
|
+
# #835 step 2. Every MRI leg runs against released oga/ruby-ll; the
|
|
16
|
+
# forks' C extension is upstream's and adds nothing under MRI.
|
|
17
|
+
if ENV["LUTAML_OPAL_FORKS"]
|
|
18
|
+
gem "oga", path: "vendor/opal-oga"
|
|
19
|
+
gem "ruby-ll", path: "vendor/opal-ruby-ll"
|
|
20
|
+
else
|
|
21
|
+
gem "oga"
|
|
22
|
+
gem "ruby-ll"
|
|
23
|
+
end
|
|
15
24
|
|
|
16
25
|
# needed for liquid with ruby 3.4
|
|
17
26
|
gem "base64"
|
|
@@ -35,7 +44,12 @@ gem "rexml"
|
|
|
35
44
|
# Native engines stay development-only: every adapter detects and
|
|
36
45
|
# degrades gracefully at runtime (safe requires + prefer-if-available).
|
|
37
46
|
# yeptris ships no Windows prebuilt platform gem.
|
|
38
|
-
|
|
47
|
+
# leptris floor: the single capability line for the plan fast path
|
|
48
|
+
# (see Lutaml::Xml::Leptris::MIN_LEPTRIS_VERSION) — child snapshots,
|
|
49
|
+
# exact-URI attribute rows, nested-attribute capture, wildcard rows,
|
|
50
|
+
# walk-side ns retention and leniency, :unqualified rows, and the
|
|
51
|
+
# address-based text attach all ride it.
|
|
52
|
+
gem "leptris", ">= 1.9.336.0"
|
|
39
53
|
gem "teptris"
|
|
40
54
|
gem "yeptris" unless Gem.win_platform?
|
|
41
55
|
# TODO: revert rng branch to main when lutaml/rng#32 is merged
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module AllocAudit
|
|
6
|
+
class Row < Lutaml::Model::Serializable
|
|
7
|
+
attribute :id, :integer
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
|
|
10
|
+
xml do
|
|
11
|
+
root "row"
|
|
12
|
+
map_element "id", to: :id
|
|
13
|
+
map_element "name", to: :name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
json do
|
|
17
|
+
map "id", to: :id
|
|
18
|
+
map "name", to: :name
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Doc < Lutaml::Model::Serializable
|
|
23
|
+
attribute :rows, AllocAudit::Row, collection: true
|
|
24
|
+
|
|
25
|
+
xml do
|
|
26
|
+
root "doc"
|
|
27
|
+
map_element "row", to: :rows
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
json do
|
|
31
|
+
map "rows", to: :rows
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
DOC = Doc.new(rows: Array.new(500) { |i| Row.new(id: i, name: "n#{i}") })
|
|
36
|
+
|
|
37
|
+
def self.alloc
|
|
38
|
+
GC.start
|
|
39
|
+
before = GC.stat(:total_allocated_objects)
|
|
40
|
+
yield
|
|
41
|
+
GC.stat(:total_allocated_objects) - before
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
3.times do
|
|
45
|
+
DOC.to_xml
|
|
46
|
+
DOC.to_json
|
|
47
|
+
DOC.to_hash
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
puts "rows=500"
|
|
51
|
+
puts "to_xml_alloc=#{alloc { DOC.to_xml }}"
|
|
52
|
+
puts "to_json_alloc=#{alloc { DOC.to_json }}"
|
|
53
|
+
puts "to_hash_alloc=#{alloc { DOC.to_hash }}"
|
|
54
|
+
puts "from_xml_alloc=#{alloc { Doc.from_xml(DOC.to_xml) }}"
|
|
55
|
+
puts "from_json_alloc=#{alloc { Doc.from_json(DOC.to_json) }}"
|
|
56
|
+
end
|
data/bench/bench_common.rb
CHANGED
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
# bundle exec ruby bench/bench_uniword.rb
|
|
13
13
|
|
|
14
14
|
require "bundler/setup"
|
|
15
|
+
|
|
16
|
+
# TODO.perf/15: run any benchmark with PLAN_COMPILE_STATS=1 to append
|
|
17
|
+
# the plan-compiler opt-out histogram (which clauses keep a corpus on
|
|
18
|
+
# the interpretive path) after the benchmark output.
|
|
19
|
+
if ENV["PLAN_COMPILE_STATS"]
|
|
20
|
+
at_exit do
|
|
21
|
+
require "pp"
|
|
22
|
+
puts "\nPlanCompiler opt-out histogram:"
|
|
23
|
+
pp(Lutaml::Xml::Leptris::PlanCompiler.plan_stats)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
15
26
|
require "benchmark"
|
|
16
27
|
require "fileutils"
|
|
17
28
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lutaml-model#834: merge N benchmark-attempt JSONs into one result set,
|
|
4
|
+
# taking the per-fixture minimum of every metric. Contention inflates
|
|
5
|
+
# timings; the minimum across attempts is the least-interfered
|
|
6
|
+
# measurement, so gates evaluated on merged output misfire only when
|
|
7
|
+
# EVERY attempt was slow.
|
|
8
|
+
#
|
|
9
|
+
# Usage: ruby bench_merge_attempts.rb <merged.json> <attempt1.json> [attempt2.json ...]
|
|
10
|
+
#
|
|
11
|
+
# Attempt files may be missing (crashed attempt): missing files are
|
|
12
|
+
# skipped; if none exist, nothing is written and the caller's compare
|
|
13
|
+
# step fails on the missing merged file.
|
|
14
|
+
if ARGV.length < 3
|
|
15
|
+
warn "Usage: #{$PROGRAM_NAME} <merged.json> <attempt1.json> [attempt2.json ...]"
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
merged_path = ARGV[0]
|
|
20
|
+
attempt_paths = ARGV[1..]
|
|
21
|
+
|
|
22
|
+
require "json"
|
|
23
|
+
|
|
24
|
+
best = {}
|
|
25
|
+
attempt_paths.each do |path|
|
|
26
|
+
next unless File.exist?(path)
|
|
27
|
+
|
|
28
|
+
results = JSON.parse(File.read(path))
|
|
29
|
+
results.each do |label, metrics|
|
|
30
|
+
cur = best[label]
|
|
31
|
+
if cur
|
|
32
|
+
cur["avg_time"] = [cur["avg_time"], metrics["avg_time"]].min
|
|
33
|
+
cur["min_time"] = [cur["min_time"], metrics["min_time"]].min
|
|
34
|
+
cur["max_time"] = [cur["max_time"], metrics["max_time"]].min
|
|
35
|
+
cur["allocations"] = [cur["allocations"], metrics["allocations"]].min
|
|
36
|
+
cur["ips"] = [cur["ips"], metrics["ips"]].max
|
|
37
|
+
else
|
|
38
|
+
best[label] = metrics.dup
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if best.nil? || best.empty?
|
|
44
|
+
warn "No attempt JSONs found among: #{attempt_paths.join(', ')}"
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
File.write(merged_path, JSON.pretty_generate(best))
|
|
49
|
+
puts "Merged #{attempt_paths.size} attempt file(s) into #{merged_path} (#{best.size} fixtures)"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Integrating lutaml-model in a Gem
|
|
3
|
+
nav_order: 21
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
= Integrating lutaml-model in a gem
|
|
7
|
+
|
|
8
|
+
For gem authors, the integration contract is simple: **depend on
|
|
9
|
+
`lutaml-model` and nothing else format-specific.** Every XML, YAML,
|
|
10
|
+
JSON, and TOML engine is resolved at runtime, and the standard
|
|
11
|
+
adapters (Nokogiri through moxml, Psych, the `json` gem, toml-rb
|
|
12
|
+
compatible engines) cover a working default everywhere.
|
|
13
|
+
|
|
14
|
+
== Declaring the dependency
|
|
15
|
+
|
|
16
|
+
[source,ruby]
|
|
17
|
+
----
|
|
18
|
+
spec.add_dependency "lutaml-model", "~> 0.8"
|
|
19
|
+
----
|
|
20
|
+
|
|
21
|
+
Do not add Nokogiri, Ox, Oga, leptris, yeptris, or teptris to your
|
|
22
|
+
gemspec. All engines are optional at runtime:
|
|
23
|
+
|
|
24
|
+
* when an engine gem is in the end user's bundle, the matching
|
|
25
|
+
adapters prefer it (see the
|
|
26
|
+
link:../native-engines[native engines guide]);
|
|
27
|
+
* when it is not, adapters fall back to the standard engine for
|
|
28
|
+
that format — parsing and generation semantics are identical,
|
|
29
|
+
error classes are re-raised as their standard-library equivalents.
|
|
30
|
+
|
|
31
|
+
There is no code path to write, no require order to respect, and no
|
|
32
|
+
capability probe to perform in your gem.
|
|
33
|
+
|
|
34
|
+
== Testing across adapters
|
|
35
|
+
|
|
36
|
+
Your test suite should exercise the adapters your users may run.
|
|
37
|
+
Lutaml::Model's own suite parameterizes the XML adapter through an
|
|
38
|
+
environment variable; the same shape works downstream:
|
|
39
|
+
|
|
40
|
+
[source,ruby]
|
|
41
|
+
----
|
|
42
|
+
ADAPTER = ENV.fetch("XML_ADAPTER", "nokogiri").to_sym
|
|
43
|
+
|
|
44
|
+
RSpec.configure do |config|
|
|
45
|
+
config.before(:suite) do
|
|
46
|
+
Lutaml::Model::Config.xml_adapter_type = ADAPTER
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
----
|
|
50
|
+
|
|
51
|
+
Run the matrix in CI — one leg per adapter the standard runners can
|
|
52
|
+
install:
|
|
53
|
+
|
|
54
|
+
[source,sh]
|
|
55
|
+
----
|
|
56
|
+
XML_ADAPTER=nokogiri bundle exec rspec
|
|
57
|
+
XML_ADAPTER=ox bundle exec rspec
|
|
58
|
+
XML_ADAPTER=leptris bundle exec rspec # requires the leptris gem in that leg's bundle
|
|
59
|
+
----
|
|
60
|
+
|
|
61
|
+
Engine gems belong in the CI *Gemfile* (a `group :test` entry or a
|
|
62
|
+
separate CI gemfile), never in the gemspec. Start with the primary
|
|
63
|
+
adapter (Nokogiri) and enable further legs incrementally as they
|
|
64
|
+
pass — the same "adapter-first" strategy this library's own suite
|
|
65
|
+
uses.
|
|
66
|
+
|
|
67
|
+
== Semantics your users can rely on
|
|
68
|
+
|
|
69
|
+
* Adapter choice never changes results: the plan fast path and the
|
|
70
|
+
interpretive pipeline produce identical output; on engines below
|
|
71
|
+
a capability floor the affected models parse interpretively
|
|
72
|
+
rather than degrade (see the version-floor note in the
|
|
73
|
+
link:../native-engines[native engines guide]).
|
|
74
|
+
* Parse failures raise the standard error classes wrapped as
|
|
75
|
+
`Lutaml::Model::InvalidFormatError` on every engine — code that
|
|
76
|
+
rescues those classes works regardless of the bundle.
|
|
77
|
+
|
|
78
|
+
== Rules of thumb
|
|
79
|
+
|
|
80
|
+
* Gemspec: `lutaml-model` only. CI gemfiles: the engines you test.
|
|
81
|
+
* Expose format options you want to forward (pretty printing,
|
|
82
|
+
encoding) — never the adapter itself.
|
|
83
|
+
* If you ship models for multiple schema versions, see
|
|
84
|
+
link:../type-registers-contexts[type registers and contexts] for
|
|
85
|
+
per-version trees and substitution.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
= Grammar-backed string formats
|
|
2
|
+
|
|
3
|
+
A serialization format whose input is a *grammar*, not a data structure:
|
|
4
|
+
identifier strings, query languages, DSLs — text that is parsed, not
|
|
5
|
+
decoded. lutaml-model supports registering these as first-class formats
|
|
6
|
+
through the same `FormatRegistry` extension point as xml/json/yaml; the
|
|
7
|
+
parse side is implemented by a grammar engine (the reference
|
|
8
|
+
implementation is https://github.com/parsanol/parsanol-ruby[Parsanol]'s
|
|
9
|
+
`Parsanol::PARG::Lutaml.register`, which consumes a compiled PARG
|
|
10
|
+
artifact plus its bindings).
|
|
11
|
+
|
|
12
|
+
== The contract
|
|
13
|
+
|
|
14
|
+
A grammar-backed format needs three things:
|
|
15
|
+
|
|
16
|
+
* a *parse* — text → attribute hash (the grammar engine's job)
|
|
17
|
+
* *bindings* — capture names → model attributes (the artifact's
|
|
18
|
+
bindings section)
|
|
19
|
+
* a *model class* — a regular `Serializable` with attributes; no
|
|
20
|
+
hand-written `from_h`/`to_h` anywhere
|
|
21
|
+
|
|
22
|
+
== Reference implementation (Parsanol)
|
|
23
|
+
|
|
24
|
+
[source,ruby]
|
|
25
|
+
----
|
|
26
|
+
require "parsanol/parg/lutaml"
|
|
27
|
+
|
|
28
|
+
Parsanol::PARG::Lutaml.register(
|
|
29
|
+
IsoIdentifier,
|
|
30
|
+
format_name: :pubid_iso,
|
|
31
|
+
artifact: "artifacts/iso.json", # compiled grammar + bindings + tests
|
|
32
|
+
entry: "identifier",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
IsoIdentifier.from_pubid_iso("ISO/IEC 12345-1:2020")
|
|
36
|
+
# => #<IsoIdentifier number: "12345-1", year: 2020, ...>
|
|
37
|
+
----
|
|
38
|
+
|
|
39
|
+
What `register` does:
|
|
40
|
+
|
|
41
|
+
. loads the checksummed artifact (parse failures raise the engine's
|
|
42
|
+
error type — register it via `error_types:` so users can rescue it);
|
|
43
|
+
. registers the format through `FormatRegistry.register` with an
|
|
44
|
+
adapter whose `parse` is artifact-parse-then-bind;
|
|
45
|
+
. defines `from_<format>` on the model class.
|
|
46
|
+
|
|
47
|
+
The artifact is the parser of record: it carries the grammar, the
|
|
48
|
+
bindings, and the conformance tests (the grammar's own test block is
|
|
49
|
+
baked in and can be re-run by consumers), and its checksum pins the
|
|
50
|
+
pinned version of the language.
|
|
51
|
+
|
|
52
|
+
== Writing the grammar side
|
|
53
|
+
|
|
54
|
+
Grammars are authored in PARG (`.parg` — ABNF-flavoured surface, PEG
|
|
55
|
+
semantics, one rule per line) and compiled with the `parsanol` CLI:
|
|
56
|
+
|
|
57
|
+
----
|
|
58
|
+
parsanol compile grammars/iso.parg --tables tables
|
|
59
|
+
----
|
|
60
|
+
|
|
61
|
+
The compile-time lint rejects left recursion and shadowed
|
|
62
|
+
alternatives; the embedded test block runs at compile time, so a
|
|
63
|
+
grammar that stops accepting its documented inputs fails the build
|
|
64
|
+
rather than the consumer.
|
|
65
|
+
|
|
66
|
+
== See also
|
|
67
|
+
|
|
68
|
+
* parsanol-ruby `docs/pg-language-guide.md` — the PARG language
|
|
69
|
+
* `Parsanol::PARG::Lutaml.register` — the bridge (reference implementation)
|
data/docs/_guides/index.adoc
CHANGED
|
@@ -26,9 +26,15 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
|
|
|
26
26
|
== Advanced mapping
|
|
27
27
|
|
|
28
28
|
* link:../advanced-mapping[Advanced Attribute Mapping] - Same-name dispatch on attribute values (`when_attribute`), grouped declarations, custom methods, delegation
|
|
29
|
+
* link:../grammar-backed-formats[Grammar-Backed String Formats] - Register a grammar engine as a serialization format (identifier strings, DSLs)
|
|
29
30
|
|
|
30
|
-
==
|
|
31
|
+
== Model architecture and integration
|
|
32
|
+
|
|
33
|
+
* link:../type-registers-contexts[Type Registers and Contexts] - Multi-version class trees, derived contexts, and model substitution
|
|
34
|
+
* link:../gem-integration[Integrating lutaml-model in a Gem] - Dependency contract, adapter deferral, cross-adapter testing
|
|
31
35
|
|
|
36
|
+
== Performance
|
|
37
|
+
/
|
|
32
38
|
* link:../native-engines[Native engines (leptris / yeptris / teptris)] - Drop-in XML/YAML/JSON/TOML engine upgrades via bundle opt-in
|
|
33
39
|
|
|
34
40
|
== Linked Data serialization
|
|
@@ -47,6 +53,7 @@ See link:../references/rdf-namespaces[RDF Namespaces] for namespace classes,
|
|
|
47
53
|
* link:../value-transformations[Value Transformations] - Transform values during serialization
|
|
48
54
|
* link:../missing-values-handling[Missing Values Handling] - Handle nil/empty/omitted
|
|
49
55
|
* link:../advanced-mapping[Advanced Mapping] - Custom methods and delegation
|
|
56
|
+
* link:../grammar-backed-formats[Grammar-Backed String Formats] - Grammar engines as serialization formats
|
|
50
57
|
* link:../schema-generation[Schema Generation] - Generate schemas from models
|
|
51
58
|
* link:../creating-xsd[Creating XSD Schemas] - Complete reference for XSD generation from LutaML models
|
|
52
59
|
* link:../schema-import[Schema Import] - Import schemas to create models
|