shale 0.2.1 → 0.3.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +274 -7
  4. data/exe/shaleb +75 -0
  5. data/lib/shale/adapter/json.rb +7 -2
  6. data/lib/shale/adapter/nokogiri.rb +48 -12
  7. data/lib/shale/adapter/ox.rb +28 -4
  8. data/lib/shale/adapter/rexml.rb +56 -13
  9. data/lib/shale/attribute.rb +1 -1
  10. data/lib/shale/error.rb +6 -0
  11. data/lib/shale/mapper.rb +11 -11
  12. data/lib/shale/mapping/descriptor/dict.rb +57 -0
  13. data/lib/shale/mapping/descriptor/xml.rb +43 -0
  14. data/lib/shale/mapping/descriptor/xml_namespace.rb +37 -0
  15. data/lib/shale/mapping/{key_value.rb → dict.rb} +8 -6
  16. data/lib/shale/mapping/validator.rb +51 -0
  17. data/lib/shale/mapping/xml.rb +86 -15
  18. data/lib/shale/schema/json/base.rb +41 -0
  19. data/lib/shale/schema/json/boolean.rb +23 -0
  20. data/lib/shale/schema/json/collection.rb +39 -0
  21. data/lib/shale/schema/json/date.rb +23 -0
  22. data/lib/shale/schema/json/float.rb +23 -0
  23. data/lib/shale/schema/json/integer.rb +23 -0
  24. data/lib/shale/schema/json/object.rb +37 -0
  25. data/lib/shale/schema/json/ref.rb +28 -0
  26. data/lib/shale/schema/json/schema.rb +57 -0
  27. data/lib/shale/schema/json/string.rb +23 -0
  28. data/lib/shale/schema/json/time.rb +23 -0
  29. data/lib/shale/schema/json.rb +165 -0
  30. data/lib/shale/schema/xml/attribute.rb +41 -0
  31. data/lib/shale/schema/xml/complex_type.rb +67 -0
  32. data/lib/shale/schema/xml/element.rb +55 -0
  33. data/lib/shale/schema/xml/import.rb +46 -0
  34. data/lib/shale/schema/xml/ref_attribute.rb +37 -0
  35. data/lib/shale/schema/xml/ref_element.rb +39 -0
  36. data/lib/shale/schema/xml/schema.rb +121 -0
  37. data/lib/shale/schema/xml/typed_attribute.rb +46 -0
  38. data/lib/shale/schema/xml/typed_element.rb +46 -0
  39. data/lib/shale/schema/xml.rb +316 -0
  40. data/lib/shale/schema.rb +47 -0
  41. data/lib/shale/type/boolean.rb +2 -2
  42. data/lib/shale/type/composite.rb +67 -56
  43. data/lib/shale/type/date.rb +35 -2
  44. data/lib/shale/type/float.rb +2 -2
  45. data/lib/shale/type/integer.rb +2 -2
  46. data/lib/shale/type/string.rb +2 -2
  47. data/lib/shale/type/time.rb +35 -2
  48. data/lib/shale/type/{base.rb → value.rb} +18 -7
  49. data/lib/shale/utils.rb +18 -2
  50. data/lib/shale/version.rb +1 -1
  51. data/lib/shale.rb +10 -10
  52. data/shale.gemspec +6 -2
  53. metadata +41 -13
  54. data/lib/shale/mapping/base.rb +0 -32
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module Shale
6
+ module Schema
7
+ class XML
8
+ # Class representing XML Schema <element ref=""> element
9
+ # with a reference
10
+ #
11
+ # @api private
12
+ class RefElement < Element
13
+ # Initialize RefElement object
14
+ #
15
+ # @param [String] ref
16
+ # @param [String, nil] default
17
+ # @param [true, false] collection
18
+ # @param [true, false] required
19
+ #
20
+ # @api private
21
+ def initialize(ref:, default: nil, collection: false, required: false)
22
+ super(default, collection, required)
23
+ @ref = ref
24
+ end
25
+
26
+ private
27
+
28
+ # Return attributes as Ruby Hash
29
+ #
30
+ # @return [Hash]
31
+ #
32
+ # @api private
33
+ def attributes
34
+ { 'ref' => @ref }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../shale'
4
+ require_relative 'complex_type'
5
+
6
+ module Shale
7
+ module Schema
8
+ class XML
9
+ class Schema
10
+ # XML Schema namespace
11
+ # @api private
12
+ NAMESPACE_NAME = 'http://www.w3.org/2001/XMLSchema'
13
+
14
+ # Return name
15
+ #
16
+ # @return [String]
17
+ #
18
+ # @api private
19
+ attr_reader :name
20
+
21
+ # Initialize Schema object
22
+ #
23
+ # @param [String] name
24
+ # @param [String, nil] target_namespace
25
+ #
26
+ # @api private
27
+ def initialize(name, target_namespace)
28
+ @name = name
29
+ @target_namespace = target_namespace
30
+ @namespaces = []
31
+ @children = []
32
+ end
33
+
34
+ # Add namespace to XML Schema
35
+ #
36
+ # @param [String] prefix
37
+ # @param [String] name
38
+ #
39
+ # @api private
40
+ def add_namespace(prefix, name)
41
+ @namespaces << { prefix: prefix, name: name }
42
+ end
43
+
44
+ # Add child element to XML Schema
45
+ #
46
+ # @param [Shale::Schema::XML::Import,
47
+ # Shale::Schema::XML::Element,
48
+ # Shale::Schema::XML::Attribute,
49
+ # Shale::Schema::XML::ComplesType] child
50
+ #
51
+ # @api private
52
+ def add_child(child)
53
+ @children << child
54
+ end
55
+
56
+ # Append element to the XML document
57
+ #
58
+ # @param [Shale::Adapter::<XML adapter>::Document] doc
59
+ #
60
+ # @return [Shale::Adapter::REXML::Document,
61
+ # Shale::Adapter::Nokogiri::Document,
62
+ # Shale::Adapter::Ox::Document]
63
+ #
64
+ # @api private
65
+ def as_xml
66
+ doc = Shale.xml_adapter.create_document
67
+ doc.add_namespace('xs', NAMESPACE_NAME)
68
+
69
+ @namespaces.uniq.each do |namespace|
70
+ doc.add_namespace(namespace[:prefix], namespace[:name])
71
+ end
72
+
73
+ schema = doc.create_element('xs:schema')
74
+ doc.add_element(doc.doc, schema)
75
+
76
+ doc.add_attribute(schema, 'elementFormDefault', 'qualified')
77
+ doc.add_attribute(schema, 'attributeFormDefault', 'qualified')
78
+
79
+ unless @target_namespace.nil?
80
+ doc.add_attribute(schema, 'targetNamespace', @target_namespace)
81
+ end
82
+
83
+ imports = @children
84
+ .select { |e| e.is_a?(Import) }
85
+ .uniq(&:namespace)
86
+ .sort { |a, b| (a.namespace || '') <=> (b.namespace || '') }
87
+
88
+ elements = @children
89
+ .select { |e| e.is_a?(Element) }
90
+ .sort { |a, b| a.name <=> b.name }
91
+
92
+ attributes = @children
93
+ .select { |e| e.is_a?(Attribute) }
94
+ .sort { |a, b| a.name <=> b.name }
95
+
96
+ complex_types = @children
97
+ .select { |e| e.is_a?(ComplexType) }
98
+ .sort { |a, b| a.name <=> b.name }
99
+
100
+ imports.each do |import|
101
+ doc.add_element(schema, import.as_xml(doc))
102
+ end
103
+
104
+ elements.each do |element|
105
+ doc.add_element(schema, element.as_xml(doc))
106
+ end
107
+
108
+ attributes.each do |attribute|
109
+ doc.add_element(schema, attribute.as_xml(doc))
110
+ end
111
+
112
+ complex_types.each do |complex_type|
113
+ doc.add_element(schema, complex_type.as_xml(doc))
114
+ end
115
+
116
+ doc.doc
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attribute'
4
+
5
+ module Shale
6
+ module Schema
7
+ class XML
8
+ # Class representing XML Schema <attribute name="" type=""> element
9
+ # with a name and a type
10
+ #
11
+ # @api private
12
+ class TypedAttribute < Attribute
13
+ # Return name
14
+ #
15
+ # @return [String]
16
+ #
17
+ # @api private
18
+ attr_reader :name
19
+
20
+ # Initialize TypedAttribute object
21
+ #
22
+ # @param [String] name
23
+ # @param [String] type
24
+ # @param [String, nil] default
25
+ #
26
+ # @api private
27
+ def initialize(name:, type:, default: nil)
28
+ super(default)
29
+ @name = name
30
+ @type = type
31
+ end
32
+
33
+ private
34
+
35
+ # Return attributes as Ruby Hash
36
+ #
37
+ # @return [Hash]
38
+ #
39
+ # @api private
40
+ def attributes
41
+ { 'name' => @name, 'type' => @type }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module Shale
6
+ module Schema
7
+ class XML
8
+ # Class representing XML Schema <element mame="" type=""> element
9
+ # with a name and a type
10
+ #
11
+ # @api private
12
+ class TypedElement < Element
13
+ # Return name
14
+ #
15
+ # @return [String]
16
+ #
17
+ # @api private
18
+ attr_reader :name
19
+
20
+ # Initialize TypedElement object
21
+ #
22
+ # @param [String] name
23
+ # @param [String] type
24
+ # @param [String, nil] default
25
+ # @param [true, false] collection
26
+ # @param [true, false] required
27
+ #
28
+ # @api private
29
+ def initialize(name:, type:, default: nil, collection: false, required: false)
30
+ super(default, collection, required)
31
+ @name = name
32
+ @type = type
33
+ end
34
+
35
+ # Return attributes as Ruby Hash
36
+ #
37
+ # @return [Hash]
38
+ #
39
+ # @api private
40
+ def attributes
41
+ { 'name' => @name, 'type' => @type }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shale'
4
+ require_relative 'xml/complex_type'
5
+ require_relative 'xml/import'
6
+ require_relative 'xml/ref_attribute'
7
+ require_relative 'xml/ref_element'
8
+ require_relative 'xml/schema'
9
+ require_relative 'xml/typed_attribute'
10
+ require_relative 'xml/typed_element'
11
+
12
+ module Shale
13
+ module Schema
14
+ # Class for handling XML schema
15
+ #
16
+ # @api public
17
+ class XML
18
+ # XML Schema default name
19
+ # @api private
20
+ DEFAULT_SCHEMA_NAME = 'schema'
21
+
22
+ @xml_types = Hash.new('string')
23
+
24
+ # Register Shale to XML type mapping
25
+ #
26
+ # @param [Shale::Type::Value] shale_type
27
+ # @param [String] xml_type
28
+ #
29
+ # @example
30
+ # Shale::Schema::XML.register_xml_type(Shale::Type::String, 'myType')
31
+ #
32
+ # @api public
33
+ def self.register_xml_type(shale_type, xml_type)
34
+ @xml_types[shale_type] = xml_type
35
+ end
36
+
37
+ # Return XML type for given Shale type
38
+ #
39
+ # @param [Shale::Type::Value] shale_type
40
+ #
41
+ # @return [String]
42
+ #
43
+ # @example
44
+ # Shale::Schema::XML.get_xml_type(Shale::Type::String)
45
+ # # => 'string'
46
+ #
47
+ # @api private
48
+ def self.get_xml_type(shale_type)
49
+ @xml_types[shale_type]
50
+ end
51
+
52
+ register_xml_type(Shale::Type::Boolean, 'boolean')
53
+ register_xml_type(Shale::Type::Date, 'date')
54
+ register_xml_type(Shale::Type::Float, 'decimal')
55
+ register_xml_type(Shale::Type::Integer, 'integer')
56
+ register_xml_type(Shale::Type::Time, 'dateTime')
57
+
58
+ # Generate XML Schema from Shale model and return
59
+ # it as a Shale::Schema::XML::Schema array
60
+ #
61
+ # @param [Shale::Mapper] klass
62
+ # @param [String, nil] base_name
63
+ #
64
+ # @raise [NotAShaleMapperError] when attribute is not a Shale model
65
+ #
66
+ # @return [Array<Shale::Schema::XML::Schema>]
67
+ #
68
+ # @example
69
+ # Shale::Schema::XML.new.as_schemas(Person)
70
+ #
71
+ # @api public
72
+ def as_schemas(klass, base_name = nil)
73
+ unless mapper_type?(klass)
74
+ raise NotAShaleMapperError, "XML Shema can't be generated for '#{klass}' type"
75
+ end
76
+
77
+ schemas = Hash.new do |hash, key|
78
+ name = "#{base_name || DEFAULT_SCHEMA_NAME}#{hash.keys.length}.xsd"
79
+ hash[key] = Schema.new(name, key)
80
+ end
81
+
82
+ default_namespace = klass.xml_mapping.default_namespace
83
+
84
+ root_element = TypedElement.new(
85
+ name: klass.xml_mapping.unprefixed_root,
86
+ type: [default_namespace.prefix, schematize(klass.name)].compact.join(':'),
87
+ required: true
88
+ )
89
+
90
+ schemas[default_namespace.name].add_namespace(
91
+ default_namespace.prefix,
92
+ default_namespace.name
93
+ )
94
+ schemas[default_namespace.name].add_child(root_element)
95
+
96
+ composites = collect_composite_types(klass, klass.xml_mapping.default_namespace.name)
97
+
98
+ composites.each do |composite|
99
+ type = composite[:type]
100
+ namespace = composite[:namespace]
101
+ children = []
102
+
103
+ type.xml_mapping.elements.values.each do |mapping|
104
+ attribute = type.attributes[mapping.attribute]
105
+ next unless attribute
106
+
107
+ xml_type = get_xml_type_for_attribute(attribute.type, mapping.namespace)
108
+ default = get_default_for_attribute(attribute)
109
+
110
+ if namespace == mapping.namespace.name
111
+ children << TypedElement.new(
112
+ name: mapping.name,
113
+ type: xml_type,
114
+ collection: attribute.collection?,
115
+ default: default
116
+ )
117
+ else
118
+ target_namespace = mapping.namespace
119
+ target_schema = schemas[target_namespace.name]
120
+
121
+ children << RefElement.new(
122
+ ref: mapping.prefixed_name,
123
+ collection: attribute.collection?,
124
+ default: default
125
+ )
126
+
127
+ target_element = TypedElement.new(
128
+ name: mapping.name,
129
+ type: xml_type,
130
+ required: true
131
+ )
132
+
133
+ import_element = Import.new(
134
+ target_namespace.name,
135
+ target_schema.name
136
+ )
137
+
138
+ target_schema.add_namespace(
139
+ target_namespace.prefix,
140
+ target_namespace.name
141
+ )
142
+ target_schema.add_child(target_element)
143
+
144
+ schemas[namespace].add_namespace(
145
+ target_namespace.prefix,
146
+ target_namespace.name
147
+ )
148
+ schemas[namespace].add_child(import_element)
149
+ end
150
+ end
151
+
152
+ type.xml_mapping.attributes.values.each do |mapping|
153
+ attribute = type.attributes[mapping.attribute]
154
+ next unless attribute
155
+
156
+ xml_type = get_xml_type_for_attribute(attribute.type, mapping.namespace)
157
+ default = get_default_for_attribute(attribute)
158
+
159
+ if namespace == mapping.namespace.name
160
+ children << TypedAttribute.new(
161
+ name: mapping.name,
162
+ type: xml_type,
163
+ default: default
164
+ )
165
+ else
166
+ target_namespace = mapping.namespace
167
+ target_schema = schemas[target_namespace.name]
168
+
169
+ children << RefAttribute.new(
170
+ ref: mapping.prefixed_name,
171
+ default: default
172
+ )
173
+
174
+ target_element = TypedAttribute.new(name: mapping.name, type: xml_type)
175
+
176
+ import_element = Import.new(
177
+ target_namespace.name,
178
+ target_schema.name
179
+ )
180
+
181
+ target_schema.add_namespace(
182
+ target_namespace.prefix,
183
+ target_namespace.name
184
+ )
185
+ target_schema.add_child(target_element)
186
+
187
+ schemas[namespace].add_namespace(
188
+ target_namespace.prefix,
189
+ target_namespace.name
190
+ )
191
+ schemas[namespace].add_child(import_element)
192
+ end
193
+ end
194
+
195
+ complex = ComplexType.new(
196
+ schematize(type.name),
197
+ children,
198
+ mixed: !type.xml_mapping.content.nil?
199
+ )
200
+
201
+ schemas[namespace].add_child(complex)
202
+ end
203
+
204
+ schemas.values
205
+ end
206
+
207
+ # Generate XML Schema from Shale model
208
+ #
209
+ # @param [Shale::Mapper] klass
210
+ # @param [String, nil] base_name
211
+ # @param [true, false] pretty
212
+ # @param [true, false] declaration
213
+ #
214
+ # @return [Hash<String, String>]
215
+ #
216
+ # @example
217
+ # Shale::Schema::XML.new.to_schemas(Person)
218
+ #
219
+ # @api public
220
+ def to_schemas(klass, base_name = nil, pretty: false, declaration: false)
221
+ schemas = as_schemas(klass, base_name)
222
+
223
+ options = [
224
+ pretty ? :pretty : nil,
225
+ declaration ? :declaration : nil,
226
+ ]
227
+
228
+ schemas.to_h do |schema|
229
+ [schema.name, Shale.xml_adapter.dump(schema.as_xml, *options)]
230
+ end
231
+ end
232
+
233
+ private
234
+
235
+ # Check it type inherits from Shale::Mapper
236
+ #
237
+ # @param [Class] type
238
+ #
239
+ # @return [true, false]
240
+ #
241
+ # @api private
242
+ def mapper_type?(type)
243
+ type < Shale::Mapper
244
+ end
245
+
246
+ # Collect recursively Shale::Mapper types
247
+ #
248
+ # @param [Shale::Mapper] type
249
+ # @param [String, nil] namespace
250
+ #
251
+ # @return [Array<Hash<Symbol, String>>]
252
+ #
253
+ # @api private
254
+ def collect_composite_types(type, namespace)
255
+ types = [{ type: type, namespace: namespace }]
256
+
257
+ type.xml_mapping.elements.values.each do |mapping|
258
+ attribute = type.attributes[mapping.attribute]
259
+ next unless attribute
260
+
261
+ is_mapper = mapper_type?(attribute.type)
262
+ is_included = types.include?({ type: attribute.type, namespace: namespace })
263
+
264
+ if is_mapper && !is_included
265
+ types += collect_composite_types(attribute.type, mapping.namespace.name)
266
+ end
267
+ end
268
+
269
+ types.uniq
270
+ end
271
+
272
+ # Convert Ruby class name to XML Schema name
273
+ #
274
+ # @param [String] word
275
+ #
276
+ # @return [String]
277
+ #
278
+ # @api private
279
+ def schematize(word)
280
+ word.gsub('::', '_')
281
+ end
282
+
283
+ # Return XML Schema type for given Shale::Type::Value
284
+ #
285
+ # @param [Shale::Type::Value] type
286
+ # @param [String] namespace
287
+ #
288
+ # @return [String]
289
+ #
290
+ # @api private
291
+ def get_xml_type_for_attribute(type, namespace)
292
+ if mapper_type?(type)
293
+ [namespace.prefix, schematize(type.name)].compact.join(':')
294
+ else
295
+ "xs:#{self.class.get_xml_type(type)}"
296
+ end
297
+ end
298
+
299
+ # Return default value for given Shale::Attribute
300
+ #
301
+ # @param [Shale::Attribute] attribute
302
+ #
303
+ # @return [String, nil]
304
+ #
305
+ # @api private
306
+ def get_default_for_attribute(attribute)
307
+ return unless attribute.default
308
+ return if attribute.collection?
309
+ return if mapper_type?(attribute.type)
310
+
311
+ value = attribute.type.cast(attribute.default.call)
312
+ attribute.type.as_xml_value(value)
313
+ end
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'schema/json'
4
+ require_relative 'schema/xml'
5
+
6
+ module Shale
7
+ # Module for handling JSON and XML schema
8
+ #
9
+ # @api private
10
+ module Schema
11
+ # Generate JSON Schema from Shale model
12
+ #
13
+ # @param [Shale::Mapper] klass
14
+ # @param [String, nil] id
15
+ # @param [String, nil] description
16
+ # @param [true, false] pretty
17
+ #
18
+ # @return [String]
19
+ #
20
+ # @example
21
+ # Shale::Schema.to_json(Person, id: 'My ID', description: 'My description', pretty: true)
22
+ # # => JSON schema
23
+ #
24
+ # @api public
25
+ def self.to_json(klass, id: nil, description: nil, pretty: false)
26
+ JSON.new.to_schema(klass, id: id, description: description, pretty: pretty)
27
+ end
28
+
29
+ # Generate XML Schema from Shale model
30
+ #
31
+ # @param [Shale::Mapper] klass
32
+ # @param [String, nil] base_name
33
+ # @param [true, false] pretty
34
+ # @param [true, false] declaration
35
+ #
36
+ # @return [Hash<String, String>]
37
+ #
38
+ # @example
39
+ # Shale::Schema.to_xml(Person, pretty: true, declaration: true)
40
+ # # => { 'schema0.xsd' => <JSON schema>, 'schema1.xsd' => <JSON schema> }
41
+ #
42
+ # @api public
43
+ def self.to_xml(klass, base_name = nil, pretty: false, declaration: false)
44
+ XML.new.to_schemas(klass, base_name, pretty: pretty, declaration: declaration)
45
+ end
46
+ end
47
+ end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base'
3
+ require_relative 'value'
4
4
 
5
5
  module Shale
6
6
  module Type
7
7
  # Cast value to Boolean
8
8
  #
9
9
  # @api public
10
- class Boolean < Base
10
+ class Boolean < Value
11
11
  FALSE_VALUES = [
12
12
  false,
13
13
  0,