class Lutaml::Model::Schema::RngCompiler::ElementVisitor
for individual children is delegated to ValueTypeResolver.
union) is delegated to DefineClassifier. Value-shape resolution
Shape classification (define-wraps-data, define-wraps-enum-choice,
interleave
optional, zeroOrMore, oneOrMore, group, choice, mixed, list,
element, attribute, ref, text, data, value, empty,
Supported constructs:
objects, registered in the shared ‘classes` hash.
Definitions::Model / Definitions::RestrictedType / Definitions::UnionType
Walks an RNG <element> / <define> subtree and produces
def attribute_type_symbol(child)
namespace, which lutaml-model expresses only through a Type::Value
namespace (a prefix other than the built-in "xml") must carry that
The attribute's value type, as a Symbol. An attribute in a foreign
def attribute_type_symbol(child) base = @value_type_resolver.resolve(child) || :string uri = foreign_namespace_uri(child) return base unless uri namespaced_attribute_type(child, base, @register_namespace.call(uri)) end
def build_attribute(child, type_ref, kind, ctx, doc, default: nil)
def build_attribute(child, type_ref, kind, ctx, doc, default: nil) xml_name = xml_name_for(child) Definitions::Attribute.new( name: Utils.snake_case(xml_name.tr(":", "_")), type: type_ref, xml_name: xml_name, kind: kind, collection: ctx[:collection] || false, initialize_empty: ctx[:initialize_empty] || false, documentation: doc, default: default, ) end
def build_complex_define(define, class_name)
def build_complex_define(define, class_name) wrapping = define.element.size == 1 ? define.element.first : nil xml_root = if wrapping Definitions::XmlRoot.new(kind: :element, name: wrapping.attr_name) else Definitions::XmlRoot.new(kind: :fragment) end model = Definitions::Model.new( class_name: class_name, xml_root: xml_root, documentation: documentation_text(define) || documentation_text(wrapping), namespace_class_name: namespace_class_for(wrapping || define), ) register_class!(model) visit_content(wrapping || define, model) model end
def build_ref_attribute(ref, type_ref, xml_name, ctx)
def build_ref_attribute(ref, type_ref, xml_name, ctx) Definitions::Attribute.new( name: Utils.snake_case(ref.name), type: type_ref, xml_name: xml_name, kind: :element, collection: ctx[:collection] || false, initialize_empty: ctx[:initialize_empty] || false, ) end
def compile_define(define)
- Wraps exactly one element -> rooted Definitions::Model.
- Define body matches a simple-type shape -> RestrictedType/UnionType.
Compile a
def compile_define(define) class_name = Utils.camel_case(define.name) return @classes[class_name] if @classes.key?(class_name) if (simple = DefineClassifier.build(define, class_name)) register_class!(simple) return simple end build_complex_define(define, class_name) end
def compile_element(element)
def compile_element(element) class_name = Utils.camel_case(element.attr_name) model = @classes[class_name] ||= Definitions::Model.new( class_name: class_name, xml_root: Definitions::XmlRoot.new(kind: :element, name: element.attr_name), documentation: documentation_text(element), namespace_class_name: namespace_class_for(element), ) visit_content(element, model) model end
def default_ctx
def default_ctx { collection: nil, initialize_empty: false } end
def dispatch(kind, child, parent, ctx)
def dispatch(kind, child, parent, ctx) handler = HANDLERS[kind] or return send(handler, kind, child, parent, ctx) end
def documentation_text(node)
def documentation_text(node) return nil unless node.respond_to?(:documentation) docs = Array(node.documentation).map(&:to_s).reject(&:empty?) docs.empty? ? nil : docs.join("\n") end
def element_order_entries(node)
def element_order_entries(node) return [] unless node.respond_to?(:element_order) Array(node.element_order).select do |e| e.respond_to?(:node_type) && e.node_type == :element end end
def fallback_each_child_kind(node)
def fallback_each_child_kind(node) DISPATCHABLE_KINDS.each_with_object([]) do |attr_name, pairs| next unless node.respond_to?(attr_name) Array(node.public_send(attr_name)).each do |child| pairs << [attr_name, child] unless child.nil? end end end
def fixed_value_default(container)
Match XSD compiler's behavior — emit `default: -> { "X" }` so
def fixed_value_default(container) return nil unless container.respond_to?(:value) values = Array(container.value) return nil if values.size != 1 return nil if RngHelpers.structural_content?(container) values.first.value.to_s end
def foreign_namespace_uri(node)
honored -- excluding "" here just avoids registering a bogus
represented distinctly from the default and is not separately
only at class level, so a per-node `ns=""` override cannot be
node the grammar default namespace. lutaml expresses namespaces
both treated as not foreign, so `namespace_class_for` hands the
`ns=""` no-namespace override) and the built-in "xml" prefix are
A node's foreign XML namespace URI, or nil. An empty `ns` (RNC's
def foreign_namespace_uri(node) uri = node.respond_to?(:ns) ? node.ns : nil uri if uri.is_a?(String) && !uri.empty? && uri != "xml" end
def generated_type_for(base)
even if a define shares its name (a custom `dateTime` must not shadow
built-in symbol. A built-in symbol always means the built-in type,
by symbol identity (so acronym class names round-trip) — or nil for a
The generated RestrictedType a resolved type symbol refers to, found
def generated_type_for(base) return nil if Lutaml::Model::Type::TYPE_CODES.key?(base) @classes.values.find do |klass| klass.is_a?(Definitions::RestrictedType) && RngHelpers.type_symbol(klass.class_name) == base end end
def handle_attribute(_kind, child, parent, ctx)
def handle_attribute(_kind, child, parent, ctx) doc = documentation_text(child) symbol = attribute_type_symbol(child) type_ref = Definitions::TypeRef.new(kind: :symbol, value: symbol.to_s) fixed = fixed_value_default(child) push_attribute(parent, build_attribute(child, type_ref, :attribute, ctx, doc, default: fixed)) end
def handle_choice(_kind, choice, parent, ctx)
def handle_choice(_kind, choice, parent, ctx) return if RngHelpers.pure_value_choice?(choice) collector = MemberCollector.new visit_content(choice, collector, ctx) return if collector.members.empty? parent.members << Definitions::Choice.new( alternatives: collector.members, header: "choice", ) collector.imports.each { |name| push_import(parent, name) } end
def handle_element(_kind, child, parent, ctx)
def handle_element(_kind, child, parent, ctx) doc = documentation_text(child) value_type = @value_type_resolver.resolve(child) type_ref = type_ref_for_element(child, value_type) push_attribute(parent, build_attribute(child, type_ref, :element, ctx, doc)) end
def handle_empty(_kind, _child, _parent, _ctx)
def handle_empty(_kind, _child, _parent, _ctx) # <empty/> contributes no content — intentionally no-op. end
def handle_group(_kind, group, parent, ctx)
def handle_group(_kind, group, parent, ctx) collector = MemberCollector.new visit_content(group, collector, ctx) return if collector.members.empty? parent.members << Definitions::Sequence.new(members: collector.members) collector.imports.each { |name| push_import(parent, name) } end
def handle_interleave(_kind, interleave, parent, ctx)
def handle_interleave(_kind, interleave, parent, ctx) visit_content(interleave, parent, ctx) end
def handle_optional(_kind, opt, parent, ctx)
def handle_optional(_kind, opt, parent, ctx) visit_content(opt, parent, ctx) end
def handle_ref(_kind, ref, parent, ctx)
def handle_ref(_kind, ref, parent, ctx) target_define = @defines[ref.name] raise Lutaml::Model::Error, "ref to unknown define: #{ref.name}" unless target_define target_class = compile_define(target_define) if RngHelpers.simple_type?(target_class) type_ref = Definitions::TypeRef.new(kind: :symbol, value: RngHelpers.type_symbol(target_class.class_name).to_s) push_attribute(parent, build_ref_attribute(ref, type_ref, ref.name, ctx)) elsif RngHelpers.fragment_model?(target_class) && ctx[:collection].nil? push_import(parent, target_class.class_name) else type_ref = Definitions::TypeRef.new(kind: :class_ref, value: target_class.class_name) xml_name = target_class.xml_root.name || ref.name push_attribute(parent, build_ref_attribute(ref, type_ref, xml_name, ctx)) end end
def handle_repeating(kind, node, parent, ctx)
def handle_repeating(kind, node, parent, ctx) visit_content(node, parent, ctx.merge(REPETITION_CTX.fetch(kind))) end
def initialize(defines, classes, default_namespace_class: nil,
def initialize(defines, classes, default_namespace_class: nil, register_namespace: nil) @defines = defines @classes = classes @default_namespace_class = default_namespace_class @register_namespace = register_namespace @value_type_resolver = ValueTypeResolver.new( defines, classes, compile_define: method(:compile_define), register_class: method(:register_class!) ) end
def mixed?(node)
def mixed?(node) node.respond_to?(:mixed) && Utils.present?(node.mixed) end
def namespace_class_for(node)
An element's own ns (from `element ex:name`) becomes its model's
def namespace_class_for(node) uri = foreign_namespace_uri(node) uri ? @register_namespace.call(uri) : @default_namespace_class end
def namespaced_attribute_type(child, base, ns_class)
resolved base is itself a generated RestrictedType, subclass it so
define or an inline type shared with unqualified members. When the
type — never by mutating the resolved type, which may be a named
Give the attribute's value a namespace via a dedicated generated
def namespaced_attribute_type(child, base, ns_class) resolved = generated_type_for(base) parent = resolved ? resolved.class_name : RngHelpers.parent_class_for(base) type = Definitions::RestrictedType.new( class_name: RngHelpers.unique_class_name( @classes, "#{Utils.camel_case(child.attr_name)}Type" ), parent_class: parent, facets: Definitions::Facet.new, namespace_class_name: ns_class, ) register_class!(type) RngHelpers.type_symbol(type.class_name) end
def ordered_children(node)
def ordered_children(node) element_entries = element_order_entries(node) return fallback_each_child_kind(node) if element_entries.empty? arrays = {} indices = ::Hash.new(0) element_entries.each_with_object([]) do |entry, pairs| kind = entry.name.to_sym next unless DISPATCHABLE_KINDS.include?(kind) && node.respond_to?(kind) children = arrays[kind] ||= Array(node.public_send(kind)) child = children[indices[kind]] indices[kind] += 1 pairs << [kind, child] if child end end
def push_attribute(parent, attr)
def push_attribute(parent, attr) parent.members.reject! do |m| m.is_a?(Definitions::Attribute) && m.name == attr.name end parent.members << attr end
def push_import(parent, name)
def push_import(parent, name) parent.imports << name unless parent.imports.include?(name) end
def register_class!(klass)
def register_class!(klass) @classes[klass.class_name] = klass end
def text?(node)
def text?(node) node.respond_to?(:text) && Utils.present?(node.text) end
def type_ref_for_element(child, value_type)
def type_ref_for_element(child, value_type) return Definitions::TypeRef.new(kind: :symbol, value: value_type.to_s) if value_type compiled = compile_element(child) Definitions::TypeRef.new(kind: :class_ref, value: compiled.class_name) end
def visit_content(node, model, ctx = default_ctx)
def visit_content(node, model, ctx = default_ctx) ordered_children(node).each do |kind, child| dispatch(kind, child, model, ctx) end model.mixed = true if mixed?(node) model.text_content = true if text?(node) end
def xml_name_for(child)
def xml_name_for(child) name = child.attr_name ns = child.respond_to?(:ns) ? child.ns : nil return name unless ns == "xml" "#{ns}:#{name}" end