shale 0.2.1 → 0.3.1

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