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,20 @@
1
+ # lib/lutaml/model/json_adapter/standard.rb
2
+ require "json"
3
+ require_relative "../json_adapter"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module JsonAdapter
8
+ class StandardDocument < Document
9
+ def self.parse(json)
10
+ attributes = JSON.parse(json, create_additions: false)
11
+ new(attributes)
12
+ end
13
+
14
+ def to_json(*args)
15
+ JSON.generate(@attributes, *args)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ # lib/lutaml/model/json_adapter.rb
2
+ require "json"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module JsonAdapter
7
+ class JsonObject
8
+ attr_reader :attributes
9
+
10
+ def initialize(attributes = {})
11
+ @attributes = attributes
12
+ end
13
+
14
+ def [](key)
15
+ @attributes[key]
16
+ end
17
+
18
+ def []=(key, value)
19
+ @attributes[key] = value
20
+ end
21
+
22
+ def to_h
23
+ @attributes
24
+ end
25
+ end
26
+
27
+ class Document < JsonObject
28
+ def self.parse(json)
29
+ raise NotImplementedError, "Subclasses must implement `parse`."
30
+ end
31
+
32
+ def to_json(*args)
33
+ raise NotImplementedError, "Subclasses must implement `to_json`."
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,20 @@
1
+ # lib/lutaml/model/key_value_mapping.rb
2
+ require_relative "key_value_mapping_rule"
3
+
4
+ module Lutaml
5
+ module Model
6
+ class KeyValueMapping
7
+ attr_reader :mappings
8
+
9
+ def initialize
10
+ @mappings = []
11
+ end
12
+
13
+ def map(name, to:, render_nil: false, with: {}, delegate: nil)
14
+ @mappings << KeyValueMappingRule.new(name, to: to, render_nil: render_nil, with: with, delegate: delegate)
15
+ end
16
+
17
+ alias map_element map
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ # lib/lutaml/model/key_value_mapping_rule.rb
2
+ require_relative "mapping_rule"
3
+
4
+ module Lutaml
5
+ module Model
6
+ class KeyValueMappingRule < MappingRule
7
+ # No additional attributes or methods required for now
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,34 @@
1
+ # lib/lutaml/model/mapping_rule.rb
2
+ module Lutaml
3
+ module Model
4
+ class MappingRule
5
+ attr_reader :name, :to, :render_nil, :custom_methods, :delegate
6
+
7
+ def initialize(name, to:, render_nil: false, with: {}, delegate: nil)
8
+ @name = name
9
+ @to = to
10
+ @render_nil = render_nil
11
+ @custom_methods = with
12
+ @delegate = delegate
13
+ end
14
+
15
+ alias from name
16
+
17
+ def serialize(model, value)
18
+ if custom_methods[:to]
19
+ model.send(custom_methods[:to], model, value)
20
+ else
21
+ value
22
+ end
23
+ end
24
+
25
+ def deserialize(model, doc)
26
+ if custom_methods[:from]
27
+ model.send(custom_methods[:from], model, doc)
28
+ else
29
+ doc[name.to_s]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,63 @@
1
+ # lib/lutaml/model/schema/json_schema.rb
2
+ require "json"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module Schema
7
+ class JsonSchema
8
+ def self.generate(klass, options = {})
9
+ schema = {
10
+ "$schema" => "https://json-schema.org/draft/2020-12/schema",
11
+ "$id" => options[:id],
12
+ "description" => options[:description],
13
+ "$ref" => "#/$defs/#{klass.name}",
14
+ "$defs" => generate_definitions(klass),
15
+ }.compact
16
+
17
+ options[:pretty] ? JSON.pretty_generate(schema) : schema.to_json
18
+ end
19
+
20
+ private
21
+
22
+ def self.generate_definitions(klass)
23
+ {
24
+ klass.name => {
25
+ "type" => "object",
26
+ "properties" => generate_properties(klass),
27
+ "required" => klass.attributes.keys,
28
+ },
29
+ }
30
+ end
31
+
32
+ def self.generate_properties(klass)
33
+ klass.attributes.each_with_object({}) do |(name, attr), properties|
34
+ properties[name] = generate_property_schema(attr)
35
+ end
36
+ end
37
+
38
+ def self.generate_property_schema(attr)
39
+ { "type" => get_json_type(attr.type) }
40
+ end
41
+
42
+ def self.get_json_type(type)
43
+ case type
44
+ when Lutaml::Model::Type::String
45
+ "string"
46
+ when Lutaml::Model::Type::Integer
47
+ "integer"
48
+ when Lutaml::Model::Type::Boolean
49
+ "boolean"
50
+ when Lutaml::Model::Type::Float
51
+ "number"
52
+ when Lutaml::Model::Type::Array
53
+ "array"
54
+ when Lutaml::Model::Type::Hash
55
+ "object"
56
+ else
57
+ "string" # Default to string for unknown types
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,50 @@
1
+ # lib/lutaml/model/schema/relaxng_schema.rb
2
+ require "nokogiri"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module Schema
7
+ class RelaxngSchema
8
+ def self.generate(klass, options = {})
9
+ schema = Nokogiri::XML::Builder.new do |xml|
10
+ xml.element(name: klass.name) do
11
+ xml.complexType do
12
+ xml.sequence do
13
+ generate_elements(klass, xml)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ schema.to_xml
19
+ end
20
+
21
+ private
22
+
23
+ def self.generate_elements(klass, xml)
24
+ klass.attributes.each do |name, attr|
25
+ xml.element(name: name, type: get_relaxng_type(attr.type))
26
+ end
27
+ end
28
+
29
+ def self.get_relaxng_type(type)
30
+ case type
31
+ when Lutaml::Model::Type::String
32
+ "string"
33
+ when Lutaml::Model::Type::Integer
34
+ "integer"
35
+ when Lutaml::Model::Type::Boolean
36
+ "boolean"
37
+ when Lutaml::Model::Type::Float
38
+ "float"
39
+ when Lutaml::Model::Type::Array
40
+ "array"
41
+ when Lutaml::Model::Type::Hash
42
+ "object"
43
+ else
44
+ "string" # Default to string for unknown types
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,54 @@
1
+ # lib/lutaml/model/schema/xsd_schema.rb
2
+ require "nokogiri"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module Schema
7
+ class XsdSchema
8
+ def self.generate(klass, options = {})
9
+ schema = Nokogiri::XML::Builder.new do |xml|
10
+ xml.schema(xmlns: "http://www.w3.org/2001/XMLSchema") do
11
+ xml.element(name: klass.name) do
12
+ xml.complexType do
13
+ xml.sequence do
14
+ generate_elements(klass, xml)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ schema.to_xml
21
+ end
22
+
23
+ private
24
+
25
+ def self.generate_elements(klass, xml)
26
+ klass.attributes.each do |name, attr|
27
+ xml.element(name: name, type: get_xsd_type(attr.type))
28
+ end
29
+ end
30
+
31
+ def self.get_xsd_type(type)
32
+ case type
33
+ when Lutaml::Model::Type::String
34
+ "xs:string"
35
+ when Lutaml::Model::Type::Integer
36
+ "xs:integer"
37
+ when Lutaml::Model::Type::Boolean
38
+ "xs:boolean"
39
+ when Lutaml::Model::Type::Float
40
+ "xs:float"
41
+ when Lutaml::Model::Type::Decimal
42
+ "xs:decimal"
43
+ when Lutaml::Model::Type::Array
44
+ "xs:array"
45
+ when Lutaml::Model::Type::Hash
46
+ "xs:object"
47
+ else
48
+ "xs:string" # Default to string for unknown types
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ # lib/lutaml/model/schema/yaml_schema.rb
2
+ require "yaml"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module Schema
7
+ class YamlSchema
8
+ def self.generate(klass, options = {})
9
+ schema = {
10
+ "type" => "map",
11
+ "mapping" => generate_mapping(klass),
12
+ }
13
+ YAML.dump(schema)
14
+ end
15
+
16
+ private
17
+
18
+ def self.generate_mapping(klass)
19
+ klass.attributes.each_with_object({}) do |(name, attr), mapping|
20
+ mapping[name.to_s] = { "type" => get_yaml_type(attr.type) }
21
+ end
22
+ end
23
+
24
+ def self.get_yaml_type(type)
25
+ case type
26
+ when Lutaml::Model::Type::String
27
+ "str"
28
+ when Lutaml::Model::Type::Integer
29
+ "int"
30
+ when Lutaml::Model::Type::Boolean
31
+ "bool"
32
+ when Lutaml::Model::Type::Float
33
+ "float"
34
+ when Lutaml::Model::Type::Decimal
35
+ "float" # YAML does not have a separate decimal type, so we use float
36
+ when Lutaml::Model::Type::Array
37
+ "seq"
38
+ when Lutaml::Model::Type::Hash
39
+ "map"
40
+ else
41
+ "str" # Default to string for unknown types
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ # lib/lutaml/model/schema.rb
2
+ require_relative "schema/json_schema"
3
+ require_relative "schema/xsd_schema"
4
+ require_relative "schema/relaxng_schema"
5
+ require_relative "schema/yaml_schema"
6
+
7
+ module Lutaml
8
+ module Model
9
+ module Schema
10
+ def self.to_json(klass, options = {})
11
+ JsonSchema.generate(klass, options)
12
+ end
13
+
14
+ def self.to_xsd(klass, options = {})
15
+ XsdSchema.generate(klass, options)
16
+ end
17
+
18
+ def self.to_relaxng(klass, options = {})
19
+ RelaxngSchema.generate(klass, options)
20
+ end
21
+
22
+ def self.to_yaml(klass, options = {})
23
+ YamlSchema.generate(klass, options)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ # lib/lutaml/model/serializable.rb
2
+ require_relative "serialize"
3
+
4
+ module Lutaml
5
+ module Model
6
+ class Serializable
7
+ include Serialize
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,179 @@
1
+ # lib/lutaml/model/serialize.rb
2
+ require_relative "json_adapter/standard"
3
+ require_relative "json_adapter/multi_json"
4
+ require_relative "yaml_adapter"
5
+ require_relative "xml_adapter"
6
+ require_relative "toml_adapter/toml_rb_adapter"
7
+ require_relative "toml_adapter/tomlib_adapter"
8
+ require_relative "config"
9
+ require_relative "type"
10
+ require_relative "attribute"
11
+ require_relative "mapping_rule"
12
+ require_relative "xml_mapping"
13
+ require_relative "key_value_mapping"
14
+ require_relative "json_adapter"
15
+
16
+ module Lutaml
17
+ module Model
18
+ module Serialize
19
+ FORMATS = %i[xml json yaml toml].freeze
20
+
21
+ def self.included(base)
22
+ base.extend(ClassMethods)
23
+ end
24
+
25
+ module ClassMethods
26
+ attr_accessor :attributes, :mappings
27
+
28
+ def attribute(name, type, options = {})
29
+ self.attributes ||= {}
30
+ attr = Attribute.new(name, type, options)
31
+ attributes[name] = attr
32
+
33
+ define_method(name) do
34
+ instance_variable_get("@#{name}")
35
+ end
36
+
37
+ define_method("#{name}=") do |value|
38
+ instance_variable_set("@#{name}", value)
39
+ end
40
+ end
41
+
42
+ FORMATS.each do |format|
43
+ define_method(format) do |&block|
44
+ self.mappings ||= {}
45
+ klass = format == :xml ? XmlMapping : KeyValueMapping
46
+ self.mappings[format] = klass.new
47
+ self.mappings[format].instance_eval(&block)
48
+ end
49
+
50
+ define_method("from_#{format}") do |data|
51
+ adapter = Lutaml::Model::Config.send("#{format}_adapter")
52
+ doc = adapter.parse(data)
53
+ mapped_attrs = apply_mappings(doc.to_h, format)
54
+ apply_content_mapping(doc, mapped_attrs) if format == :xml
55
+ new(mapped_attrs)
56
+ end
57
+ end
58
+
59
+ def mappings_for(format)
60
+ self.mappings[format] || default_mappings(format)
61
+ end
62
+
63
+ def default_mappings(format)
64
+ klass = format == :xml ? XmlMapping : KeyValueMapping
65
+ klass.new.tap do |mapping|
66
+ attributes&.each do |name, attr|
67
+ mapping.map_element(name.to_s, to: name, render_nil: attr.render_nil?)
68
+ end
69
+ end
70
+ end
71
+
72
+ def apply_mappings(doc, format)
73
+ mappings = mappings_for(format).mappings
74
+ mappings.each_with_object({}) do |rule, hash|
75
+ attr = attributes[rule.to]
76
+ raise "Attribute '#{rule.to}' not found in #{self}" unless attr
77
+
78
+ value = doc[rule.name]
79
+ if attr.collection?
80
+ value = (value || []).map { |v| attr.type <= Serialize ? attr.type.new(v) : v }
81
+ elsif value.is_a?(Hash) && attr.type <= Serialize
82
+ value = attr.type.new(value)
83
+ end
84
+ hash[rule.to] = value
85
+ end
86
+ end
87
+
88
+ def apply_content_mapping(doc, mapped_attrs)
89
+ content_mapping = mappings_for(:xml).content_mapping
90
+ return unless content_mapping
91
+
92
+ content = doc.root.children.select(&:text?).map(&:text)
93
+ mapped_attrs[content_mapping.to] = content
94
+ end
95
+ end
96
+
97
+ def initialize(attrs = {})
98
+ return self unless self.class.attributes
99
+
100
+ self.class.attributes.each do |name, attr|
101
+ value = attrs.key?(name) ? attrs[name] : attr.default
102
+ value = if attr.collection?
103
+ (value || []).map { |v| Lutaml::Model::Type.cast(v, attr.type) }
104
+ else
105
+ Lutaml::Model::Type.cast(value, attr.type)
106
+ end
107
+ send("#{name}=", ensure_utf8(value))
108
+ end
109
+ end
110
+
111
+ # TODO: Make this work
112
+ # FORMATS.each do |format|
113
+ # define_method("to_#{format}") do |options = {}|
114
+ # adapter = Lutaml::Model::Config.send("#{format}_adapter")
115
+ # representation = format == :yaml ? self : hash_representation(format, options)
116
+ # adapter.new(representation).send("to_#{format}", options)
117
+ # end
118
+ # end
119
+
120
+ def to_xml(options = {})
121
+ adapter = Lutaml::Model::Config.xml_adapter
122
+ adapter.new(self).to_xml(options)
123
+ end
124
+
125
+ def to_json(options = {})
126
+ adapter = Lutaml::Model::Config.json_adapter
127
+ adapter.new(hash_representation(:json, options)).to_json(options)
128
+ end
129
+
130
+ def to_yaml(options = {})
131
+ adapter = Lutaml::Model::Config.yaml_adapter
132
+ adapter.to_yaml(self, options)
133
+ end
134
+
135
+ def to_toml(options = {})
136
+ adapter = Lutaml::Model::Config.toml_adapter
137
+ adapter.new(hash_representation(:toml, options)).to_toml
138
+ end
139
+
140
+ # TODO: END Make this work
141
+
142
+ def hash_representation(format, options = {})
143
+ only = options[:only]
144
+ except = options[:except]
145
+ mappings = self.class.mappings_for(format).mappings
146
+
147
+ mappings.each_with_object({}) do |rule, hash|
148
+ name = rule.to
149
+ next if except&.include?(name) || (only && !only.include?(name))
150
+
151
+ value = send(name)
152
+ next if value.nil? && !rule.render_nil
153
+
154
+ hash[rule.from] = case value
155
+ when Array
156
+ value.map { |v| v.is_a?(Serialize) ? v.hash_representation(format, options) : v }
157
+ else
158
+ value.is_a?(Serialize) ? value.hash_representation(format, options) : value
159
+ end
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def ensure_utf8(value)
166
+ case value
167
+ when String
168
+ value.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
169
+ when Array
170
+ value.map { |v| ensure_utf8(v) }
171
+ when Hash
172
+ value.transform_keys { |k| ensure_utf8(k) }.transform_values { |v| ensure_utf8(v) }
173
+ else
174
+ value
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,20 @@
1
+ # lib/lutaml/model/toml_adapter/toml_rb_adapter.rb
2
+ require "toml-rb"
3
+ require_relative "../toml_adapter"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module TomlAdapter
8
+ class TomlRbDocument < Document
9
+ def self.parse(toml)
10
+ data = TomlRB.parse(toml)
11
+ new(data)
12
+ end
13
+
14
+ def to_toml(*args)
15
+ TomlRB.dump(to_h, *args)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # lib/lutaml/model/toml_adapter/tomlib_adapter.rb
2
+ require "tomlib"
3
+ require_relative "../toml_adapter"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module TomlAdapter
8
+ class TomlibDocument < Document
9
+ def self.parse(toml)
10
+ data = Tomlib::Parser.new(toml).parsed
11
+ new(data)
12
+ end
13
+
14
+ def to_toml(*args)
15
+ Tomlib::Generator.new(to_h).toml_str
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ # lib/lutaml/model/toml_adapter.rb
2
+
3
+ module Lutaml
4
+ module Model
5
+ module TomlAdapter
6
+ class TomlObject
7
+ attr_reader :attributes
8
+
9
+ def initialize(attributes = {})
10
+ @attributes = attributes
11
+ end
12
+
13
+ def [](key)
14
+ @attributes[key]
15
+ end
16
+
17
+ def []=(key, value)
18
+ @attributes[key] = value
19
+ end
20
+
21
+ def to_h
22
+ @attributes
23
+ end
24
+ end
25
+
26
+ class Document < TomlObject
27
+ def self.parse(toml)
28
+ raise NotImplementedError, "Subclasses must implement `parse`."
29
+ end
30
+
31
+ def to_toml(*args)
32
+ raise NotImplementedError, "Subclasses must implement `to_toml`."
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # lib/lutaml/model/type/time_without_date.rb
2
+ module Lutaml
3
+ module Model
4
+ module Type
5
+ class TimeWithoutDate
6
+ def self.cast(value)
7
+ parsed_time = ::Time.parse(value.to_s)
8
+ parsed_time.strftime("%H:%M:%S")
9
+ end
10
+
11
+ def self.serialize(value)
12
+ value.strftime("%H:%M:%S")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end