lutaml-model 0.8.2 → 0.8.4

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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +23 -23
  3. data/README.adoc +213 -1
  4. data/docs/_guides/document-validation.adoc +303 -0
  5. data/docs/_guides/index.adoc +1 -0
  6. data/docs/_guides/xml-mapping.adoc +9 -1
  7. data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
  8. data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
  9. data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
  10. data/lib/lutaml/model/attribute.rb +19 -1
  11. data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
  12. data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
  13. data/lib/lutaml/model/global_context.rb +1 -0
  14. data/lib/lutaml/model/liquefiable.rb +12 -15
  15. data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
  16. data/lib/lutaml/model/mapping_hash.rb +1 -1
  17. data/lib/lutaml/model/services/transformer.rb +67 -32
  18. data/lib/lutaml/model/transform.rb +41 -4
  19. data/lib/lutaml/model/uninitialized_class.rb +11 -5
  20. data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
  21. data/lib/lutaml/model/validation/context.rb +36 -0
  22. data/lib/lutaml/model/validation/issue.rb +62 -0
  23. data/lib/lutaml/model/validation/layer_result.rb +34 -0
  24. data/lib/lutaml/model/validation/profile.rb +66 -0
  25. data/lib/lutaml/model/validation/registry.rb +60 -0
  26. data/lib/lutaml/model/validation/remediation.rb +33 -0
  27. data/lib/lutaml/model/validation/remediation_result.rb +20 -0
  28. data/lib/lutaml/model/validation/report.rb +39 -0
  29. data/lib/lutaml/model/validation/rule.rb +59 -0
  30. data/lib/lutaml/model/validation.rb +2 -1
  31. data/lib/lutaml/model/validation_framework.rb +77 -0
  32. data/lib/lutaml/model/version.rb +1 -1
  33. data/lib/lutaml/model.rb +4 -0
  34. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
  35. data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
  36. data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
  37. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
  38. data/lib/lutaml/xml/adapter_element.rb +26 -2
  39. data/lib/lutaml/xml/data_model.rb +14 -0
  40. data/lib/lutaml/xml/document.rb +3 -0
  41. data/lib/lutaml/xml/element.rb +8 -2
  42. data/lib/lutaml/xml/mapping.rb +9 -0
  43. data/lib/lutaml/xml/model_transform.rb +42 -0
  44. data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
  45. data/lib/lutaml/xml/schema/xsd/schema_path.rb +6 -0
  46. data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
  47. data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
  48. data/lib/lutaml/xml/transformation.rb +40 -1
  49. data/lib/lutaml/xml/xml_element.rb +8 -7
  50. data/lutaml-model.gemspec +1 -2
  51. data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
  52. data/spec/lutaml/model/liquefiable_spec.rb +22 -6
  53. data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
  54. data/spec/lutaml/model/ordered_content_spec.rb +5 -5
  55. data/spec/lutaml/model/services/transformer_spec.rb +43 -0
  56. data/spec/lutaml/model/transform_cache_spec.rb +62 -0
  57. data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
  58. data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
  59. data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
  60. data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
  61. data/spec/lutaml/model/validation/context_spec.rb +60 -0
  62. data/spec/lutaml/model/validation/issue_spec.rb +77 -0
  63. data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
  64. data/spec/lutaml/model/validation/profile_spec.rb +134 -0
  65. data/spec/lutaml/model/validation/registry_spec.rb +94 -0
  66. data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
  67. data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
  68. data/spec/lutaml/model/validation/report_spec.rb +58 -0
  69. data/spec/lutaml/model/validation/rule_spec.rb +134 -0
  70. data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
  71. data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
  72. data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
  73. data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
  74. data/spec/lutaml/xml/mapping_spec.rb +12 -7
  75. data/spec/lutaml/xml/schema/xsd/glob_spec.rb +12 -0
  76. metadata +46 -21
  77. data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6eb717e543e98728e8819eef97823780156ce2f594c360f678b9d23f9baf230
4
- data.tar.gz: 64906eed0282afa83f0905db2e3a75e607a22bacf5e9e40cafa3c0007bdc8fec
3
+ metadata.gz: c512d678682ea756cb7eb7a3d439a7d511b763405936e598900f42f7f07aa705
4
+ data.tar.gz: e6623585797f809090619a9e6268e026773d962cac0f940ccae608627d8e714c
5
5
  SHA512:
