lutaml-model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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