shale 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +200 -21
- data/exe/shaleb +108 -36
- data/lib/shale/adapter/nokogiri/document.rb +87 -0
- data/lib/shale/adapter/nokogiri/node.rb +100 -0
- data/lib/shale/adapter/nokogiri.rb +11 -151
- data/lib/shale/adapter/ox/document.rb +80 -0
- data/lib/shale/adapter/ox/node.rb +88 -0
- data/lib/shale/adapter/ox.rb +9 -134
- data/lib/shale/adapter/rexml/document.rb +88 -0
- data/lib/shale/adapter/rexml/node.rb +99 -0
- data/lib/shale/adapter/rexml.rb +9 -150
- data/lib/shale/attribute.rb +6 -0
- data/lib/shale/error.rb +39 -0
- data/lib/shale/mapper.rb +8 -6
- data/lib/shale/schema/compiler/boolean.rb +21 -0
- data/lib/shale/schema/compiler/complex.rb +88 -0
- data/lib/shale/schema/compiler/date.rb +21 -0
- data/lib/shale/schema/compiler/float.rb +21 -0
- data/lib/shale/schema/compiler/integer.rb +21 -0
- data/lib/shale/schema/compiler/property.rb +70 -0
- data/lib/shale/schema/compiler/string.rb +21 -0
- data/lib/shale/schema/compiler/time.rb +21 -0
- data/lib/shale/schema/compiler/value.rb +21 -0
- data/lib/shale/schema/compiler/xml_complex.rb +50 -0
- data/lib/shale/schema/compiler/xml_property.rb +73 -0
- data/lib/shale/schema/json_compiler.rb +331 -0
- data/lib/shale/schema/{json → json_generator}/base.rb +2 -2
- data/lib/shale/schema/{json → json_generator}/boolean.rb +1 -1
- data/lib/shale/schema/{json → json_generator}/collection.rb +2 -2
- data/lib/shale/schema/{json → json_generator}/date.rb +1 -1
- data/lib/shale/schema/{json → json_generator}/float.rb +1 -1
- data/lib/shale/schema/{json → json_generator}/integer.rb +1 -1
- data/lib/shale/schema/{json → json_generator}/object.rb +5 -2
- data/lib/shale/schema/{json → json_generator}/ref.rb +1 -1
- data/lib/shale/schema/{json → json_generator}/schema.rb +7 -5
- data/lib/shale/schema/{json → json_generator}/string.rb +1 -1
- data/lib/shale/schema/{json → json_generator}/time.rb +1 -1
- data/lib/shale/schema/json_generator/value.rb +23 -0
- data/lib/shale/schema/{json.rb → json_generator.rb} +36 -36
- data/lib/shale/schema/xml_compiler.rb +919 -0
- data/lib/shale/schema/{xml → xml_generator}/attribute.rb +1 -1
- data/lib/shale/schema/{xml → xml_generator}/complex_type.rb +5 -2
- data/lib/shale/schema/{xml → xml_generator}/element.rb +1 -1
- data/lib/shale/schema/{xml → xml_generator}/import.rb +1 -1
- data/lib/shale/schema/{xml → xml_generator}/ref_attribute.rb +1 -1
- data/lib/shale/schema/{xml → xml_generator}/ref_element.rb +1 -1
- data/lib/shale/schema/{xml → xml_generator}/schema.rb +5 -5
- data/lib/shale/schema/{xml → xml_generator}/typed_attribute.rb +1 -1
- data/lib/shale/schema/{xml → xml_generator}/typed_element.rb +1 -1
- data/lib/shale/schema/{xml.rb → xml_generator.rb} +25 -26
- data/lib/shale/schema.rb +44 -5
- data/lib/shale/type/{composite.rb → complex.rb} +34 -22
- data/lib/shale/utils.rb +42 -7
- data/lib/shale/version.rb +1 -1
- data/lib/shale.rb +8 -19
- data/shale.gemspec +1 -1
- metadata +47 -27
@@ -0,0 +1,919 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
require_relative '../../shale'
|
6
|
+
require_relative '../error'
|
7
|
+
require_relative 'compiler/boolean'
|
8
|
+
require_relative 'compiler/date'
|
9
|
+
require_relative 'compiler/float'
|
10
|
+
require_relative 'compiler/integer'
|
11
|
+
require_relative 'compiler/string'
|
12
|
+
require_relative 'compiler/time'
|
13
|
+
require_relative 'compiler/value'
|
14
|
+
require_relative 'compiler/xml_complex'
|
15
|
+
require_relative 'compiler/xml_property'
|
16
|
+
|
17
|
+
module Shale
|
18
|
+
module Schema
|
19
|
+
# Class for compiling XML schema into Ruby data model
|
20
|
+
#
|
21
|
+
# @api public
|
22
|
+
class XMLCompiler
|
23
|
+
# XML namespace URI
|
24
|
+
# @api private
|
25
|
+
XML_NAMESPACE_URI = 'http://www.w3.org/XML/1998/namespace'
|
26
|
+
|
27
|
+
# XML namespace prefix
|
28
|
+
# @api private
|
29
|
+
XML_NAMESPACE_PREFIX = 'xml'
|
30
|
+
|
31
|
+
# XML Schema namespace URI
|
32
|
+
# @api private
|
33
|
+
XS_NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema'
|
34
|
+
|
35
|
+
# XML Schema "schema" element name
|
36
|
+
# @api private
|
37
|
+
XS_SCHEMA = "#{XS_NAMESPACE_URI}:schema"
|
38
|
+
|
39
|
+
# XML Schema "element" element name
|
40
|
+
# @api private
|
41
|
+
XS_ELEMENT = "#{XS_NAMESPACE_URI}:element"
|
42
|
+
|
43
|
+
# XML Schema "attribute" element name
|
44
|
+
# @api private
|
45
|
+
XS_ATTRIBUTE = "#{XS_NAMESPACE_URI}:attribute"
|
46
|
+
|
47
|
+
# XML Schema "attribute" element name
|
48
|
+
# @api private
|
49
|
+
XS_SIMPLE_TYPE = "#{XS_NAMESPACE_URI}:simpleType"
|
50
|
+
|
51
|
+
# XML Schema "simpleContent" element name
|
52
|
+
# @api private
|
53
|
+
XS_SIMPLE_CONTENT = "#{XS_NAMESPACE_URI}:simpleContent"
|
54
|
+
|
55
|
+
# XML Schema "restriction" element name
|
56
|
+
# @api private
|
57
|
+
XS_RESTRICTION = "#{XS_NAMESPACE_URI}:restriction"
|
58
|
+
|
59
|
+
# XML Schema "group" element name
|
60
|
+
# @api private
|
61
|
+
XS_GROUP = "#{XS_NAMESPACE_URI}:group"
|
62
|
+
|
63
|
+
# XML Schema "attributeGroup" element name
|
64
|
+
# @api private
|
65
|
+
XS_ATTRIBUTE_GROUP = "#{XS_NAMESPACE_URI}:attributeGroup"
|
66
|
+
|
67
|
+
# XML Schema "complexType" element name
|
68
|
+
# @api private
|
69
|
+
XS_COMPLEX_TYPE = "#{XS_NAMESPACE_URI}:complexType"
|
70
|
+
|
71
|
+
# XML Schema "complexContent" element name
|
72
|
+
# @api private
|
73
|
+
XS_COMPLEX_CONTENT = "#{XS_NAMESPACE_URI}:complexContent"
|
74
|
+
|
75
|
+
# XML Schema "extension" element name
|
76
|
+
# @api private
|
77
|
+
XS_EXTENSION = "#{XS_NAMESPACE_URI}:extension"
|
78
|
+
|
79
|
+
# XML Schema "anyType" type
|
80
|
+
# @api private
|
81
|
+
XS_TYPE_ANY = "#{XS_NAMESPACE_URI}:anyType"
|
82
|
+
|
83
|
+
# XML Schema "date" types
|
84
|
+
# @api private
|
85
|
+
XS_TYPE_DATE = [
|
86
|
+
"#{XS_NAMESPACE_URI}:date",
|
87
|
+
].freeze
|
88
|
+
|
89
|
+
# XML Schema "datetime" types
|
90
|
+
# @api private
|
91
|
+
XS_TYPE_TIME = [
|
92
|
+
"#{XS_NAMESPACE_URI}:dateTime",
|
93
|
+
].freeze
|
94
|
+
|
95
|
+
# XML Schema "string" types
|
96
|
+
# @api private
|
97
|
+
XS_TYPE_STRING = [
|
98
|
+
"#{XS_NAMESPACE_URI}:string",
|
99
|
+
"#{XS_NAMESPACE_URI}:normalizedString",
|
100
|
+
"#{XS_NAMESPACE_URI}:token",
|
101
|
+
].freeze
|
102
|
+
|
103
|
+
# XML Schema "float" types
|
104
|
+
# @api private
|
105
|
+
XS_TYPE_FLOAT = [
|
106
|
+
"#{XS_NAMESPACE_URI}:decimal",
|
107
|
+
"#{XS_NAMESPACE_URI}:float",
|
108
|
+
"#{XS_NAMESPACE_URI}:double",
|
109
|
+
].freeze
|
110
|
+
|
111
|
+
# XML Schema "integer" types
|
112
|
+
# @api private
|
113
|
+
XS_TYPE_INTEGER = [
|
114
|
+
"#{XS_NAMESPACE_URI}:integer",
|
115
|
+
"#{XS_NAMESPACE_URI}:byte",
|
116
|
+
"#{XS_NAMESPACE_URI}:int",
|
117
|
+
"#{XS_NAMESPACE_URI}:long",
|
118
|
+
"#{XS_NAMESPACE_URI}:negativeInteger",
|
119
|
+
"#{XS_NAMESPACE_URI}:nonNegativeInteger",
|
120
|
+
"#{XS_NAMESPACE_URI}:nonPositiveInteger",
|
121
|
+
"#{XS_NAMESPACE_URI}:positiveInteger",
|
122
|
+
"#{XS_NAMESPACE_URI}:short",
|
123
|
+
"#{XS_NAMESPACE_URI}:unsignedLong",
|
124
|
+
"#{XS_NAMESPACE_URI}:unsignedInt",
|
125
|
+
"#{XS_NAMESPACE_URI}:unsignedShort",
|
126
|
+
"#{XS_NAMESPACE_URI}:unsignedByte",
|
127
|
+
].freeze
|
128
|
+
|
129
|
+
# XML Schema "boolean" types
|
130
|
+
# @api private
|
131
|
+
XS_TYPE_BOOLEAN = [
|
132
|
+
"#{XS_NAMESPACE_URI}:boolean",
|
133
|
+
].freeze
|
134
|
+
|
135
|
+
# Shale model template
|
136
|
+
# @api private
|
137
|
+
MODEL_TEMPLATE = ERB.new(<<~TEMPLATE, trim_mode: '-')
|
138
|
+
require 'shale'
|
139
|
+
<%- unless type.references.empty? -%>
|
140
|
+
|
141
|
+
<%- type.references.each do |property| -%>
|
142
|
+
require_relative '<%= property.type.file_name %>'
|
143
|
+
<%- end -%>
|
144
|
+
<%- end -%>
|
145
|
+
|
146
|
+
class <%= type.name %> < Shale::Mapper
|
147
|
+
<%- type.properties.select(&:content?).each do |property| -%>
|
148
|
+
attribute :<%= property.attribute_name %>, <%= property.type.name -%>
|
149
|
+
<%- if property.collection? %>, collection: true<% end -%>
|
150
|
+
<%- unless property.default.nil? %>, default: -> { <%= property.default %> }<% end %>
|
151
|
+
<%- end -%>
|
152
|
+
<%- type.properties.select(&:attribute?).each do |property| -%>
|
153
|
+
attribute :<%= property.attribute_name %>, <%= property.type.name -%>
|
154
|
+
<%- if property.collection? %>, collection: true<% end -%>
|
155
|
+
<%- unless property.default.nil? %>, default: -> { <%= property.default %> }<% end %>
|
156
|
+
<%- end -%>
|
157
|
+
<%- type.properties.select(&:element?).each do |property| -%>
|
158
|
+
attribute :<%= property.attribute_name %>, <%= property.type.name -%>
|
159
|
+
<%- if property.collection? %>, collection: true<% end -%>
|
160
|
+
<%- unless property.default.nil? %>, default: -> { <%= property.default %> }<% end %>
|
161
|
+
<%- end -%>
|
162
|
+
|
163
|
+
xml do
|
164
|
+
root '<%= type.root %>'
|
165
|
+
<%- if type.namespace -%>
|
166
|
+
namespace '<%= type.namespace %>', '<%= type.prefix %>'
|
167
|
+
<%- end -%>
|
168
|
+
<%- unless type.properties.empty? -%>
|
169
|
+
|
170
|
+
<%- type.properties.select(&:content?).each do |property| -%>
|
171
|
+
map_content to: :<%= property.attribute_name %>
|
172
|
+
<%- end -%>
|
173
|
+
<%- type.properties.select(&:attribute?).each do |property| -%>
|
174
|
+
map_attribute '<%= property.mapping_name %>', to: :<%= property.attribute_name -%>
|
175
|
+
<%- if property.namespace %>, prefix: '<%= property.prefix %>'<%- end -%>
|
176
|
+
<%- if property.namespace %>, namespace: '<%= property.namespace %>'<% end %>
|
177
|
+
<%- end -%>
|
178
|
+
<%- type.properties.select(&:element?).each do |property| -%>
|
179
|
+
map_element '<%= property.mapping_name %>', to: :<%= property.attribute_name -%>
|
180
|
+
<%- if type.namespace != property.namespace %>, prefix: <%= property.prefix ? "'\#{property.prefix}'" : 'nil' %><%- end -%>
|
181
|
+
<%- if type.namespace != property.namespace %>, namespace: <%= property.namespace ? "'\#{property.namespace}'" : 'nil' %><% end %>
|
182
|
+
<%- end -%>
|
183
|
+
<%- end -%>
|
184
|
+
end
|
185
|
+
end
|
186
|
+
TEMPLATE
|
187
|
+
|
188
|
+
# Generate Shale models from XML Schema and return them as a Ruby Array of objects
|
189
|
+
#
|
190
|
+
# @param [Array<String>] schemas
|
191
|
+
#
|
192
|
+
# @raise [AdapterError] when XML adapter is not set or Ox adapter is used
|
193
|
+
# @raise [SchemaError] when XML Schema has errors
|
194
|
+
#
|
195
|
+
# @return [Array<Shale::Schema::Compiler::XMLComplex>]
|
196
|
+
#
|
197
|
+
# @example
|
198
|
+
# Shale::Schema::XMLCompiler.new.as_models([schema1, schema2])
|
199
|
+
#
|
200
|
+
# @api public
|
201
|
+
def as_models(schemas)
|
202
|
+
unless Shale.xml_adapter
|
203
|
+
raise AdapterError, ADAPTER_NOT_SET_MESSAGE
|
204
|
+
end
|
205
|
+
|
206
|
+
if Shale.xml_adapter.name == 'Shale::Adapter::Ox'
|
207
|
+
msg = "Ox doesn't support XML namespaces and can't be used to compile XML Schema"
|
208
|
+
raise AdapterError, msg
|
209
|
+
end
|
210
|
+
|
211
|
+
schemas = schemas.map do |schema|
|
212
|
+
Shale.xml_adapter.load(schema)
|
213
|
+
end
|
214
|
+
|
215
|
+
@elements_repository = {}
|
216
|
+
@attributes_repository = {}
|
217
|
+
@simple_types_repository = {}
|
218
|
+
@element_groups_repository = {}
|
219
|
+
@attribute_groups_repository = {}
|
220
|
+
@complex_types_repository = {}
|
221
|
+
@complex_types = {}
|
222
|
+
@types = []
|
223
|
+
|
224
|
+
schemas.each do |schema|
|
225
|
+
build_repositories(schema)
|
226
|
+
end
|
227
|
+
|
228
|
+
resolve_nested_refs(@simple_types_repository)
|
229
|
+
|
230
|
+
schemas.each do |schema|
|
231
|
+
compile(schema)
|
232
|
+
end
|
233
|
+
|
234
|
+
@types = @types.uniq
|
235
|
+
|
236
|
+
total_duplicates = Hash.new(0)
|
237
|
+
duplicates = Hash.new(0)
|
238
|
+
|
239
|
+
@types.each do |type|
|
240
|
+
total_duplicates[type.name] += 1
|
241
|
+
end
|
242
|
+
|
243
|
+
@types.each do |type|
|
244
|
+
duplicates[type.name] += 1
|
245
|
+
|
246
|
+
if total_duplicates[type.name] > 1
|
247
|
+
type.name = format("#{type.name}%d", duplicates[type.name])
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
@types.reverse
|
252
|
+
rescue ParseError => e
|
253
|
+
raise SchemaError, "XML Schema document is invalid: #{e.message}"
|
254
|
+
end
|
255
|
+
|
256
|
+
# Generate Shale models from XML Schema
|
257
|
+
#
|
258
|
+
# @param [Array<String>] schemas
|
259
|
+
#
|
260
|
+
# @raise [SchemaError] when XML Schema has errors
|
261
|
+
#
|
262
|
+
# @return [Hash<String, String>]
|
263
|
+
#
|
264
|
+
# @example
|
265
|
+
# Shale::Schema::XMLCompiler.new.to_models([schema1, schema2])
|
266
|
+
#
|
267
|
+
# @api public
|
268
|
+
def to_models(schemas)
|
269
|
+
types = as_models(schemas)
|
270
|
+
|
271
|
+
types.to_h do |type|
|
272
|
+
[type.file_name, MODEL_TEMPLATE.result(binding)]
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
# Check if node is a given type
|
279
|
+
#
|
280
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
281
|
+
# @param [String] name
|
282
|
+
#
|
283
|
+
# @return [true false]
|
284
|
+
#
|
285
|
+
# @api private
|
286
|
+
def node_is_a?(node, name)
|
287
|
+
node.name == name
|
288
|
+
end
|
289
|
+
|
290
|
+
# Check if node has attribute
|
291
|
+
#
|
292
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
293
|
+
# @param [String] name
|
294
|
+
#
|
295
|
+
# @return [true false]
|
296
|
+
#
|
297
|
+
# @api private
|
298
|
+
# rubocop:disable Naming/PredicateName
|
299
|
+
def has_attribute?(node, name)
|
300
|
+
node.attributes.key?(name)
|
301
|
+
end
|
302
|
+
# rubocop:enable Naming/PredicateName
|
303
|
+
|
304
|
+
# Traverse over all child nodes and call a block for each one
|
305
|
+
#
|
306
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
307
|
+
#
|
308
|
+
# @yieldparam [Shale::Adapter::<Adapter>::Node]
|
309
|
+
#
|
310
|
+
# @api private
|
311
|
+
def traverse(node, &block)
|
312
|
+
node.children.each do |child|
|
313
|
+
block.call(child)
|
314
|
+
traverse(child, &block)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# Return XML namespaces
|
319
|
+
#
|
320
|
+
# @param [String] schema
|
321
|
+
#
|
322
|
+
# @return [Hash<String, String>]
|
323
|
+
#
|
324
|
+
# @api private
|
325
|
+
def get_namespaces(schema)
|
326
|
+
schema.namespaces.merge(XML_NAMESPACE_PREFIX => XML_NAMESPACE_URI)
|
327
|
+
end
|
328
|
+
|
329
|
+
# Get schema node
|
330
|
+
#
|
331
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
332
|
+
#
|
333
|
+
# @return [Shale::Adapter::<Adapter>::Node, nil]
|
334
|
+
#
|
335
|
+
# @api private
|
336
|
+
def get_schema(node)
|
337
|
+
el = node
|
338
|
+
|
339
|
+
while el
|
340
|
+
return el if node_is_a?(el, XS_SCHEMA)
|
341
|
+
el = el.parent
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Join parts with ":"
|
346
|
+
#
|
347
|
+
# @param [String] name
|
348
|
+
# @param [String] namespace
|
349
|
+
#
|
350
|
+
# @return [String]
|
351
|
+
#
|
352
|
+
# @api private
|
353
|
+
def join_id_parts(name, namespace)
|
354
|
+
[namespace, name].compact.join(':')
|
355
|
+
end
|
356
|
+
|
357
|
+
# Build id from name
|
358
|
+
#
|
359
|
+
# @param [String] name
|
360
|
+
# @param [String] namespace
|
361
|
+
#
|
362
|
+
# @return [String]
|
363
|
+
#
|
364
|
+
# @api private
|
365
|
+
def build_id_from_name(name, namespace)
|
366
|
+
join_id_parts(name, namespace)
|
367
|
+
end
|
368
|
+
|
369
|
+
# Build id from child nodes
|
370
|
+
#
|
371
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
372
|
+
# @param [String] namespace
|
373
|
+
#
|
374
|
+
# @return [String]
|
375
|
+
#
|
376
|
+
# @api private
|
377
|
+
def build_id_from_parents(node, namespace)
|
378
|
+
parts = [node.attributes['name']]
|
379
|
+
|
380
|
+
while (node = node.parent)
|
381
|
+
parts << node.attributes['name']
|
382
|
+
end
|
383
|
+
|
384
|
+
join_id_parts(namespace, parts.compact.reverse.join('/'))
|
385
|
+
end
|
386
|
+
|
387
|
+
# Build unique id for a node
|
388
|
+
#
|
389
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
390
|
+
#
|
391
|
+
# @return [String]
|
392
|
+
#
|
393
|
+
# @api private
|
394
|
+
def build_id(node)
|
395
|
+
name = node.attributes['name']
|
396
|
+
namespace = get_schema(node).attributes['targetNamespace']
|
397
|
+
|
398
|
+
if name
|
399
|
+
build_id_from_name(name, namespace)
|
400
|
+
else
|
401
|
+
build_id_from_parents(node, namespace)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
# Replace namespace prefixes with URIs
|
406
|
+
#
|
407
|
+
# @param [String] type
|
408
|
+
# @param [Hash<String, String>] namespaces
|
409
|
+
#
|
410
|
+
# @return [String]
|
411
|
+
#
|
412
|
+
# @api private
|
413
|
+
def replace_ns_prefixes(type, namespaces)
|
414
|
+
namespaces.each do |prefix, name|
|
415
|
+
type = type.sub(/^#{prefix}/, name)
|
416
|
+
end
|
417
|
+
|
418
|
+
type
|
419
|
+
end
|
420
|
+
|
421
|
+
# Normalize reference to type
|
422
|
+
#
|
423
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
424
|
+
#
|
425
|
+
# @return [String]
|
426
|
+
#
|
427
|
+
# @api private
|
428
|
+
def normalize_type_ref(node)
|
429
|
+
schema = get_schema(node)
|
430
|
+
type = node.attributes['type']
|
431
|
+
|
432
|
+
if type
|
433
|
+
replace_ns_prefixes(type, get_namespaces(schema))
|
434
|
+
else
|
435
|
+
build_id_from_parents(node, schema.attributes['targetNamespace'])
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
# Infer simple type from node
|
440
|
+
#
|
441
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
442
|
+
#
|
443
|
+
# @return [String]
|
444
|
+
#
|
445
|
+
# @api private
|
446
|
+
def infer_simple_type(node)
|
447
|
+
type = XS_TYPE_ANY
|
448
|
+
namespaces = get_namespaces(get_schema(node))
|
449
|
+
|
450
|
+
traverse(node) do |child|
|
451
|
+
if node_is_a?(child, XS_RESTRICTION) && has_attribute?(child, 'base')
|
452
|
+
type = replace_ns_prefixes(child.attributes['base'], namespaces)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
type
|
457
|
+
end
|
458
|
+
|
459
|
+
def resolve_ref(repository, ref)
|
460
|
+
target = repository[ref]
|
461
|
+
|
462
|
+
raise SchemaError, "Can't resolve reference/type: #{ref}" unless target
|
463
|
+
|
464
|
+
target
|
465
|
+
end
|
466
|
+
|
467
|
+
# Resolve namespace for complex type
|
468
|
+
#
|
469
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
470
|
+
#
|
471
|
+
# @return [Array<String>]
|
472
|
+
#
|
473
|
+
# @api private
|
474
|
+
def resolve_complex_type_namespace(node)
|
475
|
+
schema = get_schema(node)
|
476
|
+
namespaces = get_namespaces(schema)
|
477
|
+
target_namespace = schema.attributes['targetNamespace']
|
478
|
+
form_default = schema.attributes['elementFormDefault']
|
479
|
+
form_element = node.parent.attributes['form'] || form_default
|
480
|
+
|
481
|
+
is_top = node_is_a?(node.parent, XS_SCHEMA)
|
482
|
+
parent_is_top = node.parent.parent && node_is_a?(node.parent.parent, XS_SCHEMA)
|
483
|
+
parent_is_qualified = form_element == 'qualified'
|
484
|
+
|
485
|
+
if is_top || parent_is_top || parent_is_qualified
|
486
|
+
[namespaces.key(target_namespace), target_namespace]
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# Resolve namespace for properties
|
491
|
+
#
|
492
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
493
|
+
# @param [String] form_default
|
494
|
+
#
|
495
|
+
# @return [Array<String>]
|
496
|
+
#
|
497
|
+
# @api private
|
498
|
+
def resolve_namespace(node, form_default)
|
499
|
+
schema = get_schema(node)
|
500
|
+
namespaces = get_namespaces(schema)
|
501
|
+
target_namespace = schema.attributes['targetNamespace']
|
502
|
+
form_default = schema.attributes[form_default]
|
503
|
+
form_element = node.attributes['form'] || form_default
|
504
|
+
|
505
|
+
is_top = node_is_a?(node.parent, XS_SCHEMA)
|
506
|
+
is_qualified = form_element == 'qualified'
|
507
|
+
|
508
|
+
if is_top || is_qualified
|
509
|
+
[namespaces.key(target_namespace), target_namespace]
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
# Build repository of XML Schema entities
|
514
|
+
#
|
515
|
+
# @param [Shale::Adapter::<Adapter>::Node] schema
|
516
|
+
#
|
517
|
+
# @api private
|
518
|
+
def build_repositories(schema)
|
519
|
+
traverse(schema) do |node|
|
520
|
+
id = build_id(node)
|
521
|
+
|
522
|
+
if node_is_a?(node, XS_ELEMENT) && has_attribute?(node, 'name')
|
523
|
+
@elements_repository[id] = node
|
524
|
+
elsif node_is_a?(node, XS_ATTRIBUTE) && has_attribute?(node, 'name')
|
525
|
+
@attributes_repository[id] = node
|
526
|
+
elsif node_is_a?(node, XS_SIMPLE_TYPE)
|
527
|
+
@simple_types_repository[id] = infer_simple_type(node)
|
528
|
+
elsif node_is_a?(node, XS_GROUP) && has_attribute?(node, 'name')
|
529
|
+
@element_groups_repository[id] = node
|
530
|
+
elsif node_is_a?(node, XS_ATTRIBUTE_GROUP) && has_attribute?(node, 'name')
|
531
|
+
@attribute_groups_repository[id] = node
|
532
|
+
elsif node_is_a?(node, XS_COMPLEX_TYPE)
|
533
|
+
@complex_types_repository[id] = node
|
534
|
+
|
535
|
+
name = node.attributes['name'] || node.parent.attributes['name']
|
536
|
+
prefix, namespace = resolve_complex_type_namespace(node)
|
537
|
+
|
538
|
+
@complex_types[id] = Compiler::XMLComplex.new(id, name, prefix, namespace)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Resolve nested references
|
544
|
+
#
|
545
|
+
# @param [Hash<String, String>] repo
|
546
|
+
#
|
547
|
+
# @api private
|
548
|
+
def resolve_nested_refs(repo)
|
549
|
+
unresolved = repo.values & repo.keys
|
550
|
+
|
551
|
+
until unresolved.empty?
|
552
|
+
unresolved.each do |ref|
|
553
|
+
key = repo.key(ref)
|
554
|
+
repo[key] = repo[ref]
|
555
|
+
end
|
556
|
+
|
557
|
+
unresolved = repo.values & repo.keys
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
# Infer type from node
|
562
|
+
#
|
563
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
564
|
+
#
|
565
|
+
# @return [Shale::Schema::Compiler::<any>]
|
566
|
+
#
|
567
|
+
# @api private
|
568
|
+
def infer_type(node)
|
569
|
+
namespaces = get_namespaces(get_schema(node))
|
570
|
+
type = normalize_type_ref(node)
|
571
|
+
infer_type_from_xs_type(type, namespaces)
|
572
|
+
end
|
573
|
+
|
574
|
+
def infer_type_from_xs_type(type, namespaces)
|
575
|
+
type = replace_ns_prefixes(type, namespaces)
|
576
|
+
|
577
|
+
return @complex_types[type] if @complex_types[type]
|
578
|
+
|
579
|
+
type = @simple_types_repository[type] if @simple_types_repository[type]
|
580
|
+
|
581
|
+
if XS_TYPE_DATE.include?(type)
|
582
|
+
Compiler::Date.new
|
583
|
+
elsif XS_TYPE_TIME.include?(type)
|
584
|
+
Compiler::Time.new
|
585
|
+
elsif XS_TYPE_STRING.include?(type)
|
586
|
+
Compiler::String.new
|
587
|
+
elsif XS_TYPE_FLOAT.include?(type)
|
588
|
+
Compiler::Float.new
|
589
|
+
elsif XS_TYPE_INTEGER.include?(type)
|
590
|
+
Compiler::Integer.new
|
591
|
+
elsif XS_TYPE_BOOLEAN.include?(type)
|
592
|
+
Compiler::Boolean.new
|
593
|
+
else
|
594
|
+
Compiler::Value.new
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# Test if element is a collection
|
599
|
+
#
|
600
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
601
|
+
# @param [String] max_occurs
|
602
|
+
#
|
603
|
+
# @return [true, false]
|
604
|
+
#
|
605
|
+
# @api private
|
606
|
+
def infer_collection(node, max_occurs)
|
607
|
+
max = node.parent.attributes['maxOccurs'] || max_occurs || '1'
|
608
|
+
|
609
|
+
if has_attribute?(node, 'maxOccurs')
|
610
|
+
max = node.attributes['maxOccurs'] || '1'
|
611
|
+
end
|
612
|
+
|
613
|
+
max == 'unbounded' || max.to_i > 1
|
614
|
+
end
|
615
|
+
|
616
|
+
# Return base type ref
|
617
|
+
#
|
618
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
619
|
+
#
|
620
|
+
# @return [String]
|
621
|
+
#
|
622
|
+
# @api private
|
623
|
+
def find_extension(node)
|
624
|
+
complex_content = node.children.find { |e| node_is_a?(e, XS_COMPLEX_CONTENT) }
|
625
|
+
return nil unless complex_content
|
626
|
+
|
627
|
+
child = complex_content.children.find do |ch|
|
628
|
+
node_is_a?(ch, XS_EXTENSION)
|
629
|
+
end
|
630
|
+
|
631
|
+
if child && has_attribute?(child, 'base')
|
632
|
+
namespaces = get_namespaces(get_schema(node))
|
633
|
+
replace_ns_prefixes(child.attributes['base'], namespaces)
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
# Return base type ref
|
638
|
+
#
|
639
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
640
|
+
#
|
641
|
+
# @return [String]
|
642
|
+
#
|
643
|
+
# @api private
|
644
|
+
def find_restriction(node)
|
645
|
+
complex_content = node.children.find { |e| node_is_a?(e, XS_COMPLEX_CONTENT) }
|
646
|
+
return nil unless complex_content
|
647
|
+
|
648
|
+
child = complex_content.children.find do |ch|
|
649
|
+
node_is_a?(ch, XS_RESTRICTION)
|
650
|
+
end
|
651
|
+
|
652
|
+
if child && has_attribute?(child, 'base')
|
653
|
+
namespaces = get_namespaces(get_schema(node))
|
654
|
+
replace_ns_prefixes(child.attributes['base'], namespaces)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
# Return content type
|
659
|
+
#
|
660
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
661
|
+
#
|
662
|
+
# @return [String]
|
663
|
+
#
|
664
|
+
# @api private
|
665
|
+
def find_content(node)
|
666
|
+
return "#{XS_NAMESPACE_URI}:string" if node.attributes['mixed'] == 'true'
|
667
|
+
|
668
|
+
type = nil
|
669
|
+
|
670
|
+
node.children.each do |child|
|
671
|
+
if node_is_a?(child, XS_SIMPLE_CONTENT)
|
672
|
+
child.children.each do |ch|
|
673
|
+
if ch.attributes['base']
|
674
|
+
type = ch.attributes['base']
|
675
|
+
end
|
676
|
+
end
|
677
|
+
elsif node_is_a?(child, XS_COMPLEX_CONTENT) && child.attributes['mixed'] == 'true'
|
678
|
+
type = "#{XS_NAMESPACE_URI}:string"
|
679
|
+
end
|
680
|
+
|
681
|
+
break if type
|
682
|
+
end
|
683
|
+
|
684
|
+
type
|
685
|
+
end
|
686
|
+
|
687
|
+
# Return attributes
|
688
|
+
#
|
689
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
690
|
+
#
|
691
|
+
# @return [Array<Shale::Adapter::<Adapter>::Node>]
|
692
|
+
#
|
693
|
+
# @api private
|
694
|
+
def find_attributes(node)
|
695
|
+
found = []
|
696
|
+
|
697
|
+
namespaces = get_namespaces(get_schema(node))
|
698
|
+
|
699
|
+
node.children.each do |child|
|
700
|
+
if node_is_a?(child, XS_ATTRIBUTE) && has_attribute?(child, 'ref')
|
701
|
+
ref = replace_ns_prefixes(child.attributes['ref'], namespaces)
|
702
|
+
found << resolve_ref(@attributes_repository, ref)
|
703
|
+
elsif node_is_a?(child, XS_ATTRIBUTE) && has_attribute?(child, 'name')
|
704
|
+
found << child
|
705
|
+
elsif node_is_a?(child, XS_ATTRIBUTE_GROUP) && has_attribute?(child, 'ref')
|
706
|
+
ref = replace_ns_prefixes(child.attributes['ref'], namespaces)
|
707
|
+
group = resolve_ref(@attribute_groups_repository, ref)
|
708
|
+
found += find_attributes(group)
|
709
|
+
elsif !node_is_a?(child, XS_ELEMENT)
|
710
|
+
found += find_attributes(child)
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
found
|
715
|
+
end
|
716
|
+
|
717
|
+
# Return elements
|
718
|
+
#
|
719
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
720
|
+
#
|
721
|
+
# @return [Hash]
|
722
|
+
#
|
723
|
+
# @api private
|
724
|
+
def find_elements(node)
|
725
|
+
found = []
|
726
|
+
|
727
|
+
namespaces = get_namespaces(get_schema(node))
|
728
|
+
|
729
|
+
node.children.each do |child|
|
730
|
+
if node_is_a?(child, XS_ELEMENT) && has_attribute?(child, 'ref')
|
731
|
+
max_occurs = nil
|
732
|
+
|
733
|
+
if has_attribute?(child.parent, 'maxOccurs')
|
734
|
+
max_occurs = child.parent.attributes['maxOccurs']
|
735
|
+
end
|
736
|
+
|
737
|
+
if has_attribute?(child, 'maxOccurs')
|
738
|
+
max_occurs = child.attributes['maxOccurs']
|
739
|
+
end
|
740
|
+
|
741
|
+
ref = replace_ns_prefixes(child.attributes['ref'], namespaces)
|
742
|
+
found << { element: resolve_ref(@elements_repository, ref), max_occurs: max_occurs }
|
743
|
+
elsif node_is_a?(child, XS_ELEMENT) && has_attribute?(child, 'name')
|
744
|
+
found << { element: child }
|
745
|
+
elsif node_is_a?(child, XS_GROUP) && has_attribute?(child, 'ref')
|
746
|
+
ref = replace_ns_prefixes(child.attributes['ref'], namespaces)
|
747
|
+
group = resolve_ref(@element_groups_repository, ref)
|
748
|
+
group_elements = find_elements(group)
|
749
|
+
|
750
|
+
max_occurs = nil
|
751
|
+
|
752
|
+
if has_attribute?(child.parent, 'maxOccurs')
|
753
|
+
max_occurs = child.parent.attributes['maxOccurs']
|
754
|
+
end
|
755
|
+
|
756
|
+
if has_attribute?(child, 'maxOccurs')
|
757
|
+
max_occurs = child.attributes['maxOccurs']
|
758
|
+
end
|
759
|
+
|
760
|
+
if max_occurs
|
761
|
+
group_elements.each do |data|
|
762
|
+
el = data[:element]
|
763
|
+
|
764
|
+
unless el.attributes.key?('maxOccurs')
|
765
|
+
data[:max_occurs] = max_occurs
|
766
|
+
end
|
767
|
+
end
|
768
|
+
end
|
769
|
+
|
770
|
+
found += group_elements
|
771
|
+
else
|
772
|
+
found += find_elements(child)
|
773
|
+
end
|
774
|
+
end
|
775
|
+
|
776
|
+
found
|
777
|
+
end
|
778
|
+
|
779
|
+
# Compile complex type
|
780
|
+
#
|
781
|
+
# @param [Shale::Schema::Compiler::XMLComplex] complex_type
|
782
|
+
# @param [Shale::Adapter::<Adapter>::Node] node
|
783
|
+
# @param [Symbol] mode
|
784
|
+
#
|
785
|
+
# @api private
|
786
|
+
def compile_complex_type(complex_type, node, mode: :standard)
|
787
|
+
extension = find_extension(node)
|
788
|
+
restriction = find_restriction(node)
|
789
|
+
|
790
|
+
if extension
|
791
|
+
base_node = resolve_ref(@complex_types_repository, extension)
|
792
|
+
compile_complex_type(complex_type, base_node)
|
793
|
+
end
|
794
|
+
|
795
|
+
if restriction
|
796
|
+
base_node = resolve_ref(@complex_types_repository, restriction)
|
797
|
+
compile_complex_type(complex_type, base_node, mode: :restriction)
|
798
|
+
end
|
799
|
+
|
800
|
+
if mode == :standard
|
801
|
+
content_type = find_content(node)
|
802
|
+
|
803
|
+
if content_type
|
804
|
+
namespaces = get_namespaces(get_schema(node))
|
805
|
+
type = infer_type_from_xs_type(content_type, namespaces)
|
806
|
+
|
807
|
+
property = Compiler::XMLProperty.new(
|
808
|
+
name: 'content',
|
809
|
+
type: type,
|
810
|
+
collection: false,
|
811
|
+
default: nil,
|
812
|
+
prefix: nil,
|
813
|
+
namespace: nil,
|
814
|
+
category: :content
|
815
|
+
)
|
816
|
+
|
817
|
+
complex_type.add_property(property)
|
818
|
+
end
|
819
|
+
end
|
820
|
+
|
821
|
+
elements = find_attributes(node)
|
822
|
+
|
823
|
+
elements.each do |element|
|
824
|
+
name = element.attributes['name']
|
825
|
+
type = infer_type(element)
|
826
|
+
default = element.attributes['default']
|
827
|
+
prefix, namespace = resolve_namespace(element, 'attributeFormDefault')
|
828
|
+
|
829
|
+
property = Compiler::XMLProperty.new(
|
830
|
+
name: name,
|
831
|
+
type: type,
|
832
|
+
collection: false,
|
833
|
+
default: default,
|
834
|
+
prefix: prefix,
|
835
|
+
namespace: namespace,
|
836
|
+
category: :attribute
|
837
|
+
)
|
838
|
+
|
839
|
+
complex_type.add_property(property)
|
840
|
+
end
|
841
|
+
|
842
|
+
if mode == :standard
|
843
|
+
elements = find_elements(node)
|
844
|
+
|
845
|
+
elements.each do |data|
|
846
|
+
element = data[:element]
|
847
|
+
name = element.attributes['name']
|
848
|
+
type = infer_type(element)
|
849
|
+
collection = infer_collection(element, data[:max_occurs])
|
850
|
+
default = element.attributes['default']
|
851
|
+
prefix, namespace = resolve_namespace(element, 'elementFormDefault')
|
852
|
+
|
853
|
+
property = Compiler::XMLProperty.new(
|
854
|
+
name: name,
|
855
|
+
type: type,
|
856
|
+
collection: collection,
|
857
|
+
default: default,
|
858
|
+
prefix: prefix,
|
859
|
+
namespace: namespace,
|
860
|
+
category: :element
|
861
|
+
)
|
862
|
+
|
863
|
+
complex_type.add_property(property)
|
864
|
+
end
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
# Return top level elements
|
869
|
+
#
|
870
|
+
# @param [Shale::Adapter::<Adapter>::Node] schema
|
871
|
+
#
|
872
|
+
# @return [Shale::Adapter::<Adapter>::Node]
|
873
|
+
#
|
874
|
+
# @api private
|
875
|
+
def find_top_level_elements(schema)
|
876
|
+
schema.children.select { |child| node_is_a?(child, XS_ELEMENT) }
|
877
|
+
end
|
878
|
+
|
879
|
+
# Collect active types
|
880
|
+
#
|
881
|
+
# @param [Shale::Schema::Compiler::<any>] type
|
882
|
+
#
|
883
|
+
# @api private
|
884
|
+
def collect_active_types(type)
|
885
|
+
return if @types.include?(type)
|
886
|
+
return unless type.is_a?(Compiler::XMLComplex)
|
887
|
+
|
888
|
+
@types << type
|
889
|
+
|
890
|
+
type.properties.each do |property|
|
891
|
+
collect_active_types(property.type)
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
# Compile schema
|
896
|
+
#
|
897
|
+
# @param [Shale::Adapter::<Adapter>::Node] schema
|
898
|
+
#
|
899
|
+
# @api private
|
900
|
+
def compile(schema)
|
901
|
+
@complex_types_repository.each do |id, node|
|
902
|
+
complex_type = resolve_ref(@complex_types, id)
|
903
|
+
compile_complex_type(complex_type, node)
|
904
|
+
end
|
905
|
+
|
906
|
+
elements = find_top_level_elements(schema)
|
907
|
+
|
908
|
+
elements.each do |element|
|
909
|
+
type = @complex_types[normalize_type_ref(element)]
|
910
|
+
|
911
|
+
next unless type
|
912
|
+
|
913
|
+
type.root = element.attributes['name']
|
914
|
+
collect_active_types(type)
|
915
|
+
end
|
916
|
+
end
|
917
|
+
end
|
918
|
+
end
|
919
|
+
end
|