6
- metadata.gz: 62b3ca87767a8b942d78b22800b6765467532a194484c18f494bc29ff558adb57f2fcf92f7ea5e2bba5a05cac811c39d807c40885c247190452b33556ccf78f8
7
- data.tar.gz: 1771fd424b8c31e9da2c6940b750c80af2ac38b4ae05e02d234894ee9df5cd5ad4dbd6a9486bbbee0060a3f956507a98a3bfdcc6dd1d6a074957f544649638ee
6
+ metadata.gz: 81558e237a6a7f55c3defee45c8fa8a1d59bf75a875159708065ce8db44308fdaa17d3c727d6ff1074477391ec199008088221f54d5bbb7f2b3b4546ff4d25cf
7
+ data.tar.gz: 242730ac59628f328fe07409cc54d7798e75b40d29b6b47ad7382de37a033d9f907d0578d8ab950c45c9ff1b06cd67f6cb98e3e6215948149e63e8e4591be15d
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-05-03 22:38:22 UTC using RuboCop version 1.86.1.
3
+ # on 2026-05-06 04:21:39 UTC using RuboCop version 1.86.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -11,15 +11,7 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'lutaml-model.gemspec'
13
13
 
14
- # Offense count: 2
15
- # This cop supports safe autocorrection (--autocorrect).
16
- # Configuration parameters: AllowForAlignment.
17
- Layout/CommentIndentation:
18
- Exclude:
19
- - 'lib/lutaml/model/mapping/listener.rb'
20
- - 'lib/lutaml/model/services/base.rb'
21
-
22
- # Offense count: 3210
14
+ # Offense count: 3232
23
15
  # This cop supports safe autocorrection (--autocorrect).
24
16
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
25
17
  # URISchemes: http, https
@@ -42,10 +34,16 @@ Lint/ConstantDefinitionInBlock:
42
34
  Lint/DuplicateBranch:
43
35
  Enabled: false
44
36
 
45
- # Offense count: 3
37
+ # Offense count: 22
46
38
  Lint/DuplicateMethods:
47
39
  Exclude:
48
40
  - 'lib/lutaml/xml/mapping.rb'
41
+ - 'spec/lutaml/model/liquid_compatibility_spec.rb'
42
+ - 'spec/lutaml/xml/namespace_no_hoisting_spec.rb'
43
+ - 'spec/lutaml/xml/namespace_scope_declare_spec.rb'
44
+ - 'spec/lutaml/xml/namespace_scope_vcard_spec.rb'
45
+ - 'spec/lutaml/xml/prefix_control_spec.rb'
46
+ - 'spec/lutaml/xml/type_namespace_examples_spec.rb'
49
47
 
50
48
  # Offense count: 1
51
49
  # This cop supports safe autocorrection (--autocorrect).
@@ -118,7 +116,7 @@ Lint/UselessConstantScoping:
118
116
  - 'lib/lutaml/xml/adapter/nokogiri_adapter.rb'
119
117
  - 'lib/lutaml/xml/mapping_rule.rb'
120
118
 
121
- # Offense count: 340
119
+ # Offense count: 346
122
120
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
123
121
  Metrics/AbcSize:
124
122
  Enabled: false
@@ -134,23 +132,23 @@ Metrics/BlockLength:
134
132
  Metrics/BlockNesting:
135
133
  Max: 6
136
134
 
137
- # Offense count: 301
135
+ # Offense count: 309
138
136
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
139
137
  Metrics/CyclomaticComplexity:
140
138
  Enabled: false
141
139
 
142
- # Offense count: 551
140
+ # Offense count: 562
143
141
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
144
142
  Metrics/MethodLength:
145
143
  Max: 514
146
144
 
147
- # Offense count: 85
145
+ # Offense count: 86
148
146
  # Configuration parameters: CountKeywordArgs.
149
147
  Metrics/ParameterLists:
150
148
  Max: 24
151
149
  MaxOptionalParameters: 5
152
150
 
153
- # Offense count: 255
151
+ # Offense count: 260
154
152
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
155
153
  Metrics/PerceivedComplexity:
156
154
  Enabled: false
@@ -242,7 +240,7 @@ RSpec/BeforeAfterAll:
242
240
  RSpec/ContextWording:
243
241
  Enabled: false
244
242
 
245
- # Offense count: 81
243
+ # Offense count: 88
246
244
  # Configuration parameters: IgnoredMetadata.
247
245
  RSpec/DescribeClass:
248
246
  Enabled: false
@@ -253,7 +251,7 @@ RSpec/DescribeMethod:
253
251
  - 'spec/lutaml/xml/schema/xsd/schema_helper_methods_spec.rb'
254
252
  - 'spec/lutaml/xml/serializable_namespace_spec.rb'
