shale 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.
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ox'
4
+
5
+ module Shale
6
+ module Adapter
7
+ # Ox adapter
8
+ #
9
+ # @api public
10
+ module Ox
11
+ # Parse XML into Ox document
12
+ #
13
+ # @param [String] xml XML document
14
+ #
15
+ # @return [::Ox::Document, ::Ox::Element]
16
+ #
17
+ # @api private
18
+ def self.load(xml)
19
+ Node.new(::Ox.parse(xml))
20
+ end
21
+
22
+ # Serialize Ox document into XML
23
+ #
24
+ # @param [::Ox::Document, ::Ox::Element] doc Ox document
25
+ #
26
+ # @return [String]
27
+ #
28
+ # @api private
29
+ def self.dump(doc)
30
+ ::Ox.dump(doc)
31
+ end
32
+
33
+ # Create Shale::Adapter::Ox::Document instance
34
+ #
35
+ # @api private
36
+ def self.create_document
37
+ Document.new
38
+ end
39
+
40
+ # Wrapper around Ox API
41
+ #
42
+ # @api private
43
+ class Document
44
+ # Return Ox document
45
+ #
46
+ # @return [::Ox::Document]
47
+ #
48
+ # @api private
49
+ attr_reader :doc
50
+
51
+ # Initialize object
52
+ #
53
+ # @api private
54
+ def initialize
55
+ @doc = ::Ox::Document.new
56
+ end
57
+
58
+ # Create Ox element
59
+ #
60
+ # @param [String] name Name of the XML element
61
+ #
62
+ # @return [::Ox::Element]
63
+ #
64
+ # @api private
65
+ def create_element(name)
66
+ ::Ox::Element.new(name)
67
+ end
68
+
69
+ # Add attribute to Ox element
70
+ #
71
+ # @param [::Ox::Element] element Ox element
72
+ # @param [String] name Name of the XML attribute
73
+ # @param [String] value Value of the XML attribute
74
+ #
75
+ # @api private
76
+ def add_attribute(element, name, value)
77
+ element[name] = value
78
+ end
79
+
80
+ # Add child element to Ox element
81
+ #
82
+ # @param [::Ox::Element] element Ox parent element
83
+ # @param [::Ox::Element] child Ox child element
84
+ #
85
+ # @api private
86
+ def add_element(element, child)
87
+ element << child
88
+ end
89
+
90
+ # Add text node to Ox element
91
+ #
92
+ # @param [::Ox::Element] element Ox element
93
+ # @param [String] text Text to add
94
+ #
95
+ # @api private
96
+ def add_text(element, text)
97
+ element << text
98
+ end
99
+ end
100
+
101
+ # Wrapper around Ox::Element API
102
+ #
103
+ # @api private
104
+ class Node
105
+ # Initialize object with Ox element
106
+ #
107
+ # @param [::Ox::Element] node Ox element
108
+ #
109
+ # @api private
110
+ def initialize(node)
111
+ @node = node
112
+ end
113
+
114
+ # Return fully qualified name of the node in the format of
115
+ # namespace:name when the node is namespaced or just name when it's not
116
+ #
117
+ # @return [String]
118
+ #
119
+ # @example without namespace
120
+ # node.name # => Bar
121
+ #
122
+ # @example with namespace
123
+ # node.name # => foo:Bar
124
+ #
125
+ # @api private
126
+ def name
127
+ @node.name
128
+ end
129
+
130
+ # Return all attributes associated with the node
131
+ #
132
+ # @return [Hash]
133
+ #
134
+ # @api private
135
+ def attributes
136
+ @node.attributes
137
+ end
138
+
139
+ # Return node's element children
140
+ #
141
+ # @return [Array<Shale::Adapter::Ox::Node>]
142
+ #
143
+ # @api private
144
+ def children
145
+ @node
146
+ .nodes
147
+ .filter { |e| e.is_a?(::Ox::Element) }
148
+ .map { |e| self.class.new(e) }
149
+ end
150
+
151
+ # Return first text child of a node
152
+ #
153
+ # @return [String]
154
+ #
155
+ # @api private
156
+ def text
157
+ @node.text
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rexml/document'
4
+
5
+ module Shale
6
+ module Adapter
7
+ # REXML adapter
8
+ #
9
+ # @api public
10
+ module REXML
11
+ # Parse XML into REXML document
12
+ #
13
+ # @param [String] xml XML document
14
+ #
15
+ # @return [::REXML::Document]
16
+ #
17
+ # @api private
18
+ def self.load(xml)
19
+ doc = ::REXML::Document.new(xml, ignore_whitespace_nodes: :all)
20
+ Node.new(doc.root)
21
+ end
22
+
23
+ # Serialize REXML document into XML
24
+ #
25
+ # @param [::REXML::Document] doc REXML document
26
+ #
27
+ # @return [String]
28
+ #
29
+ # @api private
30
+ def self.dump(doc)
31
+ doc.to_s
32
+ end
33
+
34
+ # Create Shale::Adapter::REXML::Document instance
35
+ #
36
+ # @api private
37
+ def self.create_document
38
+ Document.new
39
+ end
40
+
41
+ # Wrapper around REXML API
42
+ #
43
+ # @api private
44
+ class Document
45
+ # Return REXML document
46
+ #
47
+ # @return [::REXML::Document]
48
+ #
49
+ # @api private
50
+ attr_reader :doc
51
+
52
+ # Initialize object
53
+ #
54
+ # @api private
55
+ def initialize
56
+ @doc = ::REXML::Document.new
57
+ end
58
+
59
+ # Create REXML element
60
+ #
61
+ # @param [String] name Name of the XML element
62
+ #
63
+ # @return [::REXML::Element]
64
+ #
65
+ # @api private
66
+ def create_element(name)
67
+ ::REXML::Element.new(name)
68
+ end
69
+
70
+ # Add attribute to REXML element
71
+ #
72
+ # @param [::REXML::Element] element REXML element
73
+ # @param [String] name Name of the XML attribute
74
+ # @param [String] value Value of the XML attribute
75
+ #
76
+ # @api private
77
+ def add_attribute(element, name, value)
78
+ element.add_attribute(name, value)
79
+ end
80
+
81
+ # Add child element to REXML element
82
+ #
83
+ # @param [::REXML::Element] element REXML parent element
84
+ # @param [::REXML::Element] child REXML child element
85
+ #
86
+ # @api private
87
+ def add_element(element, child)
88
+ element.add_element(child)
89
+ end
90
+
91
+ # Add text node to REXML element
92
+ #
93
+ # @param [::REXML::Element] element REXML element
94
+ # @param [String] text Text to add
95
+ #
96
+ # @api private
97
+ def add_text(element, text)
98
+ element.add_text(text)
99
+ end
100
+ end
101
+
102
+ # Wrapper around REXML::Element API
103
+ #
104
+ # @api private
105
+ class Node
106
+ # Initialize object with REXML element
107
+ #
108
+ # @param [::REXML::Element] node REXML element
109
+ #
110
+ # @api private
111
+ def initialize(node)
112
+ @node = node
113
+ end
114
+
115
+ # Return fully qualified name of the node in the format of
116
+ # namespace:name when the node is namespaced or just name when it's not
117
+ #
118
+ # @return [String]
119
+ #
120
+ # @example without namespace
121
+ # node.name # => Bar
122
+ #
123
+ # @example with namespace
124
+ # node.name # => foo:Bar
125
+ #
126
+ # @api private
127
+ def name
128
+ @node.expanded_name
129
+ end
130
+
131
+ # Return all attributes associated with the node
132
+ #
133
+ # @return [Hash]
134
+ #
135
+ # @api private
136
+ def attributes
137
+ @node.attributes
138
+ end
139
+
140
+ # Return node's element children
141
+ #
142
+ # @return [Array<Shale::Adapter::REXML::Node>]
143
+ #
144
+ # @api private
145
+ def children
146
+ @node
147
+ .children
148
+ .filter { |e| e.node_type == :element }
149
+ .map { |e| self.class.new(e) }
150
+ end
151
+
152
+ # Return first text child of a node
153
+ #
154
+ # @return [String]
155
+ #
156
+ # @api private
157
+ def text
158
+ @node.text
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ # Class representing object's attribute
5
+ #
6
+ # @api private
7
+ class Attribute
8
+ # Return name
9
+ #
10
+ # @api private
11
+ attr_reader :name
12
+
13
+ # Return type
14
+ #
15
+ # @api private
16
+ attr_reader :type
17
+
18
+ # Return default
19
+ #
20
+ # @api private
21
+ attr_reader :default
22
+
23
+ # Initialize Attribute object
24
+ #
25
+ # @param [Symbol] name Name of the attribute
26
+ # @param [Shale::Type::Base] type Type of the attribute
27
+ # @param [Boolean] collection Is this attribute a collection
28
+ # @param [Proc] default Default value
29
+ #
30
+ # @api private
31
+ def initialize(name, type, collection, default)
32
+ @name = name
33
+ @type = type
34
+ @collection = collection
35
+ @default = collection ? -> { [] } : default
36
+ end
37
+
38
+ # Return wheter attribute is collection or not
39
+ #
40
+ # @return [Boolean]
41
+ #
42
+ # @api private
43
+ def collection?
44
+ @collection == true
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ # Error for assigning value to not existing attribute
5
+ #
6
+ # @api private
7
+ class UnknownAttributeError < NoMethodError
8
+ # Initialize error object
9
+ #
10
+ # @param [String] record
11
+ # @param [String] attribute
12
+ #
13
+ # @api private
14
+ def initialize(record, attribute)
15
+ super("unknown attribute '#{attribute}' for #{record}.")
16
+ end
17
+ end
18
+
19
+ # Error for trying to assign not callable object as an attribute's default
20
+ #
21
+ # @api private
22
+ class DefaultNotCallableError < StandardError
23
+ # Initialize error object
24
+ #
25
+ # @param [String] record
26
+ # @param [String] attribute
27
+ #
28
+ # @api private
29
+ def initialize(record, attribute)
30
+ super("'#{attribute}' default is not callable for #{record}.")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attribute'
4
+ require_relative 'error'
5
+ require_relative 'utils'
6
+ require_relative 'mapping/key_value'
7
+ require_relative 'mapping/xml'
8
+ require_relative 'type/complex'
9
+
10
+ module Shale
11
+ # Base class used for mapping
12
+ #
13
+ # @example
14
+ # class Address < Shale::Mapper
15
+ # attribute :city, Shale::Type::String
16
+ # attribute :street, Shale::Type::String
17
+ # attribute :state, Shale::Type::Integer
18
+ # attribute :zip, Shale::Type::String
19
+ # end
20
+ #
21
+ # class Person < Shale::Mapper
22
+ # attribute :first_name, Shale::Type::String
23
+ # attribute :last_name, Shale::Type::String
24
+ # attribute :age, Shale::Type::Integer
25
+ # attribute :address, Address
26
+ # end
27
+ #
28
+ # person = Person.from_json(%{
29
+ # {
30
+ # "first_name": "John",
31
+ # "last_name": "Doe",
32
+ # "age": 55,
33
+ # "address": {
34
+ # "city": "London",
35
+ # "street": "Oxford Street",
36
+ # "state": "London",
37
+ # "zip": "E1 6AN"
38
+ # }
39
+ # }
40
+ # })
41
+ #
42
+ # person.to_json
43
+ #
44
+ # @api public
45
+ class Mapper < Type::Complex
46
+ @attributes = {}
47
+ @hash_mapping = Mapping::KeyValue.new
48
+ @json_mapping = Mapping::KeyValue.new
49
+ @yaml_mapping = Mapping::KeyValue.new
50
+ @xml_mapping = Mapping::Xml.new
51
+
52
+ class << self
53
+ # Return attributes Hash
54
+ #
55
+ # @return [Hash<Symbol, Shale::Attribute>]
56
+ #
57
+ # @api public
58
+ attr_reader :attributes
59
+
60
+ # Return Hash mapping object
61
+ #
62
+ # @return [Shale::Mapping::KeyValue]
63
+ #
64
+ # @api public
65
+ attr_reader :hash_mapping
66
+
67
+ # Return JSON mapping object
68
+ #
69
+ # @return [Shale::Mapping::KeyValue]
70
+ #
71
+ # @api public
72
+ attr_reader :json_mapping
73
+
74
+ # Return YAML mapping object
75
+ #
76
+ # @return [Shale::Mapping::KeyValue]
77
+ #
78
+ # @api public
79
+ attr_reader :yaml_mapping
80
+
81
+ # Return XML mapping object
82
+ #
83
+ # @return [Shale::Mapping::XML]
84
+ #
85
+ # @api public
86
+ attr_reader :xml_mapping
87
+
88
+ # @api private
89
+ def inherited(subclass)
90
+ super
91
+ subclass.instance_variable_set('@attributes', @attributes.dup)
92
+
93
+ subclass.instance_variable_set('@__hash_mapping_init', @hash_mapping.dup)
94
+ subclass.instance_variable_set('@__json_mapping_init', @json_mapping.dup)
95
+ subclass.instance_variable_set('@__yaml_mapping_init', @yaml_mapping.dup)
96
+ subclass.instance_variable_set('@__xml_mapping_init', @xml_mapping.dup)
97
+
98
+ subclass.instance_variable_set('@hash_mapping', @hash_mapping.dup)
99
+ subclass.instance_variable_set('@json_mapping', @json_mapping.dup)
100
+ subclass.instance_variable_set('@yaml_mapping', @yaml_mapping.dup)
101
+
102
+ xml_mapping = @xml_mapping.dup
103
+ xml_mapping.root(Utils.underscore(subclass.name || ''))
104
+
105
+ subclass.instance_variable_set('@xml_mapping', xml_mapping.dup)
106
+ end
107
+
108
+ # Define attribute on class
109
+ #
110
+ # @param [Symbol] name Name of the attribute
111
+ # @param [Shale::Type::Base] type Type of the attribute
112
+ # @param [Boolean] collection Is the attribute a collection
113
+ # @param [Proc] default Default value for the attribute
114
+ #
115
+ # @raise [DefaultNotCallableError] when attribute's default is not callable
116
+ #
117
+ # @example
118
+ # calss Person < Shale::Mapper
119
+ # attribute :first_name, Shale::Type::String
120
+ # attribute :last_name, Shale::Type::String
121
+ # attribute :age, Shale::Type::Integer, default: -> { 1 }
122
+ # attribute :hobbies, Shale::Type::String, collection: true
123
+ # end
124
+ #
125
+ # person = Person.new
126
+ #
127
+ # person.first_name # => nil
128
+ # person.first_name = 'John'
129
+ # person.first_name # => 'John'
130
+ #
131
+ # person.age # => 1
132
+ #
133
+ # person.hobbies << 'Dancing'
134
+ # person.hobbies # => ['Dancing']
135
+ #
136
+ # @api public
137
+ def attribute(name, type, collection: false, default: nil)
138
+ name = name.to_sym
139
+
140
+ unless default.nil? || default.respond_to?(:call)
141
+ raise DefaultNotCallableError.new(to_s, name)
142
+ end
143
+
144
+ @attributes[name] = Attribute.new(name, type, collection, default)
145
+
146
+ @hash_mapping.map(name.to_s, to: name)
147
+ @json_mapping.map(name.to_s, to: name)
148
+ @yaml_mapping.map(name.to_s, to: name)
149
+ @xml_mapping.map_element(name.to_s, to: name)
150
+
151
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
152
+ attr_reader name
153
+
154
+ def #{name}=(val)
155
+ @#{name} = #{collection} ? val : #{type}.cast(val)
156
+ end
157
+ RUBY
158
+ end
159
+
160
+ # Define Hash mapping
161
+ #
162
+ # @param [Proc] block
163
+ #
164
+ # @example
165
+ # calss Person < Shale::Mapper
166
+ # attribute :first_name, Shale::Type::String
167
+ # attribute :last_name, Shale::Type::String
168
+ # attribute :age, Shale::Type::Integer
169
+ #
170
+ # yaml do
171
+ # map 'firatName', to: :first_name
172
+ # map 'lastName', to: :last_name
173
+ # map 'age', to: :age
174
+ # end
175
+ # end
176
+ #
177
+ # @api public
178
+ def hash(&block)
179
+ @hash_mapping = @__hash_mapping_init.dup
180
+ @hash_mapping.instance_eval(&block)
181
+ end
182
+
183
+ # Define JSON mapping
184
+ #
185
+ # @param [Proc] block
186
+ #
187
+ # @example
188
+ # calss Person < Shale::Mapper
189
+ # attribute :first_name, Shale::Type::String
190
+ # attribute :last_name, Shale::Type::String
191
+ # attribute :age, Shale::Type::Integer
192
+ #
193
+ # yaml do
194
+ # map 'firatName', to: :first_name
195
+ # map 'lastName', to: :last_name
196
+ # map 'age', to: :age
197
+ # end
198
+ # end
199
+ #
200
+ # @api public
201
+ def json(&block)
202
+ @json_mapping = @__json_mapping_init.dup
203
+ @json_mapping.instance_eval(&block)
204
+ end
205
+
206
+ # Define YAML mapping
207
+ #
208
+ # @param [Proc] block
209
+ #
210
+ # @example
211
+ # calss Person < Shale::Mapper
212
+ # attribute :first_name, Shale::Type::String
213
+ # attribute :last_name, Shale::Type::String
214
+ # attribute :age, Shale::Type::Integer
215
+ #
216
+ # yaml do
217
+ # map 'firat_name', to: :first_name
218
+ # map 'last_name', to: :last_name
219
+ # map 'age', to: :age
220
+ # end
221
+ # end
222
+ #
223
+ # @api public
224
+ def yaml(&block)
225
+ @yaml_mapping = @__yaml_mapping_init.dup
226
+ @yaml_mapping.instance_eval(&block)
227
+ end
228
+
229
+ # Define XML mapping
230
+ #
231
+ # @param [Proc] block
232
+ #
233
+ # @example
234
+ # calss Person < Shale::Mapper
235
+ # attribute :first_name, Shale::Type::String
236
+ # attribute :last_name, Shale::Type::String
237
+ # attribute :age, Shale::Type::Integer
238
+ #
239
+ # xml do
240
+ # root 'Person'
241
+ # map_content to: :first_name
242
+ # map_element 'LastName', to: :last_name
243
+ # map_attribute 'age', to: :age
244
+ # end
245
+ # end
246
+ #
247
+ # @api public
248
+ def xml(&block)
249
+ @xml_mapping = @__xml_mapping_init.dup
250
+ @xml_mapping.instance_eval(&block)
251
+ end
252
+ end
253
+
254
+ # Initialize instance with properties
255
+ #
256
+ # @param [Hash] props Properties
257
+ #
258
+ # @raise [UnknownAttributeError] when attribute is not defined on the class
259
+ #
260
+ # @example
261
+ # Person.new(
262
+ # first_name: 'John',
263
+ # last_name: 'Doe',
264
+ # address: Address.new(city: 'London')
265
+ # )
266
+ # # => #<Person:0x00007f82768a2370
267
+ # @first_name="John",
268
+ # @last_name="Doe"
269
+ # @address=#<Address:0x00007fe9cf0f57d8 @city="London">>
270
+ #
271
+ # @api public
272
+ def initialize(**props)
273
+ super()
274
+
275
+ props.each_key do |name|
276
+ unless self.class.attributes.keys.include?(name)
277
+ raise UnknownAttributeError.new(self.class.to_s, name.to_s)
278
+ end
279
+ end
280
+
281
+ self.class.attributes.each do |name, attribute|
282
+ if props.key?(name)
283
+ value = props[name]
284
+ elsif attribute.default
285
+ value = attribute.default.call
286
+ end
287
+
288
+ public_send("#{name}=", value)
289
+ end
290
+ end
291
+ end
292
+ end