lutaml-model 0.1.0

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +27 -0
  3. data/.github/workflows/rake.yml +15 -0
  4. data/.github/workflows/release.yml +23 -0
  5. data/.gitignore +11 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +12 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +12 -0
  10. data/README.adoc +380 -0
  11. data/Rakefile +12 -0
  12. data/bin/console +11 -0
  13. data/bin/setup +8 -0
  14. data/lib/lutaml/model/attribute.rb +26 -0
  15. data/lib/lutaml/model/config.rb +14 -0
  16. data/lib/lutaml/model/json_adapter/multi_json.rb +20 -0
  17. data/lib/lutaml/model/json_adapter/standard.rb +20 -0
  18. data/lib/lutaml/model/json_adapter.rb +38 -0
  19. data/lib/lutaml/model/key_value_mapping.rb +20 -0
  20. data/lib/lutaml/model/key_value_mapping_rule.rb +10 -0
  21. data/lib/lutaml/model/mapping_rule.rb +34 -0
  22. data/lib/lutaml/model/schema/json_schema.rb +63 -0
  23. data/lib/lutaml/model/schema/relaxng_schema.rb +50 -0
  24. data/lib/lutaml/model/schema/xsd_schema.rb +54 -0
  25. data/lib/lutaml/model/schema/yaml_schema.rb +47 -0
  26. data/lib/lutaml/model/schema.rb +27 -0
  27. data/lib/lutaml/model/serializable.rb +10 -0
  28. data/lib/lutaml/model/serialize.rb +179 -0
  29. data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +20 -0
  30. data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +20 -0
  31. data/lib/lutaml/model/toml_adapter.rb +37 -0
  32. data/lib/lutaml/model/type/time_without_date.rb +17 -0
  33. data/lib/lutaml/model/type.rb +114 -0
  34. data/lib/lutaml/model/version.rb +7 -0
  35. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +117 -0
  36. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +75 -0
  37. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +75 -0
  38. data/lib/lutaml/model/xml_adapter.rb +60 -0
  39. data/lib/lutaml/model/xml_mapping.rb +65 -0
  40. data/lib/lutaml/model/xml_mapping_rule.rb +16 -0
  41. data/lib/lutaml/model/yaml_adapter.rb +24 -0
  42. data/lib/lutaml/model.rb +21 -0
  43. data/lutaml-model.gemspec +50 -0
  44. data/sig/lutaml/model.rbs +6 -0
  45. metadata +228 -0
