lutaml-model 0.3.2 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,12 +17,20 @@ module Lutaml
17
17
  end
18
18
 
19
19
  def self.generate_definitions(klass)
20
+ defs = { klass.name => generate_class_schema(klass) }
21
+ klass.attributes.each_value do |attr|
22
+ if attr.type <= Lutaml::Model::Serialize
23
+ defs.merge!(generate_definitions(attr.type))
24
+ end
25
+ end
26
+ defs
27
+ end
28
+
29
+ def self.generate_class_schema(klass)
20
30
  {
21
- klass.name => {
22
- "type" => "object",
23
- "properties" => generate_properties(klass),
24
- "required" => klass.attributes.keys,
25
- },
31
+ "type" => "object",
32
+ "properties" => generate_properties(klass),
33
+ "required" => klass.attributes.keys,
26
34
  }
27
35
  end
28
36
 
@@ -33,7 +41,16 @@ module Lutaml
33
41
  end
34
42
 
35
43
  def self.generate_property_schema(attr)
36
- { "type" => get_json_type(attr.type) }
44
+ if attr.type <= Lutaml::Model::Serialize
45
+ { "$ref" => "#/$defs/#{attr.type.name}" }
46
+ elsif attr.collection?
47
+ {
48
+ "type" => "array",
49
+ "items" => { "type" => get_json_type(attr.type) },
50
+ }
51
+ else
52
+ { "type" => get_json_type(attr.type) }
53
+ end
37
54
  end
38
55
 
39
56
  def self.get_json_type(type)
@@ -42,7 +59,6 @@ module Lutaml
42
59
  Lutaml::Model::Type::Integer => "integer",
43
60
  Lutaml::Model::Type::Boolean => "boolean",
44
61
  Lutaml::Model::Type::Float => "number",
45
- Lutaml::Model::Type::Array => "array",
46
62
  Lutaml::Model::Type::Hash => "object",
47
63
  }[type] || "string" # Default to string for unknown types
48
64
  end
@@ -0,0 +1,91 @@
1
+ require "json-schema"
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ class JsonSchemaParser
7
+ def self.parse(schema_json)
8
+ schema = JSON::Schema.parse(schema_json)
9
+ definitions = schema.schema.fetch("$defs", {})
10
+ definitions.map do |class_name, class_schema|
11
+ generate_class_definition(class_name, class_schema)
12
+ end.join("\n")
13
+ end
14
+
15
+ def self.generate_class_definition(class_name, class_schema)
16
+ attributes = class_schema["properties"] || {}
17
+ required_attributes = class_schema["required"] || []
18
+
19
+ <<~RUBY
20
+ class #{class_name} < Lutaml::Model::Serializable
21
+ #{generate_attributes(attributes, required_attributes)}
22
+
23
+ json do
24
+ #{generate_json_mappings(attributes)}
25
+ end
26
+ end
27
+ RUBY
28
+ end
29
+
30
+ def self.generate_attributes(attributes, required_attributes)
31
+ attributes.map do |name, schema|
32
+ type = schema["type"]
33
+ ruby_type = get_ruby_type(type, schema)
34
+ attributes = [
35
+ "attribute :#{name}",
36
+ "Lutaml::Model::Type::#{ruby_type}",
37
+ "required: #{required_attributes.include?(name).inspect}",
38
+ ]
39
+
40
+ attributes.join(", ")
41
+ end.join("\n ")
42
+ end
43
+
44
+ def self.generate_json_mappings(attributes)
45
+ attributes.keys.map do |name|
46
+ "map '#{name}', to: :#{name}"
47
+ end.join("\n ")
48
+ end
49
+
50
+ def self.get_ruby_type(type, schema)
51
+ case type
52
+ when "integer"
53
+ "Integer"
54
+ when "boolean"
55
+ "Boolean"
56
+ when "number"
57
+ "Float"
58
+ when "array"
59
+ item_schema = schema["items"]
60
+ item_type = get_ruby_type(item_schema["type"], item_schema)
61
+ "Array.of(#{item_type})"
62
+ when "object"
63
+ object_class_name(schema)
64
+ else
65
+ "String" # Default to string for unknown types
66
+ end
67
+ end
68
+
69
+ def self.object_class_name(schema)
70
+ nested_class_name = schema["title"] || "NestedObject"
71
+ nested_class_definition = generate_class_definition(
72
+ nested_class_name, schema
73
+ )
74
+ @nested_classes ||= []
75
+ @nested_classes << nested_class_definition
76
+ nested_class_name
77
+ end
78
+
79
+ def self.nested_classes
80
+ @nested_classes ||= []
81
+ end
82
+
83
+ def self.generate(schema_json)
84
+ @nested_classes = []
85
+ main_classes = parse(schema_json)
86
+ (nested_classes + [main_classes]).join("\n")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -4,22 +4,52 @@ module Lutaml
4
4
  module Model
