lutaml-model 0.8.33 → 0.8.34
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/.rubocop.yml +5 -0
- data/Gemfile +4 -0
- data/README.adoc +39 -0
- data/docs/conversion_caching.adoc +143 -0
- data/lib/compat/opal/lutaml_model_boot.rb +15 -0
- data/lib/lutaml/model/attribute.rb +89 -0
- data/lib/lutaml/model/config.rb +13 -0
- data/lib/lutaml/model/configuration.rb +23 -0
- data/lib/lutaml/model/error/fraction_digits_error.rb +20 -0
- data/lib/lutaml/model/error/length_error.rb +19 -0
- data/lib/lutaml/model/error/max_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/max_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/max_length_error.rb +20 -0
- data/lib/lutaml/model/error/min_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/min_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/min_length_error.rb +20 -0
- data/lib/lutaml/model/error/restriction_error.rb +8 -0
- data/lib/lutaml/model/error/total_digits_error.rb +20 -0
- data/lib/lutaml/model/error/type/max_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type.rb +2 -0
- data/lib/lutaml/model/restriction_validation.rb +437 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +4 -2
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +134 -4
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +3 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +9 -1
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +2 -0
- data/lib/lutaml/model/schema/templates.rb +2 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +39 -7
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +3 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +18 -7
- data/lib/lutaml/model/serialize/builder.rb +4 -5
- data/lib/lutaml/model/serialize/conversion_caching.rb +124 -0
- data/lib/lutaml/model/serialize/enum_handling.rb +50 -11
- data/lib/lutaml/model/serialize/format_conversion.rb +46 -38
- data/lib/lutaml/model/serialize/initialization.rb +4 -2
- data/lib/lutaml/model/serialize.rb +61 -0
- data/lib/lutaml/model/services/type/validator.rb +23 -0
- data/lib/lutaml/model/type/string.rb +28 -3
- data/lib/lutaml/model/type/value.rb +231 -0
- data/lib/lutaml/model/type.rb +2 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +16 -0
- data/lib/lutaml/xml/model_transform.rb +4 -1
- data/lib/lutaml/xml/schema/xsd/max_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/min_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd_schema.rb +183 -11
- data/lib/lutaml/xml/transformation/order_reconciler.rb +267 -0
- data/lib/lutaml/xml/transformation/ordered_applier.rb +80 -122
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
- data/lib/lutaml/xml/transformation.rb +2 -0
- data/lib/lutaml/xml/transformation_support.rb +1 -0
- data/spec/fixtures/xml/restriction_facets.xsd +46 -0
- data/spec/lutaml/model/attribute_restriction_spec.rb +472 -0
- data/spec/lutaml/model/attribute_spec.rb +7 -2
- data/spec/lutaml/model/collection_reader_liveness_spec.rb +516 -0
- data/spec/lutaml/model/conversion_caching_spec.rb +280 -0
- data/spec/lutaml/model/lazy_collection_spec.rb +28 -3
- data/spec/lutaml/model/parsed_model_mutation_spec.rb +486 -0
- data/spec/lutaml/model/phantom_value_spec.rb +799 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +1 -0
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +340 -695
- data/spec/lutaml/model/serialize_perf_guard_spec.rb +53 -1
- data/spec/lutaml/model/type_digit_facet_spec.rb +229 -0
- data/spec/lutaml/model/type_facet_restriction_spec.rb +667 -0
- data/spec/lutaml/model/type_white_space_facet_spec.rb +132 -0
- data/spec/lutaml/xml/schema/compiler_spec.rb +302 -0
- data/spec/lutaml/xml/schema/facet_round_trip_spec.rb +153 -0
- metadata +27 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65361e31ef80bfd3600c0bc3ea4241c2b9a9e9d227b1772b9bd23f3324e92ad7
|
|
4
|
+
data.tar.gz: da258d5127e14511cd77bd1ca44e6ad1647aaaf445cc14a92102096f180a82a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d5ece44cac496100e30c2d4f3c5ca3934f656bd81352ee9215e57c2a4b8563f0a7a8ae3c155909eaf132efcce992e1954731b89cd79808fcf8c948555b06abd
|
|
7
|
+
data.tar.gz: 9db5fc0ac630ad33336250d06a953357d8a2d60bb0f0cae037d5a2d9d6cc12da2fca2937aa5b73c5b490852381995937fad226049e51fd167906e900084fa837
|
data/.rubocop.yml
CHANGED
|
@@ -32,6 +32,11 @@ RSpec/EmptyExampleGroup:
|
|
|
32
32
|
Exclude:
|
|
33
33
|
- 'spec/lutaml/jsonld/transform_spec.rb'
|
|
34
34
|
|
|
35
|
+
# emits_nothing_extra carries the expectations for these examples.
|
|
36
|
+
RSpec/NoExpectationExample:
|
|
37
|
+
AllowedPatterns:
|
|
38
|
+
- '^emits_nothing_extra'
|
|
39
|
+
|
|
35
40
|
RSpec/NamedSubject:
|
|
36
41
|
inherit_mode:
|
|
37
42
|
merge:
|
data/Gemfile
CHANGED
|
@@ -20,6 +20,10 @@ gem "bigdecimal"
|
|
|
20
20
|
gem "canon" # , path: "../canon"
|
|
21
21
|
gem "json-ld"
|
|
22
22
|
gem "liquid", "~> 5"
|
|
23
|
+
# lutaml-store depends on lutaml-model, so it can never be a gemspec dependency.
|
|
24
|
+
# 0.2.4 relaxes rubyzip to >= 2.3, < 4 (lutaml/lutaml-store#5), co-resolving
|
|
25
|
+
# with lutaml-model's rubyzip ~> 3.4 CVE floor.
|
|
26
|
+
gem "lutaml-store", "~> 0.2.4"
|
|
23
27
|
gem "multi_json"
|
|
24
28
|
gem "nokogiri"
|
|
25
29
|
gem "oj"
|
data/README.adoc
CHANGED
|
@@ -67,6 +67,7 @@ link:docs/migration-guides/0-1-0-migrate-from-shale.adoc[Migrating from Shale to
|
|
|
67
67
|
* Reusable XML mapping classes for sharing mappings across models
|
|
68
68
|
* RDF namespace classes and JSON-LD/Turtle format adapters for Linked Data serialization
|
|
69
69
|
* Consolidation mapping: group sibling XML elements into structured model instances (see <<consolidation-mapping>>)
|
|
70
|
+
* Opt-in caching of repeated serialization/deserialization, backed by the `lutaml-store` gem (see <<conversion-caching>>)
|
|
70
71
|
* Document-level validation framework with composable rules, profiles, and remediation (see <<document-validation-framework>>)
|
|
71
72
|
|
|
72
73
|
|
|
@@ -16547,6 +16548,44 @@ raised to indicate that the input format is malformed and cannot be parsed.
|
|
|
16547
16548
|
|
|
16548
16549
|
|
|
16549
16550
|
|
|
16551
|
+
[[conversion-caching]]
|
|
16552
|
+
== Conversion caching
|
|
16553
|
+
|
|
16554
|
+
Repeated serialization/deserialization of the same content can be cached.
|
|
16555
|
+
A model class opts in with `cache_conversions`; the application provides the
|
|
16556
|
+
storage backend by adding the
|
|
16557
|
+
link:https://github.com/lutaml/lutaml-store[lutaml-store] gem to its
|
|
16558
|
+
`Gemfile` (lutaml-model itself does not depend on it). Without both, nothing
|
|
16559
|
+
changes.
|
|
16560
|
+
|
|
16561
|
+
[source,ruby]
|
|
16562
|
+
----
|
|
16563
|
+
require "lutaml/store"
|
|
16564
|
+
|
|
16565
|
+
class Invoice < Lutaml::Model::Serializable
|
|
16566
|
+
attribute :number, :string
|
|
16567
|
+
|
|
16568
|
+
cache_conversions
|
|
16569
|
+
|
|
16570
|
+
xml do
|
|
16571
|
+
root "invoice"
|
|
16572
|
+
map_element "number", to: :number
|
|
16573
|
+
end
|
|
16574
|
+
end
|
|
16575
|
+
|
|
16576
|
+
Invoice.from_xml(xml) # parsed once
|
|
16577
|
+
Invoice.from_xml(xml) # the same instance, served from the cache
|
|
16578
|
+
----
|
|
16579
|
+
|
|
16580
|
+
When `Lutaml::Store` is loaded, a memory-backed `Lutaml::Store::BasicStore`
|
|
16581
|
+
is created automatically on first use. Assign
|
|
16582
|
+
`Lutaml::Model::Config.conversion_cache` to use a custom backend (e.g.
|
|
16583
|
+
`Lutaml::Store::CacheStore` with TTL/LRU, or any object responding to
|
|
16584
|
+
`get`/`set`), or set it to `false` to disable caching globally.
|
|
16585
|
+
|
|
16586
|
+
See link:docs/conversion_caching.adoc[Conversion caching] for activation
|
|
16587
|
+
rules, backend configuration, cache-key semantics, and limitations.
|
|
16588
|
+
|
|
16550
16589
|
[[custom-adapters]]
|
|
16551
16590
|
== Custom serialization adapters
|
|
16552
16591
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
= LutaML Model — Conversion caching
|
|
2
|
+
:doctype: book
|
|
3
|
+
|
|
4
|
+
== Overview
|
|
5
|
+
|
|
6
|
+
LutaML Model can cache the results of format conversions to avoid redundant
|
|
7
|
+
processing when the same content is converted repeatedly:
|
|
8
|
+
|
|
9
|
+
- *Deserialization* (`from_xml`, `from_json`, ...): repeated parsing of the
|
|
10
|
+
same input string returns the cached model instance instead of re-parsing.
|
|
11
|
+
- *Serialization* (`to_xml`, `to_json`, ...): repeated serialization of an
|
|
12
|
+
unchanged instance returns the cached output string.
|
|
13
|
+
|
|
14
|
+
Caching is opt-in twice over: a model class declares it, and the application
|
|
15
|
+
provides a storage backend (the
|
|
16
|
+
link:https://github.com/lutaml/lutaml-store[lutaml-store] gem, or any
|
|
17
|
+
compatible object). Without both, nothing changes — classes that do not opt
|
|
18
|
+
in are never cached, and `cache_conversions` without a backend is a no-op.
|
|
19
|
+
|
|
20
|
+
== Quick start
|
|
21
|
+
|
|
22
|
+
Add the backend gem to your application's `Gemfile` (lutaml-model itself does
|
|
23
|
+
not depend on it):
|
|
24
|
+
|
|
25
|
+
[source,ruby]
|
|
26
|
+
----
|
|
27
|
+
gem "lutaml-store"
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
Require it, and declare caching on the models that need it:
|
|
31
|
+
|
|
32
|
+
[source,ruby]
|
|
33
|
+
----
|
|
34
|
+
require "lutaml/model"
|
|
35
|
+
require "lutaml/store"
|
|
36
|
+
|
|
37
|
+
class Invoice < Lutaml::Model::Serializable
|
|
38
|
+
attribute :number, :string
|
|
39
|
+
attribute :amount, :decimal
|
|
40
|
+
|
|
41
|
+
cache_conversions
|
|
42
|
+
|
|
43
|
+
xml do
|
|
44
|
+
root "invoice"
|
|
45
|
+
map_element "number", to: :number
|
|
46
|
+
map_element "amount", to: :amount
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Invoice.from_xml(xml) # parsed once
|
|
51
|
+
Invoice.from_xml(xml) # the same instance, served from the cache
|
|
52
|
+
|
|
53
|
+
invoice = Invoice.new(number: "42", amount: 10)
|
|
54
|
+
invoice.to_xml # serialized once
|
|
55
|
+
invoice.to_xml # served from the cache
|
|
56
|
+
----
|
|
57
|
+
|
|
58
|
+
That is all: when `Lutaml::Store` is loaded, the first conversion call on an
|
|
59
|
+
opted-in class lazily creates a default memory-backed
|
|
60
|
+
`Lutaml::Store::BasicStore` and caching is active from then on.
|
|
61
|
+
|
|
62
|
+
== Activation rules
|
|
63
|
+
|
|
64
|
+
[cols="1,1,1", options="header"]
|
|
65
|
+
|===
|
|
66
|
+
| Class declares `cache_conversions` | Backend available | Result
|
|
67
|
+
|
|
68
|
+
| no | — | never cached
|
|
69
|
+
| yes | no store loaded or assigned | no-op (works as without caching)
|
|
70
|
+
| yes | lutaml-store loaded | cached via auto-created memory `BasicStore`
|
|
71
|
+
| yes | store assigned via config | cached via that store
|
|
72
|
+
| any | `Config.conversion_cache = false` | caching disabled globally
|
|
73
|
+
|===
|
|
74
|
+
|
|
75
|
+
Subclasses inherit the declaration:
|
|
76
|
+
|
|
77
|
+
[source,ruby]
|
|
78
|
+
----
|
|
79
|
+
class SignedInvoice < Invoice
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
SignedInvoice.conversion_caching_enabled? # => true
|
|
83
|
+
----
|
|
84
|
+
|
|
85
|
+
== Configuring the backend
|
|
86
|
+
|
|
87
|
+
The default (memory `BasicStore`, unbounded) suits scripts and CLI runs. For
|
|
88
|
+
long-running processes, assign a bounded or custom store:
|
|
89
|
+
|
|
90
|
+
[source,ruby]
|
|
91
|
+
----
|
|
92
|
+
Lutaml::Model::Config.configure do |config|
|
|
93
|
+
# TTL + LRU eviction; suitable for to_* output strings.
|
|
94
|
+
config.conversion_cache = Lutaml::Store::CacheStore.new(
|
|
95
|
+
adapter: { type: :memory },
|
|
96
|
+
default_ttl: 600,
|
|
97
|
+
max_size: 1000,
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Disable caching entirely (even with lutaml-store loaded):
|
|
102
|
+
Lutaml::Model::Config.conversion_cache = false
|
|
103
|
+
|
|
104
|
+
# Return to auto-detection:
|
|
105
|
+
Lutaml::Model::Config.conversion_cache = nil
|
|
106
|
+
----
|
|
107
|
+
|
|
108
|
+
Any object responding to `get(key)` and `set(key, value)` works as a backend.
|
|
109
|
+
|
|
110
|
+
NOTE: `Lutaml::Store::CacheStore` JSON-marshals stored values, so it
|
|
111
|
+
accelerates serialization output (strings survive the round-trip) while
|
|
112
|
+
deserialized model instances are recomputed on every call — correct, just
|
|
113
|
+
uncached. Use `BasicStore` to cache both directions.
|
|
114
|
+
|
|
115
|
+
== Semantics and guarantees
|
|
116
|
+
|
|
117
|
+
Correctness first:: Cache keys are content-derived — they digest the input
|
|
118
|
+
string (deserialization) or the instance state (serialization), plus the
|
|
119
|
+
class, format, register, resolved adapter, and any conversion options.
|
|
120
|
+
Mutating an instance changes its key, so stale output is never served.
|
|
121
|
+
Anything that cannot be keyed safely bypasses the cache and is computed
|
|
122
|
+
normally rather than risking a wrong result.
|
|
123
|
+
|
|
124
|
+
Shared instances:: Repeated deserialization of identical input returns the
|
|
125
|
+
*same* object — across callers and threads. Treat cached results as
|
|
126
|
+
read-only. If your code mutates parsed results, do not declare
|
|
127
|
+
`cache_conversions` on that class.
|
|
128
|
+
|
|
129
|
+
What bypasses the cache:: Non-`String` inputs (`Pathname`, `IO` — the content
|
|
130
|
+
lives elsewhere); non-`Hash` options (e.g. the generator state objects Ruby's
|
|
131
|
+
JSON/YAML protocols pass); instance graphs `Marshal` cannot digest — notably,
|
|
132
|
+
instances parsed from XML retain native parser nodes for round-trip fidelity
|
|
133
|
+
and therefore skip serialization caching (programmatically built instances
|
|
134
|
+
cache fully); and any environment without native `Marshal`/`Digest` (Opal).
|
|
135
|
+
|
|
136
|
+
Invalidation:: Entirely the store's concern — TTL, eviction, persistence, and
|
|
137
|
+
clearing belong to the backend you configured. If you mutate a register's
|
|
138
|
+
mappings at runtime, clear or replace the store afterwards; structural
|
|
139
|
+
changes are not propagated automatically.
|
|
140
|
+
|
|
141
|
+
Errors:: Backend exceptions propagate unchanged so operational problems stay
|
|
142
|
+
visible; malformed input still raises
|
|
143
|
+
`Lutaml::Model::InvalidFormatError` exactly as without caching.
|
|
@@ -78,6 +78,7 @@ require "lutaml/model/organization"
|
|
|
78
78
|
require "lutaml/model/register"
|
|
79
79
|
require "lutaml/model/registrable"
|
|
80
80
|
require "lutaml/model/render_policy"
|
|
81
|
+
require "lutaml/model/restriction_validation"
|
|
81
82
|
require "lutaml/model/runtime_compatibility"
|
|
82
83
|
require "lutaml/model/schema"
|
|
83
84
|
require "lutaml/model/schema_location"
|
|
@@ -235,6 +236,7 @@ require "lutaml/model/error/collection_count_out_of_range_error"
|
|
|
235
236
|
require "lutaml/model/error/collection_true_missing_error"
|
|
236
237
|
require "lutaml/model/error/element_count_out_of_range_error"
|
|
237
238
|
require "lutaml/model/error/format_adapter_not_specified_error"
|
|
239
|
+
require "lutaml/model/error/fraction_digits_error"
|
|
238
240
|
require "lutaml/model/error/import_model_with_root_error"
|
|
239
241
|
require "lutaml/model/error/incorrect_mapping_argument_error"
|
|
240
242
|
require "lutaml/model/error/incorrect_sequence_error"
|
|
@@ -244,12 +246,19 @@ require "lutaml/model/error/invalid_attribute_type_error"
|
|
|
244
246
|
require "lutaml/model/error/invalid_choice_range_error"
|
|
245
247
|
require "lutaml/model/error/invalid_format_error"
|
|
246
248
|
require "lutaml/model/error/invalid_value_error"
|
|
249
|
+
require "lutaml/model/error/length_error"
|
|
247
250
|
require "lutaml/model/error/liquid_class_not_found_error"
|
|
248
251
|
require "lutaml/model/error/liquid_drop_already_registered_error"
|
|
249
252
|
require "lutaml/model/error/liquid_not_enabled_error"
|
|
250
253
|
require "lutaml/model/error/mapping_already_exists_error"
|
|
251
254
|
require "lutaml/model/error/mapping_attribute_missing_error"
|
|
252
255
|
require "lutaml/model/error/mapping_attribute_type_error"
|
|
256
|
+
require "lutaml/model/error/max_exclusive_error"
|
|
257
|
+
require "lutaml/model/error/max_inclusive_error"
|
|
258
|
+
require "lutaml/model/error/max_length_error"
|
|
259
|
+
require "lutaml/model/error/min_exclusive_error"
|
|
260
|
+
require "lutaml/model/error/min_inclusive_error"
|
|
261
|
+
require "lutaml/model/error/min_length_error"
|
|
253
262
|
require "lutaml/model/error/mixed_content_collection_error"
|
|
254
263
|
require "lutaml/model/error/multiple_mappings_error"
|
|
255
264
|
require "lutaml/model/error/no_attributes_defined_liquid_error"
|
|
@@ -260,9 +269,11 @@ require "lutaml/model/error/ordered_content_mapping_error"
|
|
|
260
269
|
require "lutaml/model/error/pattern_not_matched_error"
|
|
261
270
|
require "lutaml/model/error/polymorphic_error"
|
|
262
271
|
require "lutaml/model/error/required_attribute_missing_error"
|
|
272
|
+
require "lutaml/model/error/restriction_error"
|
|
263
273
|
require "lutaml/model/error/reverse_transform_block_not_defined_error"
|
|
264
274
|
require "lutaml/model/error/reverse_transformation_declaration_error"
|
|
265
275
|
require "lutaml/model/error/sorting_configuration_conflict_error"
|
|
276
|
+
require "lutaml/model/error/total_digits_error"
|
|
266
277
|
require "lutaml/model/error/transform_block_not_defined_error"
|
|
267
278
|
require "lutaml/model/error/type_error"
|
|
268
279
|
require "lutaml/model/error/type_not_enabled_error"
|
|
@@ -304,6 +315,7 @@ require "lutaml/model/schema/templates"
|
|
|
304
315
|
require "lutaml/model/schema/yaml_schema"
|
|
305
316
|
require "lutaml/model/serialize/attribute_definition"
|
|
306
317
|
require "lutaml/model/serialize/builder"
|
|
318
|
+
require "lutaml/model/serialize/conversion_caching"
|
|
307
319
|
require "lutaml/model/serialize/deserialization_context"
|
|
308
320
|
require "lutaml/model/serialize/enum_handling"
|
|
309
321
|
require "lutaml/model/serialize/format_conversion"
|
|
@@ -380,6 +392,7 @@ require "lutaml/xml/serialization/instance_methods"
|
|
|
380
392
|
require "lutaml/xml/serialization/model_import_ext"
|
|
381
393
|
require "lutaml/xml/transformation/custom_method_wrapper"
|
|
382
394
|
require "lutaml/xml/transformation/element_builder"
|
|
395
|
+
require "lutaml/xml/transformation/order_reconciler"
|
|
383
396
|
require "lutaml/xml/transformation/ordered_applier"
|
|
384
397
|
require "lutaml/xml/transformation/rule_applier"
|
|
385
398
|
require "lutaml/xml/transformation/rule_compiler"
|
|
@@ -437,8 +450,10 @@ require "lutaml/key_value/adapter/yamls/transform"
|
|
|
437
450
|
require "lutaml/model/error/register/not_registrable_class_error"
|
|
438
451
|
require "lutaml/model/error/type/invalid_value_error"
|
|
439
452
|
require "lutaml/model/error/type/max_bound_error"
|
|
453
|
+
require "lutaml/model/error/type/max_exclusive_error"
|
|
440
454
|
require "lutaml/model/error/type/max_length_error"
|
|
441
455
|
require "lutaml/model/error/type/min_bound_error"
|
|
456
|
+
require "lutaml/model/error/type/min_exclusive_error"
|
|
442
457
|
require "lutaml/model/error/type/min_length_error"
|
|
443
458
|
require "lutaml/model/error/type/pattern_not_matched_error"
|
|
444
459
|
require "lutaml/model/schema/decorators/attribute"
|
|
@@ -7,6 +7,7 @@ module Lutaml
|
|
|
7
7
|
|
|
8
8
|
include CollectionHandler
|
|
9
9
|
include DeepDupable
|
|
10
|
+
include RestrictionValidation
|
|
10
11
|
|
|
11
12
|
ALLOWED_OPTIONS = %i[
|
|
12
13
|
raw
|
|
@@ -29,6 +30,11 @@ module Lutaml
|
|
|
29
30
|
ref_key_attribute
|
|
30
31
|
xsd_type
|
|
31
32
|
union_member_types
|
|
33
|
+
min
|
|
34
|
+
max
|
|
35
|
+
signed
|
|
36
|
+
min_length
|
|
37
|
+
max_length
|
|
32
38
|
].freeze
|
|
33
39
|
|
|
34
40
|
# Per-type-class memo of the custom from_xml/from_json probe used by #cast.
|
|
@@ -344,6 +350,32 @@ module Lutaml
|
|
|
344
350
|
value.map { |v| cast_element(v, register) }
|
|
345
351
|
end
|
|
346
352
|
|
|
353
|
+
# Cast the value a derived reader just recomputed.
|
|
354
|
+
#
|
|
355
|
+
# A derived attribute has no ivar and no writer. Its reader runs the
|
|
356
|
+
# source method and casts the result on every call, which makes the
|
|
357
|
+
# reader a casting entry point of its own — nothing a writer-side guard
|
|
358
|
+
# can reach. A collection still has to come back as a collection here,
|
|
359
|
+
# even when the source method returned nothing.
|
|
360
|
+
#
|
|
361
|
+
# @param value [Object] whatever the source method returned
|
|
362
|
+
# @param register [Symbol] register for type resolution
|
|
363
|
+
# @return [Object] the cast value, or an empty collection
|
|
364
|
+
def cast_derived(value, register)
|
|
365
|
+
return cast_element(value, register) unless collection?
|
|
366
|
+
return build_collection if no_data?(value)
|
|
367
|
+
return cast_value(value, register) if collection_instance?(value)
|
|
368
|
+
# A plain Array is a list of source elements even when the attribute
|
|
369
|
+
# declares its own collection class, which collection_instance? only
|
|
370
|
+
# recognises by that class. Without this it becomes one element whose
|
|
371
|
+
# value is the inspected Array.
|
|
372
|
+
if value.is_a?(::Array)
|
|
373
|
+
return build_collection(value.map { |v| cast_element(v, register) })
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
build_collection(cast_element(value, register))
|
|
377
|
+
end
|
|
378
|
+
|
|
347
379
|
# Apply a value map to transform a value.
|
|
348
380
|
#
|
|
349
381
|
# value_map keys (:nil, :empty, :omitted) each map either to a symbolic
|
|
@@ -389,8 +421,32 @@ module Lutaml
|
|
|
389
421
|
true
|
|
390
422
|
end
|
|
391
423
|
|
|
424
|
+
# True when the value is "nothing arrived" rather than data.
|
|
425
|
+
#
|
|
426
|
+
# nil and the uninitialized sentinel both mean the source carried no
|
|
427
|
+
# value. Neither is something a type can be asked to turn into an
|
|
428
|
+
# instance.
|
|
429
|
+
#
|
|
430
|
+
# @param value [Object]
|
|
431
|
+
# @return [Boolean]
|
|
432
|
+
def no_data?(value)
|
|
433
|
+
value.nil? || Utils.uninitialized?(value)
|
|
434
|
+
end
|
|
435
|
+
|
|
392
436
|
def cast_element(value, register)
|
|
437
|
+
# Resolve first: an undeclared type has to raise UnknownTypeError even
|
|
438
|
+
# when the value carries nothing, the way it did before the guard
|
|
439
|
+
# below existed.
|
|
393
440
|
resolved_type = type(register)
|
|
441
|
+
|
|
442
|
+
# Casting is for data. Every built-in type already hands nil and the
|
|
443
|
+
# uninitialized sentinel straight back, so this changes nothing for
|
|
444
|
+
# them. A type with its own `self.cast` returns a real instance
|
|
445
|
+
# instead, and that instance becomes a value nobody wrote — a phantom
|
|
446
|
+
# element in the document, or a one-item collection the source never
|
|
447
|
+
# contained. `cast` below guards the format entry point the same way.
|
|
448
|
+
return value if no_data?(value)
|
|
449
|
+
|
|
394
450
|
return cast_union(value, nil, register) if union?
|
|
395
451
|
return resolved_type.new(value) if value.is_a?(::Hash) && !hash_type?
|
|
396
452
|
|
|
@@ -570,6 +626,7 @@ instance_object = nil)
|
|
|
570
626
|
register)
|
|
571
627
|
end
|
|
572
628
|
resolved_type = type(register)
|
|
629
|
+
validate_restriction_configuration!(resolved_type)
|
|
573
630
|
|
|
574
631
|
# Shape before contents. An over-count is a cardinality violation, not
|
|
575
632
|
# an enum or pattern one, and checking it first keeps those predicates
|
|
@@ -579,6 +636,8 @@ instance_object = nil)
|
|
|
579
636
|
valid_pattern!(value, resolved_type) &&
|
|
580
637
|
validate_polymorphic!(value, resolved_type) &&
|
|
581
638
|
execute_validations!(value)
|
|
639
|
+
|
|
640
|
+
validate_restriction_values!(value, resolved_type)
|
|
582
641
|
end
|
|
583
642
|
|
|
584
643
|
# execute custom validations on the attribute value
|
|
@@ -724,6 +783,14 @@ instance_object = nil)
|
|
|
724
783
|
end
|
|
725
784
|
|
|
726
785
|
def cast(value, format, register, options = {})
|
|
786
|
+
# Same rule as cast_element, for the format entry point. from_json /
|
|
787
|
+
# from_yaml / from_toml / from_hash reach a missing key as nil, and a
|
|
788
|
+
# type with its own `self.cast` turns that nil into an instance the
|
|
789
|
+
# document never carried. Unlike cast_element this returns before
|
|
790
|
+
# resolving the type: resolution here depends on the format options
|
|
791
|
+
# below, and an undeclared type still raises through cast_element.
|
|
792
|
+
return value if no_data?(value)
|
|
793
|
+
|
|
727
794
|
# Namespace-aware type resolution: use type_with_namespace if namespace_uri provided
|
|
728
795
|
namespace_uri = options[:namespace_uri]
|
|
729
796
|
resolved_type = if options[:resolved_type]
|
|
@@ -1122,6 +1189,28 @@ instance_object = nil)
|
|
|
1122
1189
|
raise StandardError,
|
|
1123
1190
|
"Invalid option `initialize_empty` given without `collection: true` option"
|
|
1124
1191
|
end
|
|
1192
|
+
|
|
1193
|
+
# Length facets are XSD nonNegativeInteger; reject a misconfigured
|
|
1194
|
+
# type here so a String/Float/nil fails fast instead of raising an
|
|
1195
|
+
# opaque TypeError later at `value.length >= min`.
|
|
1196
|
+
%i[min_length max_length].each do |key|
|
|
1197
|
+
next unless options.key?(key)
|
|
1198
|
+
|
|
1199
|
+
value = options[key]
|
|
1200
|
+
next if value.is_a?(::Integer) && !value.negative?
|
|
1201
|
+
|
|
1202
|
+
raise ArgumentError,
|
|
1203
|
+
"Invalid options for `#{name}`: " \
|
|
1204
|
+
"`#{key}` must be a non-negative Integer, got #{value.inspect}"
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
# `signed` is true|false per issue #191; a non-boolean (e.g. "false")
|
|
1208
|
+
# would silently read as signed, so reject it up front.
|
|
1209
|
+
if options.key?(:signed) && ![true, false].include?(options[:signed])
|
|
1210
|
+
raise ArgumentError,
|
|
1211
|
+
"Invalid options for `#{name}`: " \
|
|
1212
|
+
"`signed` must be true or false, got #{options[:signed].inspect}"
|
|
1213
|
+
end
|
|
1125
1214
|
true
|
|
1126
1215
|
end
|
|
1127
1216
|
|
data/lib/lutaml/model/config.rb
CHANGED
|
@@ -92,6 +92,19 @@ module Lutaml
|
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# Store used by classes that declare `cache_conversions`.
|
|
96
|
+
# Duck-typed: anything responding to #get(key) and #set(key, value).
|
|
97
|
+
# Defaults to a memory-backed Lutaml::Store::BasicStore when the
|
|
98
|
+
# lutaml-store gem is loaded; assign false to disable caching, or
|
|
99
|
+
# nil to return to auto-detection.
|
|
100
|
+
def conversion_cache
|
|
101
|
+
instance.conversion_cache
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def conversion_cache=(store)
|
|
105
|
+
instance.conversion_cache = store
|
|
106
|
+
end
|
|
107
|
+
|
|
95
108
|
def mappings_class_for(format)
|
|
96
109
|
instance.mappings_class_for(format)
|
|
97
110
|
end
|
|
@@ -19,9 +19,11 @@ module Lutaml
|
|
|
19
19
|
# compilable XML models (Leptris::XML::Descriptor walk). Opt-in
|
|
20
20
|
# while the full semantics audit completes.
|
|
21
21
|
attr_accessor :xml_plan_fast_path
|
|
22
|
+
attr_writer :conversion_cache
|
|
22
23
|
|
|
23
24
|
def initialize
|
|
24
25
|
@default_register = :default
|
|
26
|
+
@conversion_cache = nil
|
|
25
27
|
@configured = false
|
|
26
28
|
@xml_plan_fast_path = false
|
|
27
29
|
end
|
|
@@ -32,6 +34,18 @@ module Lutaml
|
|
|
32
34
|
self
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
# Store consulted by classes that declare `cache_conversions`.
|
|
38
|
+
# Defaults to a memory-backed Lutaml::Store::BasicStore when the
|
|
39
|
+
# lutaml-store gem is loaded (the application opts in by adding it
|
|
40
|
+
# to its Gemfile and requiring "lutaml/store"). Assign any object
|
|
41
|
+
# responding to #get/#set to override, false to disable caching
|
|
42
|
+
# entirely, or nil to return to auto-detection.
|
|
43
|
+
def conversion_cache
|
|
44
|
+
return if @conversion_cache == false
|
|
45
|
+
|
|
46
|
+
@conversion_cache ||= default_conversion_cache
|
|
47
|
+
end
|
|
48
|
+
|
|
35
49
|
def configured?
|
|
36
50
|
@configured
|
|
37
51
|
end
|
|
@@ -68,6 +82,7 @@ module Lutaml
|
|
|
68
82
|
# Reset configuration to defaults
|
|
69
83
|
def reset!
|
|
70
84
|
@default_register = :default
|
|
85
|
+
@conversion_cache = nil
|
|
71
86
|
@configured = false
|
|
72
87
|
AdapterResolver.reset!
|
|
73
88
|
AdapterScope.reset!
|
|
@@ -91,6 +106,14 @@ module Lutaml
|
|
|
91
106
|
# Adapter accessors are defined dynamically by FormatRegistry.register
|
|
92
107
|
# via define_method. These forward to AdapterResolver.
|
|
93
108
|
# The generic set_adapter/get_adapter handle any format.
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def default_conversion_cache
|
|
113
|
+
return unless defined?(::Lutaml::Store::BasicStore)
|
|
114
|
+
|
|
115
|
+
::Lutaml::Store::BasicStore.new(adapter: { type: :memory })
|
|
116
|
+
end
|
|
94
117
|
end
|
|
95
118
|
end
|
|
96
119
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class FractionDigitsError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, fraction_digits)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@fraction_digits = fraction_digits
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} (#{@value}) exceeds the maximum of " \
|
|
16
|
+
"#{@fraction_digits} fraction digits"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class LengthError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, length)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@length = length
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} length (#{@value.to_s.length}) must be exactly #{@length}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class MaxExclusiveError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, max)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@max = max
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} is `#{@value}`, must be less than #{@max}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class MaxInclusiveError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, max)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@max = max
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} is `#{@value}`, " \
|
|
16
|
+
"must be less than or equal to #{@max}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class MaxLengthError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, max_length)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@max_length = max_length
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} length (#{@value.to_s.length}) is greater than " \
|
|
16
|
+
"the maximum allowed length #{@max_length}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class MinExclusiveError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, min)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@min = min
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} is `#{@value}`, must be greater than #{@min}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class MinInclusiveError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, min)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@min = min
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} is `#{@value}`, " \
|
|
16
|
+
"must be greater than or equal to #{@min}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|