shale 0.3.1 → 0.6.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +398 -41
  4. data/exe/shaleb +108 -36
  5. data/lib/shale/adapter/nokogiri/document.rb +97 -0
  6. data/lib/shale/adapter/nokogiri/node.rb +100 -0
  7. data/lib/shale/adapter/nokogiri.rb +11 -151
  8. data/lib/shale/adapter/ox/document.rb +90 -0
  9. data/lib/shale/adapter/ox/node.rb +97 -0
  10. data/lib/shale/adapter/ox.rb +9 -134
  11. data/lib/shale/adapter/rexml/document.rb +98 -0
  12. data/lib/shale/adapter/rexml/node.rb +99 -0
  13. data/lib/shale/adapter/rexml.rb +9 -150
  14. data/lib/shale/adapter/toml_rb.rb +34 -0
  15. data/lib/shale/attribute.rb +6 -0
  16. data/lib/shale/error.rb +56 -0
  17. data/lib/shale/mapper.rb +67 -13
  18. data/lib/shale/mapping/descriptor/xml.rb +10 -1
  19. data/lib/shale/mapping/dict.rb +18 -0
  20. data/lib/shale/mapping/xml.rb +40 -5
  21. data/lib/shale/schema/compiler/boolean.rb +21 -0
  22. data/lib/shale/schema/compiler/complex.rb +88 -0
  23. data/lib/shale/schema/compiler/date.rb +21 -0
  24. data/lib/shale/schema/compiler/float.rb +21 -0
  25. data/lib/shale/schema/compiler/integer.rb +21 -0
  26. data/lib/shale/schema/compiler/property.rb +70 -0
  27. data/lib/shale/schema/compiler/string.rb +21 -0
  28. data/lib/shale/schema/compiler/time.rb +21 -0
  29. data/lib/shale/schema/compiler/value.rb +21 -0
  30. data/lib/shale/schema/compiler/xml_complex.rb +50 -0
  31. data/lib/shale/schema/compiler/xml_property.rb +73 -0
  32. data/lib/shale/schema/json_compiler.rb +331 -0
  33. data/lib/shale/schema/{json → json_generator}/base.rb +2 -2
  34. data/lib/shale/schema/{json → json_generator}/boolean.rb +1 -1
  35. data/lib/shale/schema/{json → json_generator}/collection.rb +2 -2
  36. data/lib/shale/schema/{json → json_generator}/date.rb +1 -1
  37. data/lib/shale/schema/{json → json_generator}/float.rb +1 -1
  38. data/lib/shale/schema/{json → json_generator}/integer.rb +1 -1
  39. data/lib/shale/schema/{json → json_generator}/object.rb +5 -2
  40. data/lib/shale/schema/{json → json_generator}/ref.rb +1 -1
  41. data/lib/shale/schema/{json → json_generator}/schema.rb +6 -4
  42. data/lib/shale/schema/{json → json_generator}/string.rb +1 -1
  43. data/lib/shale/schema/{json → json_generator}/time.rb +1 -1
  44. data/lib/shale/schema/json_generator/value.rb +23 -0
  45. data/lib/shale/schema/{json.rb → json_generator.rb} +36 -36
  46. data/lib/shale/schema/xml_compiler.rb +919 -0
  47. data/lib/shale/schema/{xml → xml_generator}/attribute.rb +1 -1
  48. data/lib/shale/schema/{xml → xml_generator}/complex_type.rb +5 -2
  49. data/lib/shale/schema/{xml → xml_generator}/element.rb +1 -1
  50. data/lib/shale/schema/{xml → xml_generator}/import.rb +1 -1
  51. data/lib/shale/schema/{xml → xml_generator}/ref_attribute.rb +1 -1
  52. data/lib/shale/schema/{xml → xml_generator}/ref_element.rb +1 -1
  53. data/lib/shale/schema/{xml → xml_generator}/schema.rb +5 -5
  54. data/lib/shale/schema/{xml → xml_generator}/typed_attribute.rb +1 -1
  55. data/lib/shale/schema/{xml → xml_generator}/typed_element.rb +1 -1
  56. data/lib/shale/schema/{xml.rb → xml_generator.rb} +25 -26
  57. data/lib/shale/schema.rb +44 -5
  58. data/lib/shale/type/{composite.rb → complex.rb} +156 -51
  59. data/lib/shale/type/value.rb +31 -2
  60. data/lib/shale/utils.rb +42 -7
  61. data/lib/shale/version.rb +1 -1
  62. data/lib/shale.rb +22 -19
  63. data/shale.gemspec +3 -3
  64. metadata +50 -29
@@ -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