255
253
 
256
- # Offense count: 1164
254
+ # Offense count: 1207
257
255
  # Configuration parameters: CountAsOne.
258
256
  RSpec/ExampleLength:
259
257
  Max: 68
@@ -328,11 +326,11 @@ RSpec/MultipleDescribes:
328
326
  - 'spec/lutaml/xml/namespace_resolution_strategy_spec.rb'
329
327
  - 'spec/lutaml/xml/xml_space_type_spec.rb'
330
328
 
331
- # Offense count: 1336
329
+ # Offense count: 1383
332
330
  RSpec/MultipleExpectations:
333
331
  Max: 21
334
332
 
335
- # Offense count: 189
333
+ # Offense count: 190
336
334
  # Configuration parameters: AllowSubject.
337
335
  RSpec/MultipleMemoizedHelpers:
338
336
  Max: 15
@@ -376,13 +374,13 @@ RSpec/RepeatedExampleGroupDescription:
376
374
  Exclude:
377
375
  - 'spec/lutaml/model/mixed_content_spec.rb'
378
376
 
379
- # Offense count: 32
377
+ # Offense count: 35
380
378
  # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
381
379
  # SupportedInflectors: default, active_support
382
380
  RSpec/SpecFilePathFormat:
383
381
  Enabled: false
384
382
 
385
- # Offense count: 31
383
+ # Offense count: 32
386
384
  # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
387
385
  RSpec/VerifiedDoubles:
388
386
  Exclude:
@@ -393,6 +391,7 @@ RSpec/VerifiedDoubles:
393
391
  - 'spec/lutaml/model/schema/renderer_spec.rb'
394
392
  - 'spec/lutaml/model/transformation_builder_spec.rb'
395
393
  - 'spec/lutaml/model/transformation_spec.rb'
394
+ - 'spec/lutaml/model/validation/context_spec.rb'
396
395
  - 'spec/lutaml/xml/namespace_resolution_strategy_spec.rb'
397
396
 
398
397
  # Offense count: 1
@@ -400,13 +399,14 @@ Security/MarshalLoad:
400
399
  Exclude:
401
400
  - 'scripts-xmi-profile/profile_xmi.rb'
402
401
 
403
- # Offense count: 1
402
+ # Offense count: 2
404
403
  # This cop supports unsafe autocorrection (--autocorrect-all).
405
404
  # Configuration parameters: AllowedMethods, AllowedPatterns.
406
405
  # AllowedMethods: ==, equal?, eql?
407
406
  Style/ClassEqualityComparison:
408
407
  Exclude:
409
408
  - 'lib/lutaml/model/attribute.rb'
409
+ - 'lib/lutaml/model/validation/profile.rb'
410
410
 
411
411
  # Offense count: 13
412
412
  # This cop supports safe autocorrection (--autocorrect).
