shale 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +93 -7
  4. data/exe/shaleb +30 -6
  5. data/lib/shale/adapter/nokogiri/document.rb +87 -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 +80 -0
  9. data/lib/shale/adapter/ox/node.rb +88 -0
  10. data/lib/shale/adapter/ox.rb +9 -134
  11. data/lib/shale/adapter/rexml/document.rb +88 -0
  12. data/lib/shale/adapter/rexml/node.rb +99 -0
  13. data/lib/shale/adapter/rexml.rb +9 -150
  14. data/lib/shale/error.rb +35 -2
  15. data/lib/shale/mapper.rb +2 -2
  16. data/lib/shale/schema/{json_compiler → compiler}/boolean.rb +1 -1
  17. data/lib/shale/schema/{json_compiler/object.rb → compiler/complex.rb} +11 -8
  18. data/lib/shale/schema/{json_compiler → compiler}/date.rb +1 -1
  19. data/lib/shale/schema/{json_compiler → compiler}/float.rb +1 -1
  20. data/lib/shale/schema/{json_compiler → compiler}/integer.rb +1 -1
  21. data/lib/shale/schema/{json_compiler → compiler}/property.rb +6 -6
  22. data/lib/shale/schema/{json_compiler → compiler}/string.rb +1 -1
  23. data/lib/shale/schema/{json_compiler → compiler}/time.rb +1 -1
  24. data/lib/shale/schema/compiler/value.rb +21 -0
  25. data/lib/shale/schema/compiler/xml_complex.rb +50 -0
  26. data/lib/shale/schema/compiler/xml_property.rb +73 -0
  27. data/lib/shale/schema/json_compiler.rb +32 -34
  28. data/lib/shale/schema/json_generator.rb +3 -3
  29. data/lib/shale/schema/xml_compiler.rb +919 -0
  30. data/lib/shale/schema/xml_generator.rb +7 -7
  31. data/lib/shale/schema.rb +16 -0
  32. data/lib/shale/type/{composite.rb → complex.rb} +20 -2
  33. data/lib/shale/utils.rb +42 -7
  34. data/lib/shale/version.rb +1 -1
  35. data/lib/shale.rb +8 -19
  36. data/shale.gemspec +1 -1
  37. metadata +23 -15
  38. data/lib/shale/schema/json_compiler/utils.rb +0 -52
  39. data/lib/shale/schema/json_compiler/value.rb +0 -13
@@ -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