5
5
  module Schema
6
6
  class RelaxngSchema
7
- def self.generate(klass, _options = {})
8
- schema = Nokogiri::XML::Builder.new do |xml|
9
- xml.element(name: klass.name) do
10
- xml.complexType do
11
- xml.sequence do
12
- generate_elements(klass, xml)
7
+ def self.generate(klass, options = {})
8
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
9
+ xml.grammar(xmlns: "http://relaxng.org/ns/structure/1.0") do
10
+ generate_start(xml, klass)
11
+ generate_define(xml, klass)
12
+ end
13
+ end
14
+
15
+ options[:pretty] ? builder.to_xml(indent: 2) : builder.to_xml
16
+ end
17
+
18
+ def self.generate_start(xml, klass)
19
+ xml.start do
20
+ xml.ref(name: klass.name)
21
+ end
22
+ end
23
+
24
+ def self.generate_attributes(xml, klass)
25
+ klass.attributes.each do |name, attr|
26
+ if attr.type <= Lutaml::Model::Serialize
27
+ xml.ref(name: attr.type.name)
28
+ elsif attr.collection?
29
+ xml.zeroOrMore do
30
+ xml.element(name: name) do
31
+ xml.data(type: get_relaxng_type(attr.type))
13
32
  end
14
33
  end
34
+ else
35
+ xml.element(name: name) do
36
+ xml.data(type: get_relaxng_type(attr.type))
37
+ end
15
38
  end
16
39
  end
17
- schema.to_xml
18
40
  end
19
41
 
20
- def self.generate_elements(klass, xml)
21
- klass.attributes.each do |name, attr|
22
- xml.element(name: name, type: get_relaxng_type(attr.type))
42
+ def self.generate_define(xml, klass)
43
+ xml.define(name: klass.name) do
44
+ xml.element(name: klass.name) do
45
+ generate_attributes(xml, klass)
46
+ end
47
+ end
48
+
49
+ klass.attributes.each_value do |attr|
50
+ if attr.type <= Lutaml::Model::Serialize
51
+ generate_define(xml, attr.type)
52
+ end
23
53
  end
24
54
  end
25
55
 
@@ -29,8 +59,8 @@ module Lutaml
29
59
  Lutaml::Model::Type::Integer => "integer",
30
60
  Lutaml::Model::Type::Boolean => "boolean",
31
61
  Lutaml::Model::Type::Float => "float",
32
- Lutaml::Model::Type::Array => "array",
33
- Lutaml::Model::Type::Hash => "object",
62
+ Lutaml::Model::Type::Decimal => "decimal",
63
+ Lutaml::Model::Type::Hash => "string",
34
64
  }[type] || "string" # Default to string for unknown types
35
65
  end
36
66
  end
@@ -4,22 +4,41 @@ module Lutaml
4
4
  module Model
5
5
  module Schema
6
6
  class XsdSchema
7
- def self.generate(klass, _options = {})
8
- schema = Nokogiri::XML::Builder.new do |xml|
7
+ def self.generate(klass, options = {})
8
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
9
9
  xml.schema(xmlns: "http://www.w3.org/2001/XMLSchema") do
10
- xml.element(name: klass.name) do
11
- xml.complexType do
12
- xml.sequence { generate_elements(klass, xml) }
13
- end
10
+ generate_complex_type(xml, klass)
11
+ end
12
+ end
13
+
14
+ options[:pretty] ? builder.to_xml(indent: 2) : builder.to_xml
15
+ end
16
+
17
+ def self.generate_complex_type(xml, klass, element_name = nil)
18
+ xml.element(name: element_name || klass.name) do
19
+ xml.complexType do
20
+ xml.sequence do
21
+ generate_elements(xml, klass)
14
22
  end
15
23
  end
16
24
  end
17
- schema.to_xml
18
25
  end
