shale 0.4.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/LICENSE.txt +1 -1
- data/README.md +449 -40
- data/exe/shaleb +30 -6
- data/lib/shale/adapter/json.rb +3 -3
- data/lib/shale/adapter/nokogiri/document.rb +97 -0
- data/lib/shale/adapter/nokogiri/node.rb +100 -0
- data/lib/shale/adapter/nokogiri.rb +17 -156
- data/lib/shale/adapter/ox/document.rb +90 -0
- data/lib/shale/adapter/ox/node.rb +97 -0
- data/lib/shale/adapter/ox.rb +14 -138
- data/lib/shale/adapter/rexml/document.rb +98 -0
- data/lib/shale/adapter/rexml/node.rb +99 -0
- data/lib/shale/adapter/rexml.rb +14 -154
- data/lib/shale/adapter/toml_rb.rb +34 -0
- data/lib/shale/error.rb +57 -2
- data/lib/shale/mapper.rb +61 -9
- data/lib/shale/mapping/descriptor/dict.rb +12 -1
- data/lib/shale/mapping/descriptor/xml.rb +12 -2
- data/lib/shale/mapping/dict.rb +26 -2
- data/lib/shale/mapping/xml.rb +52 -6
- data/lib/shale/schema/{json_compiler → compiler}/boolean.rb +1 -1
- data/lib/shale/schema/{json_compiler/object.rb → compiler/complex.rb} +11 -8
- data/lib/shale/schema/{json_compiler → compiler}/date.rb +1 -1
- data/lib/shale/schema/{json_compiler → compiler}/float.rb +1 -1
- data/lib/shale/schema/{json_compiler → compiler}/integer.rb +1 -1
- data/lib/shale/schema/{json_compiler → compiler}/property.rb +6 -6
- data/lib/shale/schema/{json_compiler → compiler}/string.rb +1 -1
- data/lib/shale/schema/{json_compiler → compiler}/time.rb +1 -1
- 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 +32 -34
- data/lib/shale/schema/json_generator.rb +4 -6
- data/lib/shale/schema/xml_compiler.rb +919 -0
- data/lib/shale/schema/xml_generator/import.rb +2 -2
- data/lib/shale/schema/xml_generator.rb +11 -13
- data/lib/shale/schema.rb +16 -0
- data/lib/shale/type/complex.rb +763 -0
- data/lib/shale/type/value.rb +38 -9
- data/lib/shale/utils.rb +42 -7
- data/lib/shale/version.rb +1 -1
- data/lib/shale.rb +22 -19
- data/shale.gemspec +3 -3
- metadata +26 -17
- data/lib/shale/schema/json_compiler/utils.rb +0 -52
- data/lib/shale/schema/json_compiler/value.rb +0 -13
- data/lib/shale/type/composite.rb +0 -331
@@ -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, XML_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
|