@@ -0,0 +1,114 @@
1
+ # lib/lutaml/model/type.rb
2
+ require "date"
3
+ require "bigdecimal"
4
+ require "securerandom"
5
+ require "uri"
6
+ require "ipaddr"
7
+ require "json"
8
+
9
+ module Lutaml
10
+ module Model
11
+ module Type
12
+ class Boolean; end
13
+
14
+ %w(String
15
+ Integer
16
+ Float
17
+ Date
18
+ Time
19
+ Boolean
20
+ Decimal
21
+ Hash
22
+ UUID
23
+ Symbol
24
+ BigInteger
25
+ Binary
26
+ URL
27
+ Email
28
+ IPAddress
29
+ JSON
30
+ Enum).each do |t|
31
+ class_eval <<~HEREDOC
32
+ class #{t}
33
+ def self.cast(value)
34
+ Type.cast(value, #{t})
35
+ end
36
+ end
37
+
38
+ HEREDOC
39
+ end
40
+
41
+ class TimeWithoutDate
42
+ def self.cast(value)
43
+ parsed_time = ::Time.parse(value.to_s)
44
+ parsed_time.strftime("%H:%M:%S")
45
+ end
46
+
47
+ def self.serialize(value)
48
+ value.strftime("%H:%M:%S")
49
+ end
50
+ end
51
+
52
+ class DateTime
53
+ def self.cast(value)
54
+ ::DateTime.parse(value.to_s).new_offset(0).iso8601
55
+ end
56
+ end
57
+
58
+ def self.cast(value, type)
59
+ case type
60
+ when String
61
+ value.to_s
62
+ when Integer
63
+ value.to_i
64
+ when Float
65
+ value.to_f
66
+ when Date
67
+ begin
68
+ ::Date.parse(value.to_s)
69
+ rescue ArgumentError
70
+ nil
71
+ end
72
+ when DateTime
73
+ DateTime.cast(value)
74
+ when Time
75
+ ::Time.parse(value.to_s)
76
+ when TimeWithoutDate
77
+ TimeWithoutDate.cast(value)
78
+ when Boolean
79
+ to_boolean(value)
80
+ when Decimal
81
+ BigDecimal(value.to_s)
82
+ when Hash
83
+ Hash(value)
84
+ when UUID
85
+ value =~ /\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/ ? value : SecureRandom.uuid
86
+ when Symbol
87
+ value.to_sym
88
+ when BigInteger
89
+ value.to_i
90
+ when Binary
91
+ value.force_encoding("BINARY")
92
+ when URL
93
+ URI.parse(value.to_s)
94
+ when Email
95
+ value.to_s
96
+ when IPAddress
97
+ IPAddr.new(value.to_s)
98
+ when JSON
99
+ ::JSON.parse(value)
100
+ when Enum
101
+ value
102
+ else
103
+ value
104
+ end
105
+ end
106
+
107
+ def self.to_boolean(value)
108
+ return true if value == true || value.to_s =~ (/^(true|t|yes|y|1)$/i)
109
+ return false if value == false || value.nil? || value.to_s =~ (/^(false|f|no|n|0)$/i)
110
+ raise ArgumentError.new("invalid value for Boolean: \"#{value}\"")
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,117 @@
1
+ # lib/lutaml/model/xml_adapter/nokogiri_adapter.rb
2
+ require "nokogiri"
3
+ require_relative "../xml_adapter"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module XmlAdapter
8
+ class NokogiriDocument < Document
9
+ def self.parse(xml)
10
+ parsed = Nokogiri::XML(xml)
11
+ root = NokogiriElement.new(parsed.root)
12
+ new(root)
13
+ end
14
+
15
+ def initialize(root)
16
+ @root = root
17
+ end
18
+
19
+ def to_h
20
+ { @root.name => parse_element(@root) }
21
+ end
22
+
23
+ def to_xml(options = {})
24
+ builder = Nokogiri::XML::Builder.new do |xml|
25
+ build_element(xml, @root, options)
26
+ end
27
+
28
+ xml_data = builder.to_xml(options[:pretty] ? { indent: 2 } : {})
29
+ options[:declaration] ? declaration(options) + xml_data : xml_data
30
+ end
31
+
32
+ private
33
+
34
+ def build_element(xml, element, options = {})
35
+ xml_mapping = element.class.mappings_for(:xml)
36
+ return xml unless xml_mapping
37
+
38
+ attributes = build_attributes(element, xml_mapping)
39
+
40
+ xml.send(xml_mapping.root_element, attributes) do
41
+ xml_mapping.elements.each do |element_rule|
42
+ attribute_def = element.class.attributes[element_rule.to]
43
+ value = element.send(element_rule.to)
44
+
45
+ if attribute_def&.type <= Lutaml::Model::Serialize
46
+ handle_nested_elements(xml, element_rule, value)
47
+ else
48
+ xml.send(element_rule.name) { xml.text value }
49
+ end
50
+ end
51
+ xml.text element.text unless xml_mapping.elements.any?
52
+ end
53
+ end
54
+
55
+ def build_attributes(element, xml_mapping)
56
+ xml_mapping.attributes.each_with_object(namespace_attributes(xml_mapping)) do |mapping_rule, hash|
57
+ full_name = if mapping_rule.namespace
58
+ "#{mapping_rule.prefix ? "#{mapping_rule.prefix}:" : ""}#{mapping_rule.name}"
59
+ else
60
+ mapping_rule.name
61
+ end
62
+ hash[full_name] = element.send(mapping_rule.to)
63
+ end
64
+ end
65
+
66
+ def namespace_attributes(xml_mapping)
67
+ return {} unless xml_mapping.namespace_uri
68
+
69
+ if xml_mapping.namespace_prefix
70
+ { "xmlns:#{xml_mapping.namespace_prefix}" => xml_mapping.namespace_uri }
71
+ else
72
+ { "xmlns" => xml_mapping.namespace_uri }
73
+ end
74
+ end
75
+
76
+ def handle_nested_elements(xml, element_rule, value)
77
+ case value
78
+ when Array
79
+ value.each { |val| build_element(xml, val) }
80
+ else
81
+ build_element(xml, value)
82
+ end
83
+ end
84
+
85
+ def parse_element(element)
86
+ result = element.children.each_with_object({}) do |child, hash|
87
+ next if child.text?
88
+
89
+ hash[child.name] ||= []
90
+ hash[child.name] << parse_element(child)
91
+ end
92
+ result["_text"] = element.text if element.text?
93
+ result
94
+ end
95
+ end
96
+
97
+ class NokogiriElement < Element
98
+ def initialize(node)
99
+ attributes = node.attributes.transform_values do |attr|
100
+ Attribute.new(attr.name, attr.value, namespace: attr.namespace&.href, namespace_prefix: attr.namespace&.prefix)
101
+ end
102
+ super(node.name, attributes, parse_children(node), node.text, namespace: node.namespace&.href, namespace_prefix: node.namespace&.prefix)
103
+ end
104
+
105
+ def text?
106
+ false
107
+ end
108
+
109
+ private
110
+
111
+ def parse_children(node)
112
+ node.children.select(&:element?).map { |child| NokogiriElement.new(child) }
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,75 @@
1
+ # lib/lutaml/model/xml_adapter/oga_adapter.rb
2
+ require "oga"
3
+ require_relative "../xml_adapter"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module XmlAdapter
8
+ class OgaDocument < Document
9
+ def self.parse(xml)
10
+ parsed = Oga.parse_xml(xml)
11
+ root = OgaElement.new(parsed)
12
+ new(root)
13
+ end
14
+
15
+ def initialize(root)
16
+ @root = root
17
+ end
18
+
19
+ def to_h
20
+ { @root.name => parse_element(@root) }
21
+ end
22
+
23
+ def to_xml(options = {})
24
+ builder = Oga::XML::Builder.new
25
+ build_element(builder, @root, options)
26
+ xml_data = builder.to_xml
27
+ options[:declaration] ? declaration(options) + xml_data : xml_data
28
+ end
29
+
30
+ private
31
+
32
+ def build_element(builder, element, options = {})
33
+ attributes = build_attributes(element.attributes)
34
+ builder.element(element.name, attributes) do
35
+ element.children.each do |child|
36
+ build_element(builder, child, options)
37
+ end
38
+ builder.text(element.text) if element.text
39
+ end
40
+ end
41
+
42
+ def build_attributes(attributes)
43
+ attributes.each_with_object({}) do |(name, attr), hash|
44
+ hash[name] = attr.value
45
+ end
46
+ end
47
+
48
+ def parse_element(element)
49
+ result = { "_text" => element.text }
50
+ element.children.each do |child|
51
+ next if child.is_a?(Oga::XML::Text)
52
+ result[child.name] ||= []
53
+ result[child.name] << parse_element(child)
54
+ end
55
+ result
56
+ end
57
+ end
58
+
59
+ class OgaElement < Element
60
+ def initialize(node)
61
+ attributes = node.attributes.each_with_object({}) do |attr, hash|
62
+ hash[attr.name] = Attribute.new(attr.name, attr.value)
63
+ end
64
+ super(node.name, attributes, parse_children(node), node.text)
65
+ end
66
+
67
+ private
68
+
69
+ def parse_children(node)
70
+ node.children.select { |child| child.is_a?(Oga::XML::Element) }.map { |child| OgaElement.new(child) }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,75 @@
1
+ # lib/lutaml/model/xml_adapter/ox_adapter.rb
2
+ require "ox"
3
+ require_relative "../xml_adapter"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module XmlAdapter
8
+ class OxDocument < Document
9
+ def self.parse(xml)
10
+ parsed = Ox.parse(xml)
11
+ root = OxElement.new(parsed)
12
+ new(root)
13
+ end
14
+
15
+ def initialize(root)
16
+ @root = root
17
+ end
18
+
19
+ def to_h
20
+ { @root.name => parse_element(@root) }
21
+ end
22
+
23
+ def to_xml(options = {})
24
+ builder = Ox::Builder.new
25
+ build_element(builder, @root, options)
26
+ xml_data = Ox.dump(builder)
27
+ options[:declaration] ? declaration(options) + xml_data : xml_data
28
+ end
29
+
30
+ private
31
+
32
+ def build_element(builder, element, options = {})
33
+ attributes = build_attributes(element.attributes)
34
+ builder.element(element.name, attributes) do
35
+ element.children.each do |child|
36
+ build_element(builder, child, options)
37
+ end
38
+ builder.text(element.text) if element.text
39
+ end
40
+ end
41
+
42
+ def build_attributes(attributes)
43
+ attributes.each_with_object({}) do |(name, attr), hash|
44
+ hash[name] = attr.value
45
+ end
46
+ end
47
+
48
+ def parse_element(element)
49
+ result = { "_text" => element.text }
50
+ element.nodes.each do |child|
51
+ next if child.is_a?(Ox::Raw) || child.is_a?(Ox::Comment)
52
+ result[child.name] ||= []
53
+ result[child.name] << parse_element(child)
54
+ end
55
+ result
56
+ end
57
+ end
58
+
59
+ class OxElement < Element
60
+ def initialize(node)
61
+ attributes = node.attributes.each_with_object({}) do |(name, value), hash|
62
+ hash[name.to_s] = Attribute.new(name.to_s, value)
63
+ end
64
+ super(node.name.to_s, attributes, parse_children(node), node.text)
65
+ end
66
+
67
+ private
68
+
69
+ def parse_children(node)
70
+ node.nodes.select { |child| child.is_a?(Ox::Element) }.map { |child| OxElement.new(child) }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,60 @@
1
+ # lib/lutaml/model/xml_adapter.rb
2
+
3
+ module Lutaml
4
+ module Model
5
+ module XmlAdapter
6
+ class Document
7
+ attr_reader :root
8
+
9
+ def initialize(root)
10
+ @root = root
11
+ end
12
+
13
+ def self.parse(xml)
14
+ raise NotImplementedError, "Subclasses must implement `parse`."
15
+ end
16
+
17
+ def children
18
+ @root.children
19
+ end
20
+
21
+ def declaration(options)
22
+ version = options[:declaration].is_a?(String) ? options[:declaration] : "1.0"
23
+ encoding = options[:encoding].is_a?(String) ? options[:encoding] : (options[:encoding] ? "UTF-8" : nil)
24
+ declaration = "<?xml version=\"#{version}\""
25
+ declaration += " encoding=\"#{encoding}\"" if encoding
26
+ declaration += "?>\n"
27
+ declaration
28
+ end
29
+ end
30
+
31
+ class Element
32
+ attr_reader :name, :attributes, :children, :text, :namespace, :namespace_prefix
33
+
34
+ def initialize(name, attributes = {}, children = [], text = nil, namespace: nil, namespace_prefix: nil)
35
+ @name = name
36
+ @attributes = attributes.map { |k, v| Attribute.new(k, v) }
37
+ @children = children
38
+ @text = text
39
+ @namespace = namespace
40
+ @namespace_prefix = namespace_prefix
41
+ end
42
+
43
+ def document
44
+ Document.new(self)
45
+ end
46
+ end
47
+
48
+ class Attribute
49
+ attr_reader :name, :value, :namespace, :namespace_prefix
50
+
51
+ def initialize(name, value, namespace: nil, namespace_prefix: nil)
52
+ @name = name
53
+ @value = value
54
+ @namespace = namespace
55
+ @namespace_prefix = namespace_prefix
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,65 @@
1
+ # lib/lutaml/model/xml_mapping.rb
2
+ require_relative "xml_mapping_rule"
3
+
4
+ module Lutaml
5
+ module Model
6
+ class XmlMapping
7
+ attr_reader :root_element, :namespace_uri, :namespace_prefix
8
+
9
+ def initialize
10
+ @elements = []
11
+ @attributes = []
12
+ @content_mapping = nil
13
+ end
14
+
15
+ def root(name)
16
+ @root_element = name
17
+ end
18
+
19
+ def namespace(uri, prefix = nil)
20
+ @namespace_uri = uri
21
+ @namespace_prefix = prefix
22
+ end
23
+
24
+ def map_element(name, to:, render_nil: false, with: {}, delegate: nil, namespace: nil, prefix: nil)
25
+ @elements << XmlMappingRule.new(name, to: to, render_nil: render_nil, with: with, delegate: delegate, namespace: namespace, prefix: prefix)
26
+ end
27
+
28
+ def map_attribute(name, to:, render_nil: false, with: {}, delegate: nil, namespace: nil, prefix: nil)
29
+ @attributes << XmlMappingRule.new(name, to: to, render_nil: render_nil, with: with, delegate: delegate, namespace: namespace, prefix: prefix)
30
+ end
31
+
32
+ def map_content(to:, render_nil: false, with: {}, delegate: nil)
33
+ @content_mapping = XmlMappingRule.new(nil, to: to, render_nil: render_nil, with: with, delegate: delegate)
34
+ end
35
+
36
+ def elements
37
+ @elements
38
+ end
39
+
40
+ def attributes
41
+ @attributes
42
+ end
43
+
44
+ def content_mapping
45
+ @content_mapping
46
+ end
47
+
48
+ def mappings
49
+ elements + attributes + [content_mapping].compact
50
+ end
51
+
52
+ def element(name)
53
+ elements.detect do |rule|
54
+ name == rule.to
55
+ end
56
+ end
57
+
58
+ def attribute(name)
59
+ attributes.detect do |rule|
60
+ name == rule.to
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,16 @@
1
+ # lib/lutaml/model/xml_mapping_rule.rb
2
+ require_relative "mapping_rule"
3
+
4
+ module Lutaml
5
+ module Model
6
+ class XmlMappingRule < MappingRule
7
+ attr_reader :namespace, :prefix
8
+
9
+ def initialize(name, to:, render_nil: false, with: {}, delegate: nil, namespace: nil, prefix: nil)
10
+ super(name, to: to, render_nil: render_nil, with: with, delegate: delegate)
11
+ @namespace = namespace
12
+ @prefix = prefix
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # lib/lutaml/model/yaml_adapter.rb
2
+ require "yaml"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module YamlAdapter
7
+ module Standard
8
+ def self.to_yaml(model, *args)
9
+ YAML.dump(model.hash_representation(:yaml), *args)
10
+ end
11
+
12
+ def self.from_yaml(yaml, klass)
13
+ data = parse(yaml)
14
+ mapped_attrs = klass.send(:apply_mappings, data, :yaml)
15
+ klass.new(mapped_attrs)
16
+ end
17
+
18
+ def self.parse(yaml)
19
+ YAML.safe_load(yaml, permitted_classes: [Date, Time, DateTime, Symbol, BigDecimal, Hash, Array])
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model/version"
4
+
5
+ # lib/lutaml/model.rb
6
+ require "nokogiri"
7
+ require "json"
8
+ require "yaml"
9
+ require_relative "model/type"
10
+ require_relative "model/serializable"
11
+ require_relative "model/json_adapter"
12
+ require_relative "model/yaml_adapter"
13
+ require_relative "model/xml_adapter"
14
+ require_relative "model/toml_adapter"
15
+
16
+ module Lutaml
17
+ module Model
18
+ class BaseModel < Serializable
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/lutaml/model/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "lutaml-model"
7
+ spec.version = Lutaml::Model::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "LutaML creating data models in Ruby"
12
+ spec.description = <<~DESCRIPTION
13
+ LutaML creating data models in Ruby
14
+ DESCRIPTION
15
+
16
+ spec.homepage = "https://github.com/lutaml/lutaml-model"
17
+ spec.license = "BSD-2-Clause"
18
+
19
+ spec.bindir = "bin"
20
+ spec.require_paths = ["lib"]
21
+ spec.files = `git ls-files`.split("\n")
22
+ spec.test_files = `git ls-files -- {spec}/*`.split("\n")
23
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ f.match(%r{^(test|spec|features)/})
30
+ end
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ # spec.add_runtime_dependency "expressir"
37
+ # spec.add_runtime_dependency "metanorma-cli"
38
+ # spec.add_runtime_dependency "shale"
39
+ # spec.add_runtime_dependency "thor", ">= 0.20"
40
+ spec.add_development_dependency "multi_json"
41
+ spec.add_development_dependency "tomlib"
42
+ spec.add_development_dependency "toml-rb"
43
+ spec.add_development_dependency "oga"
44
+ spec.add_development_dependency "ox"
45
+ spec.add_development_dependency "nokogiri"
46
+ spec.add_development_dependency "rubocop"
47
+ spec.add_development_dependency "rubocop-performance"
48
+ spec.add_development_dependency "rubocop-rails"
49
+ spec.add_development_dependency "equivalent-xml"
50
+ end
@@ -0,0 +1,6 @@
1
+ module Lutaml
2
+ module Model
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end