data/README.adoc CHANGED
@@ -66,6 +66,7 @@ link:docs/migration-guides/0-1-0-migrate-from-shale.adoc[Migrating from Shale to
66
66
  * Parse XSD schemas into Ruby objects for inspection and manipulation (see <<xsd-schema-parsing>>)
67
67
  * Reusable XML mapping classes for sharing mappings across models
68
68
  * Consolidation mapping: group sibling XML elements into structured model instances (see <<consolidation-mapping>>)
69
+ * Document-level validation framework with composable rules, profiles, and remediation (see <<document-validation-framework>>)
69
70
 
70
71
 
71
72
  == Data modeling in a nutshell
@@ -6349,7 +6350,8 @@ end
6349
6350
  ====
6350
6351
  When a model has `mixed_content`, use `map_content` with `collection: true`
6351
6352
  to capture the multiple text segments between child elements.
6352
- Without `collection: true`, only the first (or last) text segment is captured.
6353
+ Without `collection: true`, a `MixedContentCollectionError` is raised at
6354
+ finalization time.
6353
6355
  ====
6354
6356
 
6355
6357
  .Complete round-trip with `mixed_content` and `map_content collection: true`
@@ -6498,6 +6500,51 @@ Without `mixed_content`, serialization groups all text first then all elements
6498
6500
  (or vice versa), losing the original interleaving. This is the most common
6499
6501
  cause of XML round-trip failures in document-processing applications.
6500
6502
 
6503
+ ===== XML Comment round-trip preservation
6504
+
6505
+ XML comments (`<!-- ... -->`) are preserved during round-trip serialization
6506
+ in `mixed_content` and `ordered` modes. Comments appear in `element_order`
6507
+ as entries with `node_type: :comment` and are reconstructed during output.
6508
+
6509
+ .Comment round-trip in mixed content
6510
+ [example]
6511
+ ====
6512
+ [source,ruby]
6513
+ ----
6514
+ class Paragraph < Lutaml::Model::Serializable
6515
+ attribute :content, :string, collection: true
6516
+
6517
+ xml do
6518
+ element 'p'
6519
+ mixed_content
6520
+ map_content to: :content
6521
+ end
6522
+ end
6523
+ ----
6524
+
6525
+ Input XML with a comment between text and an element:
6526
+
6527
+ [source,xml]
6528
+ ----
6529
+ <p>Text before<!-- a comment --> text after</p>
6530
+ ----
6531
+
6532
+ [source,ruby]
6533
+ ----
6534
+ parsed = Paragraph.from_xml(xml)
6535
+ parsed.element_order
6536
+ # => [#<Lutaml::Xml::Element type: "Text", node_type: :text>,
6537
+ # #<Lutaml::Xml::Element type: "Comment", node_type: :comment, text_content: " a comment ">,
6538
+ # #<Lutaml::Xml::Element type: "Text", node_type: :text>]
6539
+
6540
+ parsed.to_xml
6541
+ # => "<p>Text before<!-- a comment --> text after</p>"
6542
+ ----
6543
+ ====
6544
+
6545
+ NOTE: Comments are tracked in `element_order` but are **not** included in
6546
+ model attributes or the parsed hash. They exist solely for serialization fidelity.
6547
+
6501
6548
  ==== (DEPRECATED) Mixed content declaration (`root` method with `mixed:`)
6502
6549
 
6503
6550
  To map this to Lutaml::Model we can use the `mixed` option in either way:
@@ -6558,6 +6605,13 @@ preserves the order of **XML Elements and Content**.
6558
6605
 
6559
6606
  NOTE: When both options are used, `mixed: true` takes precedence.
6560
6607
 
6608
+ [IMPORTANT]
6609
+ ====
6610
+ `ordered` models element-only content -- `map_content` is not allowed.
6611
+ If you need to capture text content between elements, use `mixed_content`
6612
+ instead (see <<mixed-content>>).
6613
+ ====
6614
+
6561
6615
  To specify ordered content, the `ordered: true` option needs to be set at the
6562
6616
  `xml` block's `root` method.
6563
6617
 
@@ -16938,6 +16992,164 @@ end
16938
16992
  For a complete architecture overview and migration guide from Register to
16939
16993
  GlobalContext, see link:docs/architecture.md[Architecture Documentation].
16940
16994
 
16995
+ [[document-validation-framework]]
16996
+ == Document Validation Framework
16997
+
16998
+ Lutaml::Model provides a document-level validation framework (`Lutaml::Model::Validation`)
16999
+ for validating structural integrity, cross-references, and conformance against
17000
+ domain-specific rules. This is *orthogonal* to the existing attribute-level
17001
+ validation (`validate`/`validate!` on model instances) -- that validates type
17002
+ constraints, enumerations, and collections; this validates document-level concerns.
17003
+
17004
+ === Quick start
17005
+
17006
+ [source,ruby]
17007
+ ----
17008
+ require "lutaml/model/validation_framework"
17009
+
17010
+ # 1. Define rules by subclassing Lutaml::Model::Validation::Rule
17011
+ class RequiredFieldsRule < Lutaml::Model::Validation::Rule
17012
+ def code = "DOC-001"
17013
+ def severity = "error"
17014
+
17015
+ def check(context)
17016
+ missing = required_fields.select { |f| context[f].nil? || context[f].empty? }
17017
+ missing.map { |f| issue("Missing required field: #{f}") }
17018
+ end
17019
+
17020
+ private
17021
+
17022
+ def required_fields = %w[title author]
17023
+ end
17024
+
17025
+ # 2. Register rules
17026
+ registry = Lutaml::Model::Validation.new_registry
17027
+ registry.register(RequiredFieldsRule)
17028
+
17029
+ # 3. Run validation
17030
+ issues = Lutaml::Model::Validation.validate({ title: "Hello" }, registry)
17031
+ issues.each { |i| puts "[#{i.severity}] #{i.code}: #{i.message}" }
17032
+ # => [error] DOC-001: Missing required field: author
17033
+
17034
+ # 4. Or raise on errors
17035
+ begin
17036
+ Lutaml::Model::Validation.validate!({ title: "Hello" }, registry)
17037
+ rescue Lutaml::Model::Validation::ValidationError => e
17038
+ puts e.message # => "[DOC-001] Missing required field: author"
17039
+ e.issues # => [#<Issue code="DOC-001">]
17040
+ end
17041
+ ----
17042
+
17043
+ === Core components
17044
+
17045
+ [cols="1,4"]
17046
+ |===
17047
+ | Class | Purpose
17048
+
17049
+ | `Validation::Issue`
17050
+ | Serializable issue with severity (`error`/`warning`/`info`/`notice`), code, message, location, line, suggestion
17051
+
17052
+ | `Validation::Rule`
17053
+ | Abstract base class. Override `code`, `severity`, `category`, `applicable?`, and `check`
17054
+
17055
+ | `Validation::Registry`
17056
+ | Instance-based rule registration with cached instantiation via `register`, `all`, `for_category`, `find`
17057
+
17058
+ | `Validation::Profile`
17059
+ | YAML composable profiles with import resolution for rule subsets. `load` validates required `name` key
17060
+
17061
+ | `Validation::Context`
17062
+ | Mutable context with error accumulation and per-rule state (for streaming validation)
17063
+
17064
+ | `Validation::Report` / `LayerResult`
17065
+ | Serializable report models with aggregated severity filtering
17066
+
17067
+ | `Validation::Remediation`
17068
+ | Abstract base for auto-fix logic. Subclass must override `fix` (raises `NotImplementedError` on base class)
17069
+ |===
17070
+
17071
+ === Composable profiles
17072
+
17073
+ Profiles select *which* rules run, not *how* they run. They are loaded from YAML
17074
+ and support import-based composition.
17075
+
17076
+ [source,yaml]
17077
+ ----
17078
+ # profiles/basic.yml
17079
+ name: basic
17080
+ description: Basic DOCX validation
17081
+ rules:
17082
+ - RequiredFieldsRule
17083
+ - StyleReferencesRule
17084
+ ----
17085
+
17086
+ [source,yaml]
17087
+ ----
17088
+ # profiles/strict.yml
17089
+ name: strict
17090
+ import:
17091
+ - basic
17092
+ rules:
17093
+ - BookmarksRule
17094
+ - ImagesRule
17095
+ ----
17096
+
17097
+ [source,ruby]
17098
+ ----
17099
+ registry = Lutaml::Model::Validation.new_registry
17100
+ registry.register(RequiredFieldsRule)
17101
+ registry.register(StyleReferencesRule)
17102
+ registry.register(BookmarksRule)
17103
+ registry.register(ImagesRule)
17104
+
17105
+ basic = Lutaml::Model::Validation::Profile.load("profiles/basic.yml")
17106
+ strict = Lutaml::Model::Validation::Profile.load("profiles/strict.yml")
17107
+ profiles = { "basic" => basic, "strict" => strict }
17108
+
17109
+ rules = strict.resolve(registry, profiles) # returns all 4 rule instances
17110
+
17111
+ // Circular imports are detected and raise ArgumentError.
17112
+ // Profile YAML is validated: missing `name` key raises ArgumentError.
17113
+ ----
17114
+
17115
+ === Remediation (auto-fix)
17116
+
17117
+ Subclass `Validation::Remediation` to provide automatic fixes for specific issues:
17118
+
17119
+ [source,ruby]
17120
+ ----
17121
+ class FixBrokenReferences < Lutaml::Model::Validation::Remediation
17122
+ def id = "REM-001"
17123
+ def targets = ["DOC-020"]
17124
+
17125
+ def fix(context, report)
17126
+ # ... apply fixes ...
17127
+ Lutaml::Model::Validation::RemediationResult.new(
17128
+ success: true,
17129
+ message: "Fixed 3 broken references",
17130
+ fixed_codes: ["DOC-020"]
17131
+ )
17132
+ end
17133
+ end
17134
+ ----
17135
+
17136
+ === Relationship to attribute-level validation
17137
+
17138
+ |===
17139
+ | Attribute validation (existing) | Document validation framework (new)
17140
+
17141
+ | Validates model instance state (types, ranges, patterns)
17142
+ | Validates document-level concerns (structure, cross-references, conformance)
17143
+
17144
+ | Runs during deserialization via `model.validate`
17145
+ | Runs on parsed documents against domain rules via `Validation.validate`
17146
+
17147
+ | Returns `Array<Error>` error objects
17148
+ | Returns `Array<Issue>` serializable issue objects
17149
+ |===
17150
+
17151
+ See link:docs/_pages/validation.adoc[Validation] for attribute-level validation details.
17152
+
16941
17153
 
16942
17154
  == Comparison with Shale
16943
17155