lutaml-model 0.3.3 → 0.3.5

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.
@@ -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|
@@ -206,9 +210,7 @@ module Lutaml
206
210
  else
207
211
  # TODO: This code is problematic because Type.cast does not know
208
212
  # about all the types.
209
- Lutaml::Model::Type.cast(
210
- v, attr_rule.type
211
- )
213
+ Lutaml::Model::Type.cast(v, attr_rule.type)
212
214
  end
213
215
  end
214
216
  elsif value.is_a?(Hash) && attr_rule.type != Lutaml::Model::Type::Hash
@@ -272,7 +274,7 @@ module Lutaml
272
274
  end.public_send(:[]=, path.last.to_s, child_obj.send(attr_name))
273
275
  end
274
276
  end
275
- # hash[mapping.name] ||= {}
277
+
276
278
  hash[map_key] = map_value
277
279
  end
278
280
 
@@ -333,6 +335,8 @@ module Lutaml
333
335
  mapping_hash.item_order = doc.item_order
334
336
  mapping_hash.ordered = mappings_for(:xml).mixed_content? || mixed_content
335
337
 
338
+ mapping_from = []
339
+
336
340
  mappings.each_with_object(mapping_hash) do |rule, hash|
337
341
  attr = attributes[rule.to]
338
342
  raise "Attribute '#{rule.to}' not found in #{self}" unless attr
@@ -368,8 +372,22 @@ module Lutaml
368
372
  value = attr.type.cast(value) unless is_content_mapping
369
373
  end
370
374
 
375
+ mapping_from << rule if rule.custom_methods[:from]
376
+
371
377
  hash[rule.to] = value
372
378
  end
379
+
380
+ mapping_from.each do |rule|
381
+ value = if rule.name.nil?
382
+ mapping_hash[rule.to].join("\n").strip
383
+ else
384
+ mapping_hash[rule.to]
385
+ end
386
+
387
+ mapping_hash[rule.to] = new.send(rule.custom_methods[:from], mapping_hash, value)
388
+ end
389
+
390
+ mapping_hash
373
391
  end
374
392
 
375
393
  def ensure_utf8(value)
@@ -381,7 +399,9 @@ module Lutaml
381
399
  when Hash
382
400
  value.transform_keys do |k|
383
401
  ensure_utf8(k)
384
- end.transform_values { |v| ensure_utf8(v) }
402
+ end.transform_values do |v|
403
+ ensure_utf8(v)
404
+ end
385
405
  else
386
406
  value
387
407
  end
@@ -403,6 +423,8 @@ module Lutaml
403
423
 
404
424
  send(:"#{name}=", self.class.ensure_utf8(value))
405
425
  end
426
+
427
+ validate
406
428
  end
407
429
 
408
430
  def ordered?
@@ -419,6 +441,7 @@ module Lutaml
419
441
 
420
442
  FORMATS.each do |format|
421
443
  define_method(:"to_#{format}") do |options = {}|
444
+ validate
422
445
  adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
423
446
  representation = if format == :xml
424
447
  self
@@ -429,6 +452,15 @@ module Lutaml
429
452
  adapter.new(representation).public_send(:"to_#{format}", options)
430
453
  end
431
454
  end
455
+
456
+ def validate
457
+ self.class.attributes.each do |name, attr|
458
+ value = send(name)
459
+ unless self.class.attr_value_valid?(name, value)
460
+ raise Lutaml::Model::InvalidValueError.new(name, value, attr.options[:values])
461
+ end
462
+ end
463
+ end
432
464
  end
433
465
  end
434
466
  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.3"
5
+ VERSION = "0.3.5"
6
6
  end
7
7
  end
@@ -71,10 +71,14 @@ module Lutaml
71
71
  end
72
72
  end
73
73
 
74
- if xml_mapping.content_mapping
75
- text = element.send(xml_mapping.content_mapping.to)
74
+ if (content_rule = xml_mapping.content_mapping)
75
+ text = element.send(content_rule.to)
76
76
  text = text.join if text.is_a?(Array)
77
77
 
78
+ if content_rule.custom_methods[:to]
79
+ text = @root.send(content_rule.custom_methods[:to], @root, text)
80
+ end
81
+
78
82
  prefixed_xml.text text
79
83
  end
80
84
  end
@@ -130,6 +134,10 @@ module Lutaml
130
134
  end
131
135
 
132
136
  def add_to_xml(xml, value, attribute, rule)
137
+ if rule.custom_methods[:to]
138
+ value = @root.send(rule.custom_methods[:to], @root, value)
139
+ end
140
+
133
141
  if value && (attribute&.type&.<= Lutaml::Model::Serialize)
134
142
  handle_nested_elements(
135
143
  xml,
@@ -69,10 +69,14 @@ module Lutaml
69
69
  end
70
70
  end
71
71
 
72
- if xml_mapping.content_mapping
72
+ if (content_rule = xml_mapping.content_mapping)
73
73
  text = element.send(xml_mapping.content_mapping.to)
74
74
  text = text.join if text.is_a?(Array)
75
75
 
76
+ if content_rule.custom_methods[:to]
77
+ text = @root.send(content_rule.custom_methods[:to], @root, text)
78
+ end
79
+
76
80
  el.text text
77
81
  end
78
82
  end
@@ -113,6 +117,10 @@ module Lutaml
113
117
  end
114
118
 
115
119
  def add_to_xml(xml, value, attribute, rule)
120
+ if rule.custom_methods[:to]
121
+ value = @root.send(rule.custom_methods[:to], @root, value)
122
+ end
123
+
116
124
  if value && (attribute&.type&.<= Lutaml::Model::Serialize)
117
125
  handle_nested_elements(
118
126
  xml,
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.3
4
+ version: 0.3.5
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-16 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-08-18 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