19
26
 
20
- def self.generate_elements(klass, xml)
27
+ def self.generate_elements(xml, klass)
21
28
  klass.attributes.each do |name, attr|
22
- xml.element(name: name, type: get_xsd_type(attr.type))
29
+ if attr.type <= Lutaml::Model::Serialize
30
+ generate_complex_type(xml, attr.type, name)
31
+ elsif attr.collection?
32
+ xml.element(name: name, minOccurs: "0", maxOccurs: "unbounded") do
33
+ xml.complexType do
34
+ xml.sequence do
35
+ xml.element(name: "item", type: get_xsd_type(attr.type))
36
+ end
37
+ end
38
+ end
39
+ else
40
+ xml.element(name: name, type: get_xsd_type(attr.type))
41
+ end
23
42
  end
24
43
  end
25
44
 
@@ -30,8 +49,7 @@ module Lutaml
30
49
  Lutaml::Model::Type::Boolean => "xs:boolean",
31
50
  Lutaml::Model::Type::Float => "xs:float",
32
51
  Lutaml::Model::Type::Decimal => "xs:decimal",
33
- Lutaml::Model::Type::Array => "xs:array",
34
- Lutaml::Model::Type::Hash => "xs:object",
52
+ Lutaml::Model::Type::Hash => "xs:anyType",
35
53
  }[type] || "xs:string" # Default to string for unknown types
36
54
  end
37
55
  end
@@ -4,17 +4,34 @@ module Lutaml
4
4
  module Model
5
5
  module Schema
6
6
  class YamlSchema
7
- def self.generate(klass, _options = {})
8
- schema = {
7
+ def self.generate(klass, options = {})
8
+ schema = generate_schema(klass)
9
+ options[:pretty] ? schema.to_yaml : YAML.dump(schema)
10
+ end
11
+
12
+ def self.generate_schema(klass)
13
+ {
9
14
  "type" => "map",
10
15
  "mapping" => generate_mapping(klass),
11
16
  }
12
- YAML.dump(schema)
13
17
  end
14
18
 
15
19
  def self.generate_mapping(klass)
16
20
  klass.attributes.each_with_object({}) do |(name, attr), mapping|
17
- mapping[name.to_s] = { "type" => get_yaml_type(attr.type) }
21
+ mapping[name.to_s] = generate_attribute_schema(attr)
22
+ end
23
+ end
24
+
25
+ def self.generate_attribute_schema(attr)
26
+ if attr.type <= Lutaml::Model::Serialize
27
+ generate_schema(attr.type)
28
+ elsif attr.collection?
29
+ {
30
+ "type" => "seq",
31
+ "sequence" => [{ "type" => get_yaml_type(attr.type) }],
32
+ }
33
+ else
34
+ { "type" => get_yaml_type(attr.type) }
18
35
  end
19
36
  end
20
37
 
@@ -24,9 +41,7 @@ module Lutaml
24
41
  Lutaml::Model::Type::Integer => "int",
25
42
  Lutaml::Model::Type::Boolean => "bool",
26
43
  Lutaml::Model::Type::Float => "float",
27
- # YAML does not have a separate decimal type, so we use float
28
44
  Lutaml::Model::Type::Decimal => "float",
29
- Lutaml::Model::Type::Array => "seq",
30
45
  Lutaml::Model::Type::Hash => "map",
31
46
  }[type] || "str" # Default to string for unknown types
32
47
  end
@@ -7,3 +7,5 @@ module Lutaml
7
7
  end
8
8
  end
9
9
  end
10
+
11
+ require_relative "comparable_nil"
@@ -8,12 +8,15 @@ require_relative "mapping_hash"
8
8
  require_relative "xml_mapping"
9
9
  require_relative "key_value_mapping"
10
10
  require_relative "json_adapter"
11
+ require_relative "comparable_model"
11
12
 
12
13
  module Lutaml
13
14
  module Model
14
15
  module Serialize
15
16
  FORMATS = %i[xml json yaml toml].freeze
16
17
 
18
+ include ComparableModel
19
+
17
20
  def self.included(base)
18
21
  base.extend(ClassMethods)
19
22
  end
@@ -50,11 +53,8 @@ module Lutaml
50
53
  end
51
54
 
52
55
  define_method(:"#{name}=") do |value|
