shale 0.1.0

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