Class: Lutaml::Xml::Schema::XsdSchema
- Inherits:
-
Object
- Object
- Lutaml::Xml::Schema::XsdSchema
- Extended by:
- Model::Schema::SharedMethods
- Includes:
- Model::Schema::SharedMethods
- Defined in:
- lib/lutaml/xml/schema/xsd_schema.rb
Overview
XSD Schema generation for XML models
Generates W3C XML Schema (XSD) from LutaML model classes. Supports namespace declarations, type definitions, and nested models.
Class Method Summary collapse
- .attr_is_xml_attribute?(xml_mapping, attr_name) ⇒ Boolean
- .build_element_attributes(name, xsd_type, attr, xml_mapping, attr_name) ⇒ Object
-
.classify_xsd_type(type_name, klass, register) ⇒ Symbol
Classify an XSD type name into one of three categories.
- .collect_type_namespaces(klass, register) ⇒ Object
- .generate(klass, options = {}) ⇒ Object
- .generate_annotation(xml, xml_mapping) ⇒ Object
- .generate_attributes(xml, klass, register, xml_mapping) ⇒ Object
- .generate_complex_type(xml, klass, type_name, register, xml_mapping = nil) ⇒ Object
- .generate_complex_type_content(xml, klass, register, xml_mapping) ⇒ Object
- .generate_elements(xml, klass, register, xml_mapping) ⇒ Object
- .generate_imports(xml, namespace_class) ⇒ Object
- .generate_includes(xml, namespace_class) ⇒ Object
- .generate_nested_type_definitions(xml, klass, register) ⇒ Object
- .generate_schema(xml, klass, xml_mapping, register, _options) ⇒ Object
- .get_attribute_xsd_type(attr, attr_type, register, _mapping_rule = nil) ⇒ Object
-
.get_namespace_info(klass) ⇒ Object
Get unified namespace information from Model or Type class.
- .get_target_xsd_type(attr, register) ⇒ Object
- .get_xsd_type(type) ⇒ Object
- .has_explicit_xml_mapping?(klass, xml_mapping) ⇒ Boolean
-
.type_resolvable?(type_name, klass, register) ⇒ Boolean
Check if a custom XSD type can be resolved in the model hierarchy.
-
.validate_xsd_types!(klass, register) ⇒ Object
Validate all XSD types referenced by the model.
Methods included from Model::Schema::SharedMethods
Class Method Details
.attr_is_xml_attribute?(xml_mapping, attr_name) ⇒ Boolean
372 373 374 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 372 def self.attr_is_xml_attribute?(xml_mapping, attr_name) xml_mapping.attributes.any? { |rule| rule.to == attr_name } end |
.build_element_attributes(name, xsd_type, attr, xml_mapping, attr_name) ⇒ Object
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 376 def self.build_element_attributes(name, xsd_type, attr, xml_mapping, attr_name) attrs = { name: name.to_s, type: xsd_type } # Handle collection cardinality if attr.collection? range = attr.resolved_collection if range attrs[:minOccurs] = range.min.to_s attrs[:maxOccurs] = range.end.infinite? ? "unbounded" : range.max.to_s else attrs[:minOccurs] = "0" attrs[:maxOccurs] = "unbounded" end end # Add form attribute from mapping rule if present if xml_mapping rule = xml_mapping.find_element(attr_name) attrs[:form] = rule.form.to_s if rule&.form attrs[:annotation] = rule.documentation if rule&.documentation end attrs end |
.classify_xsd_type(type_name, klass, register) ⇒ Symbol
Classify an XSD type name into one of three categories
41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 41 def self.classify_xsd_type(type_name, klass, register) return :builtin if BuiltinTypes.builtin?(type_name) # Custom type - check if resolvable if type_name && !type_name.start_with?("xs:") return :custom if type_resolvable?(type_name, klass, register) return :unresolvable end :unknown end |
.collect_type_namespaces(klass, register) ⇒ Object
430 431 432 433 434 435 436 437 438 439 440 441 442 443 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 430 def self.collect_type_namespaces(klass, register) namespaces = Set.new klass.attributes.each_value do |attr| type_class = attr.type(register) next unless type_class # Use unified get_namespace_info method ns_info = get_namespace_info(type_class) namespaces << ns_info[:class] if ns_info[:class] end namespaces.to_a end |
.generate(klass, options = {}) ⇒ Object
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 15 def self.generate(klass, = {}) register = extract_register_from(klass) xml_mapping = klass.mappings_for(:xml) # Validate XSD types unless explicitly skipped validate_xsd_types!(klass, register) unless [:skip_validation] # Use Builder with adapter from options or config adapter_type = [:adapter] || Lutaml::Model::Config.xml_adapter_type || :nokogiri schema_builder = Builder.new( adapter_type: adapter_type, options: { encoding: "UTF-8" }, ) do |xml| generate_schema(xml, klass, xml_mapping, register, ) end schema_builder.to_xml() end |
.generate_annotation(xml, xml_mapping) ⇒ Object
221 222 223 224 225 226 227 228 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 221 def self.generate_annotation(xml, xml_mapping) xml.annotation do doc_text = xml_mapping.documentation_text doc_text ||= xml_mapping.namespace_class&.documentation if xml_mapping.namespace_class xml.documentation(doc_text) if doc_text end end |
.generate_attributes(xml, klass, register, xml_mapping) ⇒ Object
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 346 def self.generate_attributes(xml, klass, register, xml_mapping) return unless xml_mapping xml_mapping.attributes.each do |rule| attr = klass.attributes[rule.to] next unless attr attr_type = attr.type(register) xsd_type = get_attribute_xsd_type(attr, attr_type, register, rule) attr_attrs = { name: rule.name, type: xsd_type } attr_attrs[:use] = "required" if attr.[:required] attr_attrs[:form] = rule.form.to_s if rule.form if rule.documentation xml.attribute(attr_attrs) do xml.annotation do xml.documentation(rule.documentation) end end else xml.attribute(attr_attrs) end end end |
.generate_complex_type(xml, klass, type_name, register, xml_mapping = nil) ⇒ Object
263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 263 def self.generate_complex_type(xml, klass, type_name, register, xml_mapping = nil) xml.complexType(name: type_name) do if klass.attributes.any? xml.sequence do generate_elements(xml, klass, register, xml_mapping) end end generate_attributes(xml, klass, register, xml_mapping) end end |
.generate_complex_type_content(xml, klass, register, xml_mapping) ⇒ Object
248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 248 def self.generate_complex_type_content(xml, klass, register, xml_mapping) xml.complexType do if klass.attributes.any? xml.sequence do generate_elements(xml, klass, register, xml_mapping) end end if xml_mapping generate_attributes(xml, klass, register, xml_mapping) end end end |
.generate_elements(xml, klass, register, xml_mapping) ⇒ Object
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 275 def self.generate_elements(xml, klass, register, xml_mapping) klass.attributes.each do |name, attr| next if xml_mapping && attr_is_xml_attribute?(xml_mapping, name) # Find the mapping rule for this attribute mapping_rule = xml_mapping&.find_element(name) attr_type = attr.type(register) if attr_type <= Lutaml::Model::Serialize # Nested model - check if it has a type_name for reference nested_mapping = attr_type.mappings_for(:xml) nested_type_name = nested_mapping&.type_name_value if attr.collection? # Collection of models element_attrs = { name: name.to_s } element_attrs[:minOccurs] = "0" element_attrs[:maxOccurs] = "unbounded" if nested_type_name # Reference named type element_attrs[:type] = nested_type_name xml.element(element_attrs) else # Inline anonymous complexType xml.element(element_attrs) do xml.complexType do xml.sequence do xml.element(name: "item", type: get_xsd_type(attr_type)) end end end end elsif nested_type_name # Single nested model - Reference named type xml.element(name: name.to_s, type: nested_type_name) else # Inline anonymous complexType xml.element(name: name.to_s) do generate_complex_type_content(xml, attr_type, register, nil) end end else # Value type xsd_type = get_attribute_xsd_type(attr, attr_type, register, mapping_rule) if attr.collection? # Collection of simple types element_attrs = { name: name.to_s } element_attrs[:minOccurs] = "0" element_attrs[:maxOccurs] = "unbounded" xml.element(element_attrs) do xml.complexType do xml.sequence do xml.element(name: "item", type: xsd_type) end end end else # Simple element element_attrs = build_element_attributes(name, xsd_type, attr, xml_mapping, name) xml.element(element_attrs) end end end end |
.generate_imports(xml, namespace_class) ⇒ Object
200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 200 def self.generate_imports(xml, namespace_class) return unless namespace_class.imports&.any? namespace_class.imports.each do |imported_ns| import_attrs = { namespace: imported_ns.uri } if imported_ns.schema_location import_attrs[:schemaLocation] = imported_ns.schema_location end xml.import(import_attrs) end end |
.generate_includes(xml, namespace_class) ⇒ Object
213 214 215 216 217 218 219 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 213 def self.generate_includes(xml, namespace_class) return unless namespace_class.includes&.any? namespace_class.includes.each do |schema_location| xml.include(schemaLocation: schema_location) end end |
.generate_nested_type_definitions(xml, klass, register) ⇒ Object
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 230 def self.generate_nested_type_definitions(xml, klass, register) klass.attributes.each_value do |attr| attr_type = attr.type(register) next unless attr_type <= Lutaml::Model::Serialize nested_mapping = attr_type.mappings_for(:xml) nested_type_name = nested_mapping&.type_name_value # Generate type definition if nested model has type_name if nested_type_name generate_complex_type(xml, attr_type, nested_type_name, register, nested_mapping) # Recursively generate nested types generate_nested_type_definitions(xml, attr_type, register) end end end |
.generate_schema(xml, klass, xml_mapping, register, _options) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 111 def self.generate_schema(xml, klass, xml_mapping, register, ) schema_attrs = { xmlns: "http://www.w3.org/2001/XMLSchema" } # Add namespace metadata from XmlNamespace class if present if xml_mapping.namespace_class ns = xml_mapping.namespace_class schema_attrs[:targetNamespace] = ns.uri schema_attrs[:elementFormDefault] = ns.element_form_default.to_s schema_attrs[:attributeFormDefault] = ns.attribute_form_default.to_s schema_attrs[:version] = ns.version if ns.version # Add xmlns declarations for the target namespace prefix = xml_mapping.namespace_prefix || ns.prefix_default if prefix && !prefix.empty? schema_attrs[:"xmlns:#{prefix}"] = ns.uri end elsif xml_mapping.namespace_uri # Legacy: namespace URI without XmlNamespace class schema_attrs[:targetNamespace] = xml_mapping.namespace_uri schema_attrs[:elementFormDefault] = "unqualified" schema_attrs[:attributeFormDefault] = "unqualified" if xml_mapping.namespace_prefix schema_attrs[:"xmlns:#{xml_mapping.namespace_prefix}"] = xml_mapping.namespace_uri end end xml.schema(schema_attrs) do # Generate imports from XmlNamespace if xml_mapping.namespace_class generate_imports(xml, xml_mapping.namespace_class) generate_includes(xml, xml_mapping.namespace_class) end # Generate imports for Type namespaces type_namespaces = collect_type_namespaces(klass, register) type_namespaces.each do |ns_class| # Only import if different from target namespace next if ns_class.uri == schema_attrs[:targetNamespace] import_attrs = { namespace: ns_class.uri } if ns_class.schema_location import_attrs[:schemaLocation] = ns_class.schema_location end xml.import(import_attrs) end # Generate annotation if present if xml_mapping.documentation_text || xml_mapping.namespace_class&.documentation generate_annotation(xml, xml_mapping) end # Determine element name and type name for XSD pattern selection element_name = if has_explicit_xml_mapping?(klass, xml_mapping) xml_mapping.element_name || xml_mapping.root_element end type_name = xml_mapping.type_name_value # Generate XSD based on three patterns: # Pattern 1: element only -> inline anonymous complexType # Pattern 2: type_name only -> named complexType (no element) # Pattern 3: both element and type_name -> element + named complexType if element_name && type_name # Pattern 3: Both element and named type xml.element(name: element_name, type: type_name) generate_complex_type(xml, klass, type_name, register, xml_mapping) elsif type_name && !element_name # Pattern 2: Type-only (no element) generate_complex_type(xml, klass, type_name, register, xml_mapping) else # Pattern 1: Anonymous inline (element with no type_name) # Use class name as fallback element name if not specified elem_name = element_name || klass.name xml.element(name: elem_name) do generate_complex_type_content(xml, klass, register, xml_mapping) end end # Generate type definitions for nested models with type_name generate_nested_type_definitions(xml, klass, register) end end |
.get_attribute_xsd_type(attr, attr_type, register, _mapping_rule = nil) ⇒ Object
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 410 def self.get_attribute_xsd_type(attr, attr_type, register, _mapping_rule = nil) # 1. Check for deprecated attribute-level xsd_type override return attr.[:xsd_type] if attr.[:xsd_type] # 2. Check if type has xsd_type method (Type-level) if attr_type.respond_to?(:xsd_type) # Special handling for Reference type if attr_type == Lutaml::Model::Type::Reference target_xsd_type = get_target_xsd_type(attr, register) return attr_type.xsd_type(target_xsd_type) end return attr_type.xsd_type end # 3. Fall back to default mapping get_xsd_type(attr_type) end |
.get_namespace_info(klass) ⇒ Object
Get unified namespace information from Model or Type class
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 446 def self.get_namespace_info(klass) return {} unless klass.is_a?(::Class) # Check for Model class (Serializable) if defined?(Lutaml::Model::Serialize) && klass <= Lutaml::Model::Serialize return get_model_namespace_info(klass) end # Check for Type class (Type::Value) if defined?(Lutaml::Model::Type::Value) && klass <= Lutaml::Model::Type::Value return get_type_namespace_info(klass) end {} end |
.get_target_xsd_type(attr, register) ⇒ Object
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 497 def self.get_target_xsd_type(attr, register) return nil unless attr.[:ref_model_class] return nil unless attr.[:ref_key_attribute] begin model_class = Object.const_get(attr.[:ref_model_class]) target_attr = model_class.attributes[attr.[:ref_key_attribute]] return nil unless target_attr target_type = target_attr.type(register) get_attribute_xsd_type(target_attr, target_type, register) rescue NameError nil end end |
.get_xsd_type(type) ⇒ Object
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 513 def self.get_xsd_type(type) { Lutaml::Model::Type::String => "xs:string", Lutaml::Model::Type::Integer => "xs:integer", Lutaml::Model::Type::Boolean => "xs:boolean", Lutaml::Model::Type::Float => "xs:float", Lutaml::Model::Type::Decimal => "xs:decimal", Lutaml::Model::Type::Date => "xs:date", Lutaml::Model::Type::Time => "xs:time", Lutaml::Model::Type::DateTime => "xs:dateTime", Lutaml::Model::Type::TimeWithoutDate => "xs:time", Lutaml::Model::Type::Duration => "xs:duration", Lutaml::Model::Type::Uri => "xs:anyURI", Lutaml::Model::Type::QName => "xs:QName", Lutaml::Model::Type::Base64Binary => "xs:base64Binary", Lutaml::Model::Type::HexBinary => "xs:hexBinary", Lutaml::Model::Type::Hash => "xs:anyType", Lutaml::Model::Type::Symbol => "xs:string", }[type] || "xs:string" end |
.has_explicit_xml_mapping?(klass, xml_mapping) ⇒ Boolean
403 404 405 406 407 408 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 403 def self.has_explicit_xml_mapping?(klass, xml_mapping) return true unless xml_mapping.root_element base_name = Lutaml::Model::Utils.base_class_name(klass) xml_mapping.root_element != base_name end |
.type_resolvable?(type_name, klass, register) ⇒ Boolean
Check if a custom XSD type can be resolved in the model hierarchy
60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 60 def self.type_resolvable?(type_name, klass, register) # Search in nested model attributes klass.attributes.each_value do |attr| attr_type = attr.type(register) next unless attr_type <= Lutaml::Model::Serialize nested_mapping = attr_type.mappings_for(:xml) return true if nested_mapping&.type_name_value == type_name end false end |
.validate_xsd_types!(klass, register) ⇒ Object
Validate all XSD types referenced by the model
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/lutaml/xml/schema/xsd_schema.rb', line 78 def self.validate_xsd_types!(klass, register) errors = [] klass.attributes.each do |name, attr| attr_type = attr.type(register) # Validate Type::Value xsd_type if attr_type.respond_to?(:xsd_type) type_name = attr_type.xsd_type classification = classify_xsd_type(type_name, klass, register) if classification == :unresolvable errors << "Attribute '#{name}' uses unresolvable xsd_type '#{type_name}'. " \ "Custom types must be defined as LutaML Type::Value or Model classes." end end # Recursively validate nested models if attr_type <= Lutaml::Model::Serialize begin validate_xsd_types!(attr_type, register) rescue Lutaml::Model::UnresolvableTypeError => e errors << "In nested model #{attr_type.name}: #{e.}" end end end if errors.any? raise Lutaml::Model::UnresolvableTypeError, errors.join("\n") end end |