53
- unless self.class.attr_value_valid?(name, value)
54
- raise Lutaml::Model::InvalidValueError.new(name, value, options[:values])
55
- end
56
-
57
56
  instance_variable_set(:"@#{name}", value)
57
+ validate
58
58
  end
59
59
  end
60
60
 
@@ -64,9 +64,13 @@ module Lutaml
64
64
 
65
65
  return true unless attr.options[:values]
66
66
 
67
- # If value validation failed but there is a default value, do not
68
- # raise a validation error
69
- attr.options[:values].include?(value || attr.default)
67
+ # Allow nil values if there's no default
68
+ return true if value.nil? && !attr.default
69
+
70
+ # Use the default value if the value is nil
71
+ value = attr.default if value.nil?
72
+
73
+ attr.options[:values].include?(value)
70
74
  end
71
75
 
72
76
  FORMATS.each do |format|
@@ -403,6 +407,8 @@ module Lutaml
403
407
 
404
408
  send(:"#{name}=", self.class.ensure_utf8(value))
405
409
  end
410
+
411
+ validate
406
412
  end
407
413
 
408
414
  def ordered?
@@ -419,6 +425,7 @@ module Lutaml
419
425
 
420
426
  FORMATS.each do |format|
421
427
  define_method(:"to_#{format}") do |options = {}|
428
+ validate
422
429
  adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
423
430
  representation = if format == :xml
424
431
  self
@@ -429,6 +436,15 @@ module Lutaml
429
436
  adapter.new(representation).public_send(:"to_#{format}", options)
430
437
  end
431
438
  end
439
+
440
+ def validate
441
+ self.class.attributes.each do |name, attr|
442
+ value = send(name)
443
+ unless self.class.attr_value_valid?(name, value)
444
+ raise Lutaml::Model::InvalidValueError.new(name, value, attr.options[:values])
445
+ end
446
+ end
447
+ end
432
448
  end
433
449
  end
434
450
  end
@@ -42,14 +42,6 @@ module Lutaml
42
42
  HEREDOC
43
43
  end
44
44
 
45
- # TODO: Remove this. The XSD code depends on this but actually should
46
- # be converted into a collection, not a specific Array type.
47
- class Array
48
- def initialize(array)
49
- Array(array)
50
- end
51
- end
52
-
53
45
  UUID_REGEX = /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/
54
46
 
55
47
  def self.cast(value, type)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.3.2"
5
+ VERSION = "0.3.4"
6
6
  end
7
7
  end
@@ -48,7 +48,8 @@ module Lutaml
48
48
  xml
49
49
  end
50
50
 
51
- prefixed_xml.public_send(xml_mapping.root_element, attributes) do
51
+ tag_name = options[:tag_name] || xml_mapping.root_element
52
+ prefixed_xml.public_send(tag_name, attributes) do
52
53
  if options.key?(:namespace_prefix) && !options[:namespace_prefix]
53
54
  xml.parent.namespace = nil
54
55
  end
@@ -94,7 +95,8 @@ module Lutaml
94
95
  xml
95
96
  end
96
97
 
97
- prefixed_xml.public_send(xml_mapping.root_element, attributes) do
98
+ tag_name = options[:tag_name] || xml_mapping.root_element
99
+ prefixed_xml.public_send(tag_name, attributes) do
98
100
  if options.key?(:namespace_prefix) && !options[:namespace_prefix]
99
101
  xml.parent.namespace = nil
100
102
  end
@@ -36,12 +36,13 @@ module Lutaml
36
36
 
37
37
  attributes = build_attributes(element, xml_mapping).compact
38
38
 
39
+ tag_name = options[:tag_name] || xml_mapping.root_element
39
40
  prefixed_name = if options.key?(:namespace_prefix)
40
- [options[:namespace_prefix], xml_mapping.root_element].compact.join(":")
41
+ [options[:namespace_prefix], tag_name].compact.join(":")
41
42
  elsif xml_mapping.namespace_prefix
42
- "#{xml_mapping.namespace_prefix}:#{xml_mapping.root_element}"
43
+ "#{xml_mapping.namespace_prefix}:#{tag_name}"
43
44
  else
44
- xml_mapping.root_element
45
+ tag_name
45
46
  end
46
47
 
47
48
  builder.element(prefixed_name, attributes) do |el|
@@ -84,7 +85,8 @@ module Lutaml
84
85
 
