lutaml-model 0.8.17 → 0.8.19
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/workflows/downstream-performance.yml +27 -1
- data/.github/workflows/js-pr-check.yml +102 -0
- data/.github/workflows/js-sync.yml +51 -0
- data/.github/workflows/opal.yml +26 -6
- data/.github/workflows/performance.yml +32 -7
- data/.github/workflows/rake.yml +78 -32
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +8 -0
- data/Gemfile +16 -1
- data/README.adoc +267 -0
- data/Rakefile +166 -4
- data/docs/_guides/builder-dsl.adoc +127 -0
- data/docs/_guides/schema-import.adoc +42 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_pages/validation.adoc +65 -0
- data/lib/compat/opal/generate_boot.rb +123 -0
- data/lib/compat/opal/js_bundle_entry.rb +42 -0
- data/lib/compat/opal/lutaml_model_boot.rb +497 -0
- data/lib/compat/opal/moxml_boot.rb +68 -0
- data/lib/compat/opal/yaml_compat.rb +32 -0
- data/lib/lutaml/json.rb +1 -1
- data/lib/lutaml/key_value/transform.rb +3 -8
- data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
- data/lib/lutaml/key_value/transformation.rb +17 -5
- data/lib/lutaml/model/adapter_resolver.rb +2 -2
- data/lib/lutaml/model/attribute.rb +88 -11
- data/lib/lutaml/model/cached_type_resolver.rb +1 -1
- data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
- data/lib/lutaml/model/global_context.rb +11 -8
- data/lib/lutaml/model/instrumentation.rb +2 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
- data/lib/lutaml/model/register.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +50 -0
- data/lib/lutaml/model/schema/class_loader.rb +58 -0
- data/lib/lutaml/model/schema/compiled_output.rb +83 -0
- data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
- data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
- data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
- data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
- data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
- data/lib/lutaml/model/schema/definitions/model.rb +40 -0
- data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
- data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
- data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
- data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
- data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
- data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
- data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
- data/lib/lutaml/model/schema/definitions.rb +24 -0
- data/lib/lutaml/model/schema/file_writer.rb +70 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
- data/lib/lutaml/model/schema/generator/property.rb +21 -4
- data/lib/lutaml/model/schema/module_nesting.rb +31 -0
- data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
- data/lib/lutaml/model/schema/registry_generator.rb +115 -0
- data/lib/lutaml/model/schema/renderers/base.rb +40 -0
- data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
- data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
- data/lib/lutaml/model/schema/renderers/model.rb +121 -0
- data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
- data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
- data/lib/lutaml/model/schema/renderers/union.rb +83 -0
- data/lib/lutaml/model/schema/renderers.rb +19 -0
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
- data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
- data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
- data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
- data/lib/lutaml/model/schema/templates.rb +162 -0
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
- data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
- data/lib/lutaml/model/schema.rb +19 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +23 -11
- data/lib/lutaml/model/serialize/builder.rb +70 -2
- data/lib/lutaml/model/serialize/initialization.rb +3 -2
- data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
- data/lib/lutaml/model/serialize.rb +6 -9
- data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
- data/lib/lutaml/model/services.rb +0 -2
- data/lib/lutaml/model/store.rb +2 -1
- data/lib/lutaml/model/toml.rb +1 -1
- data/lib/lutaml/model/transform.rb +17 -39
- data/lib/lutaml/model/transformation_registry.rb +1 -1
- data/lib/lutaml/model/type.rb +1 -0
- data/lib/lutaml/model/union.rb +320 -0
- data/lib/lutaml/model/utils.rb +6 -0
- data/lib/lutaml/model/validation.rb +1 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +18 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_loader.rb +1 -1
- data/lib/lutaml/xml/builder/base.rb +1 -1
- data/lib/lutaml/xml/builder.rb +1 -1
- data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
- data/lib/lutaml/xml/model_transform.rb +5 -1
- data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
- data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
- data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
- data/lib/lutaml/xml/transformation/ordered_applier.rb +104 -4
- data/lib/lutaml/xml/xsd_validator.rb +66 -0
- data/lib/lutaml/xml.rb +53 -12
- data/lutaml-model.gemspec +4 -2
- data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
- data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
- data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
- data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
- data/spec/fixtures/xml/schema/rng/book.rng +27 -0
- data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
- data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
- data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
- data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
- data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
- data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
- data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
- data/spec/fixtures/xml/schema/rng/person.rng +23 -0
- data/spec/fixtures/xml/schema/rng/union.rng +14 -0
- data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
- data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
- data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +169 -12
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/mixed_content_spec.rb +171 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +41 -0
- data/spec/lutaml/model/raw_element_spec.rb +125 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
- data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
- data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
- data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
- data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
- data/spec/lutaml/model/serialize/builder_spec.rb +358 -0
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
- data/spec/lutaml/model/union_attribute_spec.rb +658 -0
- data/spec/lutaml/model/union_spec.rb +257 -0
- data/spec/lutaml/model/utils_spec.rb +14 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
- data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
- data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +99 -31
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
- data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
- data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
- data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
- data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
- data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
- data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
- data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
- data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
- data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
- data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
- data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
- data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
- data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
- data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
- data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
- data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
- data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Schema
|
|
6
|
+
module RncCompiler
|
|
7
|
+
# Resolves RNC input text and the base directory used for relative
|
|
8
|
+
# include paths.
|
|
9
|
+
module SourceResolver
|
|
10
|
+
ResolvedSource =
|
|
11
|
+
Struct.new(:text, :base_dir, :path, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def resolve(input, options)
|
|
16
|
+
location = options[:location].to_s
|
|
17
|
+
return string_source(input) if location.empty?
|
|
18
|
+
|
|
19
|
+
expanded = File.expand_path(location)
|
|
20
|
+
if File.file?(expanded)
|
|
21
|
+
return file_source(expanded) if input.nil? || input.to_s.empty?
|
|
22
|
+
|
|
23
|
+
return string_source(input, base_dir: File.dirname(expanded), path: expanded)
|
|
24
|
+
end
|
|
25
|
+
return string_source(input, base_dir: expanded) if File.directory?(expanded)
|
|
26
|
+
|
|
27
|
+
string_source(input)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def string_source(input, base_dir: nil, path: nil)
|
|
31
|
+
ResolvedSource.new(text: input.to_s, base_dir: base_dir, path: path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def file_source(path)
|
|
35
|
+
ResolvedSource.new(
|
|
36
|
+
text: File.read(path),
|
|
37
|
+
base_dir: File.dirname(path),
|
|
38
|
+
path: path,
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_class_method :string_source, :file_source
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Schema
|
|
6
|
+
# Compiles RELAX NG Compact Syntax (RNC) into Lutaml::Model Ruby source.
|
|
7
|
+
#
|
|
8
|
+
# RNC support is a thin adapter: the rng gem parses the compact syntax
|
|
9
|
+
# into Rng::Grammar — resolving includes, bracket annotations, and
|
|
10
|
+
# non-leading `start` definitions natively — then RngCompiler performs all
|
|
11
|
+
# model generation and output handling.
|
|
12
|
+
module RncCompiler
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
autoload :SourceResolver, "#{__dir__}/rnc_compiler/source_resolver"
|
|
16
|
+
|
|
17
|
+
DEFAULT_OUTPUT_DIR_PREFIX = "rnc_models"
|
|
18
|
+
|
|
19
|
+
def to_models(rnc, options = {})
|
|
20
|
+
require_rnc_parser!
|
|
21
|
+
|
|
22
|
+
grammar = parse_grammar(rnc, options)
|
|
23
|
+
|
|
24
|
+
RngCompiler.to_models(
|
|
25
|
+
grammar,
|
|
26
|
+
options.merge(default_output_dir: default_output_dir),
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_grammar(rnc, options)
|
|
31
|
+
return rnc if rnc.is_a?(::Rng::Grammar)
|
|
32
|
+
|
|
33
|
+
source = SourceResolver.resolve(rnc, options)
|
|
34
|
+
::Rng.parse_rnc(source.text, location: source.path || source.base_dir)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def require_rnc_parser!
|
|
38
|
+
return if rnc_parser_available?
|
|
39
|
+
|
|
40
|
+
raise "RNC schema compilation requires an rng gem whose " \
|
|
41
|
+
"`Rng.parse_rnc` accepts a `location:` option (for native " \
|
|
42
|
+
"include resolution). Point your Gemfile at an rng version " \
|
|
43
|
+
"that provides it."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# rng gained the `location:` option (native include resolution) after
|
|
47
|
+
# the 0.3.7 release; the released parse_rnc takes only `(rnc)`.
|
|
48
|
+
def rnc_parser_available?
|
|
49
|
+
defined?(::Rng::Grammar) && ::Rng.respond_to?(:parse_rnc) &&
|
|
50
|
+
::Rng.method(:parse_rnc).parameters.any? { |_type, name| name == :location }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default_output_dir
|
|
54
|
+
"#{DEFAULT_OUTPUT_DIR_PREFIX}_#{Time.now.to_i}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private :parse_grammar, :require_rnc_parser!, :rnc_parser_available?,
|
|
58
|
+
:default_output_dir
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Schema
|
|
6
|
+
module RngCompiler
|
|
7
|
+
# Detects the shape of a <define> body and builds the corresponding
|
|
8
|
+
# Definitions::* spec for it.
|
|
9
|
+
#
|
|
10
|
+
# <data type="X"><param ...>...</param></data> -> RestrictedType
|
|
11
|
+
# <choice><value>a</value><value>b</value></choice> -> RestrictedType (enum)
|
|
12
|
+
# <choice><data type="A"/><data type="B"/></choice> -> UnionType
|
|
13
|
+
#
|
|
14
|
+
# Returns nil when the define is structural (contains elements or
|
|
15
|
+
# attributes) — caller falls back to building a Definitions::Model.
|
|
16
|
+
class DefineClassifier
|
|
17
|
+
def self.build(define, class_name)
|
|
18
|
+
new(define, class_name).build
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(define, class_name)
|
|
22
|
+
@define = define
|
|
23
|
+
@class_name = class_name
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build
|
|
27
|
+
return nil if structural_define?
|
|
28
|
+
|
|
29
|
+
union_type || data_restricted_type || enum_restricted_type
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def structural_define?
|
|
35
|
+
@define.element.any? || @define.attribute.any?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def union_type
|
|
39
|
+
choice = value_choice
|
|
40
|
+
return nil unless RngHelpers.pure_union_choice?(choice)
|
|
41
|
+
|
|
42
|
+
type_refs = Array(choice.data).map do |d|
|
|
43
|
+
symbol = RngCompiler::DATA_TYPE_MAP.fetch(
|
|
44
|
+
d.type, RngCompiler::DEFAULT_DATA_TYPE
|
|
45
|
+
)
|
|
46
|
+
Definitions::TypeRef.new(
|
|
47
|
+
kind: :class_ref,
|
|
48
|
+
value: RngHelpers.parent_class_for(symbol),
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Definitions::UnionType.new(
|
|
53
|
+
class_name: @class_name,
|
|
54
|
+
members: type_refs,
|
|
55
|
+
cast_strategy: :class_refs,
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def data_restricted_type
|
|
60
|
+
data = RngHelpers.single(@define.data)
|
|
61
|
+
return nil unless data
|
|
62
|
+
|
|
63
|
+
base = RngCompiler::DATA_TYPE_MAP.fetch(
|
|
64
|
+
data.type, RngCompiler::DEFAULT_DATA_TYPE
|
|
65
|
+
)
|
|
66
|
+
Definitions::RestrictedType.new(
|
|
67
|
+
class_name: @class_name,
|
|
68
|
+
parent_class: RngHelpers.parent_class_for(base),
|
|
69
|
+
facets: RngHelpers.facet_from_data(data),
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def enum_restricted_type
|
|
74
|
+
choice = value_choice
|
|
75
|
+
return nil unless RngHelpers.pure_value_choice?(choice)
|
|
76
|
+
|
|
77
|
+
Definitions::RestrictedType.new(
|
|
78
|
+
class_name: @class_name,
|
|
79
|
+
parent_class: RngHelpers.parent_class_for(:string),
|
|
80
|
+
facets: RngHelpers.facet_from_values(choice.value),
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def value_choice
|
|
85
|
+
RngHelpers.single(@define.choice)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Schema
|
|
6
|
+
module RngCompiler
|
|
7
|
+
# Walks an RNG <element> / <define> subtree and produces
|
|
8
|
+
# Definitions::Model / Definitions::RestrictedType / Definitions::UnionType
|
|
9
|
+
# objects, registered in the shared `classes` hash.
|
|
10
|
+
#
|
|
11
|
+
# Supported constructs:
|
|
12
|
+
# element, attribute, ref, text, data, value, empty,
|
|
13
|
+
# optional, zeroOrMore, oneOrMore, group, choice, mixed, list,
|
|
14
|
+
# interleave
|
|
15
|
+
#
|
|
16
|
+
# Shape classification (define-wraps-data, define-wraps-enum-choice,
|
|
17
|
+
# union) is delegated to DefineClassifier. Value-shape resolution
|
|
18
|
+
# for individual children is delegated to ValueTypeResolver.
|
|
19
|
+
class ElementVisitor
|
|
20
|
+
DISPATCHABLE_KINDS = %i[
|
|
21
|
+
element attribute ref group optional oneOrMore zeroOrMore
|
|
22
|
+
choice list interleave empty
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
REPETITION_CTX = {
|
|
26
|
+
zeroOrMore: { collection: (0..Float::INFINITY), initialize_empty: true },
|
|
27
|
+
oneOrMore: { collection: (1..Float::INFINITY), initialize_empty: true },
|
|
28
|
+
list: { collection: (0..Float::INFINITY), initialize_empty: true },
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
HANDLERS = {
|
|
32
|
+
element: :handle_element,
|
|
33
|
+
attribute: :handle_attribute,
|
|
34
|
+
ref: :handle_ref,
|
|
35
|
+
group: :handle_group,
|
|
36
|
+
optional: :handle_optional,
|
|
37
|
+
oneOrMore: :handle_repeating,
|
|
38
|
+
zeroOrMore: :handle_repeating,
|
|
39
|
+
list: :handle_repeating,
|
|
40
|
+
choice: :handle_choice,
|
|
41
|
+
interleave: :handle_interleave,
|
|
42
|
+
empty: :handle_empty,
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def initialize(defines, classes, default_namespace_class: nil,
|
|
46
|
+
register_namespace: nil)
|
|
47
|
+
@defines = defines
|
|
48
|
+
@classes = classes
|
|
49
|
+
@default_namespace_class = default_namespace_class
|
|
50
|
+
@register_namespace = register_namespace
|
|
51
|
+
@value_type_resolver = ValueTypeResolver.new(
|
|
52
|
+
defines, classes,
|
|
53
|
+
compile_define: method(:compile_define),
|
|
54
|
+
register_class: method(:register_class!)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Compile a top-level <element>. Always becomes a rooted model.
|
|
59
|
+
def compile_element(element)
|
|
60
|
+
class_name = Utils.camel_case(element.attr_name)
|
|
61
|
+
model = @classes[class_name] ||= Definitions::Model.new(
|
|
62
|
+
class_name: class_name,
|
|
63
|
+
xml_root: Definitions::XmlRoot.new(kind: :element, name: element.attr_name),
|
|
64
|
+
documentation: documentation_text(element),
|
|
65
|
+
namespace_class_name: namespace_class_for(element),
|
|
66
|
+
)
|
|
67
|
+
visit_content(element, model)
|
|
68
|
+
model
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Compile a <define> into a class.
|
|
72
|
+
# - Define body matches a simple-type shape -> RestrictedType/UnionType.
|
|
73
|
+
# - Wraps exactly one element -> rooted Definitions::Model.
|
|
74
|
+
# - Multiple or zero wrapped elements -> fragment Definitions::Model.
|
|
75
|
+
def compile_define(define)
|
|
76
|
+
class_name = Utils.camel_case(define.name)
|
|
77
|
+
return @classes[class_name] if @classes.key?(class_name)
|
|
78
|
+
|
|
79
|
+
if (simple = DefineClassifier.build(define, class_name))
|
|
80
|
+
register_class!(simple)
|
|
81
|
+
return simple
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
build_complex_define(define, class_name)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def build_complex_define(define, class_name)
|
|
90
|
+
wrapping = define.element.size == 1 ? define.element.first : nil
|
|
91
|
+
|
|
92
|
+
xml_root = if wrapping
|
|
93
|
+
Definitions::XmlRoot.new(kind: :element, name: wrapping.attr_name)
|
|
94
|
+
else
|
|
95
|
+
Definitions::XmlRoot.new(kind: :fragment)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
model = Definitions::Model.new(
|
|
99
|
+
class_name: class_name,
|
|
100
|
+
xml_root: xml_root,
|
|
101
|
+
documentation: documentation_text(define) || documentation_text(wrapping),
|
|
102
|
+
namespace_class_name: namespace_class_for(wrapping || define),
|
|
103
|
+
)
|
|
104
|
+
register_class!(model)
|
|
105
|
+
visit_content(wrapping || define, model)
|
|
106
|
+
model
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def register_class!(klass)
|
|
110
|
+
@classes[klass.class_name] = klass
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A node's foreign XML namespace URI, or nil. An empty `ns` (RNC's
|
|
114
|
+
# `ns=""` no-namespace override) and the built-in "xml" prefix are
|
|
115
|
+
# both treated as not foreign, so `namespace_class_for` hands the
|
|
116
|
+
# node the grammar default namespace. lutaml expresses namespaces
|
|
117
|
+
# only at class level, so a per-node `ns=""` override cannot be
|
|
118
|
+
# represented distinctly from the default and is not separately
|
|
119
|
+
# honored -- excluding "" here just avoids registering a bogus
|
|
120
|
+
# empty-URI namespace that would clobber the grammar default.
|
|
121
|
+
def foreign_namespace_uri(node)
|
|
122
|
+
uri = node.respond_to?(:ns) ? node.ns : nil
|
|
123
|
+
uri if uri.is_a?(String) && !uri.empty? && uri != "xml"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# An element's own ns (from `element ex:name`) becomes its model's
|
|
127
|
+
# class-level namespace; otherwise it inherits the grammar default.
|
|
128
|
+
def namespace_class_for(node)
|
|
129
|
+
uri = foreign_namespace_uri(node)
|
|
130
|
+
uri ? @register_namespace.call(uri) : @default_namespace_class
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def documentation_text(node)
|
|
134
|
+
return nil unless node.respond_to?(:documentation)
|
|
135
|
+
|
|
136
|
+
docs = Array(node.documentation).map(&:to_s).reject(&:empty?)
|
|
137
|
+
docs.empty? ? nil : docs.join("\n")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Generic content walker.
|
|
141
|
+
def visit_content(node, model, ctx = default_ctx)
|
|
142
|
+
ordered_children(node).each do |kind, child|
|
|
143
|
+
dispatch(kind, child, model, ctx)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
model.mixed = true if mixed?(node)
|
|
147
|
+
model.text_content = true if text?(node)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def default_ctx
|
|
151
|
+
{ collection: nil, initialize_empty: false }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def ordered_children(node)
|
|
155
|
+
element_entries = element_order_entries(node)
|
|
156
|
+
return fallback_each_child_kind(node) if element_entries.empty?
|
|
157
|
+
|
|
158
|
+
arrays = {}
|
|
159
|
+
indices = ::Hash.new(0)
|
|
160
|
+
element_entries.each_with_object([]) do |entry, pairs|
|
|
161
|
+
kind = entry.name.to_sym
|
|
162
|
+
next unless DISPATCHABLE_KINDS.include?(kind) && node.respond_to?(kind)
|
|
163
|
+
|
|
164
|
+
children = arrays[kind] ||= Array(node.public_send(kind))
|
|
165
|
+
child = children[indices[kind]]
|
|
166
|
+
indices[kind] += 1
|
|
167
|
+
pairs << [kind, child] if child
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def element_order_entries(node)
|
|
172
|
+
return [] unless node.respond_to?(:element_order)
|
|
173
|
+
|
|
174
|
+
Array(node.element_order).select do |e|
|
|
175
|
+
e.respond_to?(:node_type) && e.node_type == :element
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def fallback_each_child_kind(node)
|
|
180
|
+
DISPATCHABLE_KINDS.each_with_object([]) do |attr_name, pairs|
|
|
181
|
+
next unless node.respond_to?(attr_name)
|
|
182
|
+
|
|
183
|
+
Array(node.public_send(attr_name)).each do |child|
|
|
184
|
+
pairs << [attr_name, child] unless child.nil?
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def dispatch(kind, child, parent, ctx)
|
|
190
|
+
handler = HANDLERS[kind] or return
|
|
191
|
+
|
|
192
|
+
send(handler, kind, child, parent, ctx)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# --- handlers ---------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def handle_element(_kind, child, parent, ctx)
|
|
198
|
+
doc = documentation_text(child)
|
|
199
|
+
value_type = @value_type_resolver.resolve(child)
|
|
200
|
+
type_ref = type_ref_for_element(child, value_type)
|
|
201
|
+
push_attribute(parent, build_attribute(child, type_ref, :element, ctx, doc))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def type_ref_for_element(child, value_type)
|
|
205
|
+
return Definitions::TypeRef.new(kind: :symbol, value: value_type.to_s) if value_type
|
|
206
|
+
|
|
207
|
+
compiled = compile_element(child)
|
|
208
|
+
Definitions::TypeRef.new(kind: :class_ref, value: compiled.class_name)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def handle_attribute(_kind, child, parent, ctx)
|
|
212
|
+
doc = documentation_text(child)
|
|
213
|
+
symbol = attribute_type_symbol(child)
|
|
214
|
+
type_ref = Definitions::TypeRef.new(kind: :symbol, value: symbol.to_s)
|
|
215
|
+
fixed = fixed_value_default(child)
|
|
216
|
+
push_attribute(parent, build_attribute(child, type_ref, :attribute, ctx, doc, default: fixed))
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The attribute's value type, as a Symbol. An attribute in a foreign
|
|
220
|
+
# namespace (a prefix other than the built-in "xml") must carry that
|
|
221
|
+
# namespace, which lutaml-model expresses only through a Type::Value
|
|
222
|
+
# subclass — so wrap the value in a generated namespaced type.
|
|
223
|
+
def attribute_type_symbol(child)
|
|
224
|
+
base = @value_type_resolver.resolve(child) || :string
|
|
225
|
+
uri = foreign_namespace_uri(child)
|
|
226
|
+
return base unless uri
|
|
227
|
+
|
|
228
|
+
namespaced_attribute_type(child, base, @register_namespace.call(uri))
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Give the attribute's value a namespace via a dedicated generated
|
|
232
|
+
# type — never by mutating the resolved type, which may be a named
|
|
233
|
+
# define or an inline type shared with unqualified members. When the
|
|
234
|
+
# resolved base is itself a generated RestrictedType, subclass it so
|
|
235
|
+
# its constraints are inherited; otherwise subclass the built-in base.
|
|
236
|
+
def namespaced_attribute_type(child, base, ns_class)
|
|
237
|
+
resolved = generated_type_for(base)
|
|
238
|
+
parent = resolved ? resolved.class_name : RngHelpers.parent_class_for(base)
|
|
239
|
+
|
|
240
|
+
type = Definitions::RestrictedType.new(
|
|
241
|
+
class_name: RngHelpers.unique_class_name(
|
|
242
|
+
@classes, "#{Utils.camel_case(child.attr_name)}Type"
|
|
243
|
+
),
|
|
244
|
+
parent_class: parent,
|
|
245
|
+
facets: Definitions::Facet.new,
|
|
246
|
+
namespace_class_name: ns_class,
|
|
247
|
+
)
|
|
248
|
+
register_class!(type)
|
|
249
|
+
RngHelpers.type_symbol(type.class_name)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# The generated RestrictedType a resolved type symbol refers to, found
|
|
253
|
+
# by symbol identity (so acronym class names round-trip) — or nil for a
|
|
254
|
+
# built-in symbol. A built-in symbol always means the built-in type,
|
|
255
|
+
# even if a define shares its name (a custom `dateTime` must not shadow
|
|
256
|
+
# the primitive `:date_time`).
|
|
257
|
+
def generated_type_for(base)
|
|
258
|
+
return nil if Lutaml::Model::Type::TYPE_CODES.key?(base)
|
|
259
|
+
|
|
260
|
+
@classes.values.find do |klass|
|
|
261
|
+
klass.is_a?(Definitions::RestrictedType) &&
|
|
262
|
+
RngHelpers.type_symbol(klass.class_name) == base
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def build_attribute(child, type_ref, kind, ctx, doc, default: nil)
|
|
267
|
+
xml_name = xml_name_for(child)
|
|
268
|
+
Definitions::Attribute.new(
|
|
269
|
+
name: Utils.snake_case(xml_name.tr(":", "_")),
|
|
270
|
+
type: type_ref,
|
|
271
|
+
xml_name: xml_name,
|
|
272
|
+
kind: kind,
|
|
273
|
+
collection: ctx[:collection] || false,
|
|
274
|
+
initialize_empty: ctx[:initialize_empty] || false,
|
|
275
|
+
documentation: doc,
|
|
276
|
+
default: default,
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def xml_name_for(child)
|
|
281
|
+
name = child.attr_name
|
|
282
|
+
ns = child.respond_to?(:ns) ? child.ns : nil
|
|
283
|
+
return name unless ns == "xml"
|
|
284
|
+
|
|
285
|
+
"#{ns}:#{name}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# <attribute name="x"><value>X</value></attribute> = fixed value.
|
|
289
|
+
# Match XSD compiler's behavior — emit `default: -> { "X" }` so
|
|
290
|
+
# the value round-trips even when omitted.
|
|
291
|
+
def fixed_value_default(container)
|
|
292
|
+
return nil unless container.respond_to?(:value)
|
|
293
|
+
|
|
294
|
+
values = Array(container.value)
|
|
295
|
+
return nil if values.size != 1
|
|
296
|
+
return nil if RngHelpers.structural_content?(container)
|
|
297
|
+
|
|
298
|
+
values.first.value.to_s
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def handle_ref(_kind, ref, parent, ctx)
|
|
302
|
+
target_define = @defines[ref.name]
|
|
303
|
+
raise Lutaml::Model::Error, "ref to unknown define: #{ref.name}" unless target_define
|
|
304
|
+
|
|
305
|
+
target_class = compile_define(target_define)
|
|
306
|
+
|
|
307
|
+
if RngHelpers.simple_type?(target_class)
|
|
308
|
+
type_ref = Definitions::TypeRef.new(kind: :symbol, value: RngHelpers.type_symbol(target_class.class_name).to_s)
|
|
309
|
+
push_attribute(parent, build_ref_attribute(ref, type_ref, ref.name, ctx))
|
|
310
|
+
elsif RngHelpers.fragment_model?(target_class) && ctx[:collection].nil?
|
|
311
|
+
push_import(parent, target_class.class_name)
|
|
312
|
+
else
|
|
313
|
+
type_ref = Definitions::TypeRef.new(kind: :class_ref, value: target_class.class_name)
|
|
314
|
+
xml_name = target_class.xml_root.name || ref.name
|
|
315
|
+
push_attribute(parent, build_ref_attribute(ref, type_ref, xml_name, ctx))
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def build_ref_attribute(ref, type_ref, xml_name, ctx)
|
|
320
|
+
Definitions::Attribute.new(
|
|
321
|
+
name: Utils.snake_case(ref.name),
|
|
322
|
+
type: type_ref,
|
|
323
|
+
xml_name: xml_name,
|
|
324
|
+
kind: :element,
|
|
325
|
+
collection: ctx[:collection] || false,
|
|
326
|
+
initialize_empty: ctx[:initialize_empty] || false,
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# <group> = ordered sequence of items.
|
|
331
|
+
def handle_group(_kind, group, parent, ctx)
|
|
332
|
+
collector = MemberCollector.new
|
|
333
|
+
visit_content(group, collector, ctx)
|
|
334
|
+
return if collector.members.empty?
|
|
335
|
+
|
|
336
|
+
parent.members << Definitions::Sequence.new(members: collector.members)
|
|
337
|
+
collector.imports.each { |name| push_import(parent, name) }
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def handle_optional(_kind, opt, parent, ctx)
|
|
341
|
+
visit_content(opt, parent, ctx)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def handle_interleave(_kind, interleave, parent, ctx)
|
|
345
|
+
visit_content(interleave, parent, ctx)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def handle_repeating(kind, node, parent, ctx)
|
|
349
|
+
visit_content(node, parent, ctx.merge(REPETITION_CTX.fetch(kind)))
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def handle_choice(_kind, choice, parent, ctx)
|
|
353
|
+
return if RngHelpers.pure_value_choice?(choice)
|
|
354
|
+
|
|
355
|
+
collector = MemberCollector.new
|
|
356
|
+
visit_content(choice, collector, ctx)
|
|
357
|
+
return if collector.members.empty?
|
|
358
|
+
|
|
359
|
+
parent.members << Definitions::Choice.new(
|
|
360
|
+
alternatives: collector.members,
|
|
361
|
+
header: "choice",
|
|
362
|
+
)
|
|
363
|
+
collector.imports.each { |name| push_import(parent, name) }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def handle_empty(_kind, _child, _parent, _ctx)
|
|
367
|
+
# <empty/> contributes no content — intentionally no-op.
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def push_attribute(parent, attr)
|
|
371
|
+
parent.members.reject! do |m|
|
|
372
|
+
m.is_a?(Definitions::Attribute) && m.name == attr.name
|
|
373
|
+
end
|
|
374
|
+
parent.members << attr
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def push_import(parent, name)
|
|
378
|
+
parent.imports << name unless parent.imports.include?(name)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def mixed?(node)
|
|
382
|
+
node.respond_to?(:mixed) && Utils.present?(node.mixed)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def text?(node)
|
|
386
|
+
node.respond_to?(:text) && Utils.present?(node.text)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Schema
|
|
6
|
+
module RngCompiler
|
|
7
|
+
# Lightweight accumulator used by ElementVisitor when collecting the
|
|
8
|
+
# children of a transient construct (`<group>`, `<choice>`) before
|
|
9
|
+
# promoting them into a Definitions::Sequence or Definitions::Choice on
|
|
10
|
+
# the real Definitions::Model.
|
|
11
|
+
#
|
|
12
|
+
# Mirrors the surface ElementVisitor expects on a model
|
|
13
|
+
# (members, imports, mixed, text_content) but with no XML root,
|
|
14
|
+
# documentation, or namespace — none of which apply to a scratch
|
|
15
|
+
# container.
|
|
16
|
+
class MemberCollector
|
|
17
|
+
attr_reader :members, :imports
|
|
18
|
+
attr_accessor :mixed, :text_content
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@members = []
|
|
22
|
+
@imports = []
|
|
23
|
+
@mixed = false
|
|
24
|
+
@text_content = false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|