85
86
  attributes = build_attributes(element, xml_mapping).compact
86
87
 
87
- builder.element(xml_mapping.root_element, attributes) do |el|
88
+ tag_name = options[:tag_name] || xml_mapping.root_element
89
+ builder.element(tag_name, attributes) do |el|
88
90
  index_hash = {}
89
91
 
90
92
  element.element_order.each do |name|
@@ -60,6 +60,7 @@ module Lutaml
60
60
 
61
61
  options[:namespace_prefix] = rule.prefix if rule&.namespace_set?
62
62
  options[:mixed_content] = rule.mixed_content
63
+ options[:tag_name] = rule.name
63
64
 
64
65
  options[:mapper_class] = attribute&.type if attribute
65
66
 
@@ -9,8 +9,8 @@ module Lutaml
9
9
  :mixed_content
10
10
 
11
11
  def initialize
12
- @elements = []
13
- @attributes = []
12
+ @elements = {}
13
+ @attributes = {}
14
14
  @content_mapping = nil
15
15
  @mixed_content = false
16
16
  end
@@ -47,7 +47,7 @@ module Lutaml
47
47
  prefix: nil,
48
48
  mixed: false
49
49
  )
50
- @elements << XmlMappingRule.new(
50
+ @elements[name] = XmlMappingRule.new(
51
51
  name,
52
52
  to: to,
53
53
  render_nil: render_nil,
@@ -70,7 +70,7 @@ module Lutaml
70
70
  nil),
71
71
  prefix: nil
72
72
  )
73
- @attributes << XmlMappingRule.new(
73
+ @attributes[name] = XmlMappingRule.new(
74
74
  name,
75
75
  to: to,
76
76
  render_nil: render_nil,
@@ -102,11 +102,11 @@ module Lutaml
102
102
  end
103
103
 
104
104
  def elements
105
- @elements
105
+ @elements.values
106
106
  end
107
107
 
108
108
  def attributes
109
- @attributes
109
+ @attributes.values
110
110
  end
111
111
 
112
112
  def content_mapping
data/lutaml-model.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.homepage = "https://github.com/lutaml/lutaml-model"
17
17
  spec.license = "BSD-2-Clause"
18
18
 
19
- spec.bindir = "bin"
19
+ spec.bindir = "exe"
20
20
  spec.require_paths = ["lib"]
21
21
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
22
22
 
@@ -30,8 +30,5 @@ Gem::Specification.new do |spec|
30
30
  end
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
 
33
- # spec.add_runtime_dependency "expressir"
34
- # spec.add_runtime_dependency "metanorma-cli"
35
- # spec.add_runtime_dependency "shale"
36
- # spec.add_runtime_dependency "thor", ">= 0.20"
33
+ spec.add_dependency "thor"
37
34
  end
metadata CHANGED
@@ -1,21 +1,36 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-15 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-08-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: 'LutaML creating data models in Ruby
14
28
 
15
29
  '
16
30
  email:
17
31
  - open.source@ribose.com
18
- executables: []
32
+ executables:
33
+ - lutaml-model
19
34
  extensions: []
20
35
  extra_rdoc_files: []
21
36
  files:
@@ -31,8 +46,13 @@ files:
31
46
  - Rakefile
32
47
  - bin/console
33
48
  - bin/setup
49
+ - exe/lutaml-model
34
50
  - lib/lutaml/model.rb
35
51
  - lib/lutaml/model/attribute.rb
52
+ - lib/lutaml/model/cli.rb
53
+ - lib/lutaml/model/comparable_model.rb
54
+ - lib/lutaml/model/comparable_nil.rb
55
+ - lib/lutaml/model/comparison.rb
36
56
  - lib/lutaml/model/config.rb
37
57
  - lib/lutaml/model/error.rb
38
58
  - lib/lutaml/model/error/invalid_value_error.rb
@@ -47,6 +67,7 @@ files:
47
67
  - lib/lutaml/model/mapping_rule.rb
48
68
  - lib/lutaml/model/schema.rb
49
69
  - lib/lutaml/model/schema/json_schema.rb
70
+ - lib/lutaml/model/schema/json_schema_parser.rb
50
71
  - lib/lutaml/model/schema/relaxng_schema.rb
51
72
  - lib/lutaml/model/schema/xsd_schema.rb
52
73
  - lib/lutaml/model/schema/yaml_schema.rb