xseed 1.0.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 +7 -0
- data/.github/workflows/rake.yml +16 -0
- data/.github/workflows/release.yml +25 -0
- data/.gitignore +72 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/.rubocop_todo.yml +432 -0
- data/CHANGELOG.adoc +446 -0
- data/Gemfile +21 -0
- data/LICENSE.adoc +29 -0
- data/README.adoc +386 -0
- data/Rakefile +11 -0
- data/examples/README.adoc +334 -0
- data/examples/advanced_usage.rb +286 -0
- data/examples/html_generation.rb +167 -0
- data/examples/parser_usage.rb +102 -0
- data/examples/schemas/person.xsd +171 -0
- data/examples/simple_generation.rb +149 -0
- data/exe/xseed +6 -0
- data/lib/xseed/cli.rb +376 -0
- data/lib/xseed/documentation/config.rb +101 -0
- data/lib/xseed/documentation/constants.rb +76 -0
- data/lib/xseed/documentation/generators/hierarchy_table_generator.rb +554 -0
- data/lib/xseed/documentation/generators/instance_sample_generator.rb +723 -0
- data/lib/xseed/documentation/generators/properties_table_generator.rb +983 -0
- data/lib/xseed/documentation/html_generator.rb +836 -0
- data/lib/xseed/documentation/html_generator.rb.bak +723 -0
- data/lib/xseed/documentation/presentation/css_generator.rb +510 -0
- data/lib/xseed/documentation/presentation/javascript_generator.rb +151 -0
- data/lib/xseed/documentation/presentation/navigation_builder.rb +169 -0
- data/lib/xseed/documentation/schema_loader.rb +121 -0
- data/lib/xseed/documentation/utils/helpers.rb +205 -0
- data/lib/xseed/documentation/utils/namespaces.rb +149 -0
- data/lib/xseed/documentation/utils/references.rb +135 -0
- data/lib/xseed/documentation/utils/strings.rb +75 -0
- data/lib/xseed/models/element_declaration.rb +144 -0
- data/lib/xseed/parser/xsd_parser.rb +192 -0
- data/lib/xseed/version.rb +5 -0
- data/lib/xseed.rb +76 -0
- data/xseed.gemspec +39 -0
- metadata +158 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require_relative "../config"
|
|
5
|
+
require_relative "../constants"
|
|
6
|
+
|
|
7
|
+
module Xseed
|
|
8
|
+
module Documentation
|
|
9
|
+
module Generators
|
|
10
|
+
# Generates XML instance samples for schema components
|
|
11
|
+
# Ported from XS3P instance-samples-*.xsl modules
|
|
12
|
+
class InstanceSampleGenerator
|
|
13
|
+
include Constants
|
|
14
|
+
|
|
15
|
+
attr_reader :component, :parser, :config
|
|
16
|
+
|
|
17
|
+
# Initialize the instance sample generator
|
|
18
|
+
#
|
|
19
|
+
# @param component [Nokogiri::XML::Element] Schema component
|
|
20
|
+
# @param parser [Xseed::Parser::XsdParser] XSD parser instance
|
|
21
|
+
# @param config [Config] Configuration options
|
|
22
|
+
def initialize(component, parser, config = Config.new)
|
|
23
|
+
raise ArgumentError, "Component cannot be nil" if component.nil?
|
|
24
|
+
raise ArgumentError, "Parser cannot be nil" if parser.nil?
|
|
25
|
+
|
|
26
|
+
@component = component
|
|
27
|
+
@parser = parser
|
|
28
|
+
@config = config
|
|
29
|
+
@indent_level = 0
|
|
30
|
+
@type_stack = [] # Track types to prevent infinite recursion
|
|
31
|
+
@group_stack = [] # Track groups to prevent infinite recursion
|
|
32
|
+
@recursion_depth = 0
|
|
33
|
+
@max_recursion_depth = 10
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Generate HTML with XML instance sample
|
|
37
|
+
#
|
|
38
|
+
# @return [String] HTML markup
|
|
39
|
+
def generate
|
|
40
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
|
41
|
+
xml.pre(class: "codehilite") do
|
|
42
|
+
xml << generate_xml_sample_with_highlighting
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
builder.doc.root.to_html
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Generate XML sample with syntax highlighting
|
|
51
|
+
#
|
|
52
|
+
# @return [String] HTML with highlighted XML
|
|
53
|
+
def generate_xml_sample_with_highlighting
|
|
54
|
+
xml_text = generate_xml_sample
|
|
55
|
+
highlight_xml(xml_text)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Generate XML sample based on component type
|
|
59
|
+
#
|
|
60
|
+
# @return [String] XML sample text
|
|
61
|
+
def generate_xml_sample
|
|
62
|
+
case component.name
|
|
63
|
+
when "element"
|
|
64
|
+
generate_element_sample
|
|
65
|
+
when "complexType"
|
|
66
|
+
generate_complex_type_sample
|
|
67
|
+
when "simpleType"
|
|
68
|
+
generate_simple_type_sample
|
|
69
|
+
else
|
|
70
|
+
"<!-- Unsupported component type: #{component.name} -->"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Apply syntax highlighting to XML
|
|
75
|
+
#
|
|
76
|
+
# @param xml [String] Plain XML text
|
|
77
|
+
# @return [String] HTML with syntax highlighting
|
|
78
|
+
def highlight_xml(xml)
|
|
79
|
+
result = xml.dup
|
|
80
|
+
|
|
81
|
+
# Highlight XML tags
|
|
82
|
+
result.gsub!(/<(\/?)([\w:]+)([^>]*)>/) do
|
|
83
|
+
tag_close = Regexp.last_match(1)
|
|
84
|
+
tag_name = Regexp.last_match(2)
|
|
85
|
+
rest = Regexp.last_match(3)
|
|
86
|
+
|
|
87
|
+
highlighted = "<span class=\"nt\"><#{tag_close}#{tag_name}"
|
|
88
|
+
highlighted += rest if rest && !rest.empty?
|
|
89
|
+
"#{highlighted}></span>"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Highlight type information
|
|
93
|
+
result.gsub!(/\b(xsd:\w+)\b/) do
|
|
94
|
+
type_name = Regexp.last_match(1)
|
|
95
|
+
"<span class=\"type\">#{type_name}</span>"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Highlight occurrence info [min..max]
|
|
99
|
+
result.gsub(/(\[[\d∞]+\.\.[\d∞]+\])/) do
|
|
100
|
+
"<span class=\"cs\">#{Regexp.last_match(1)}</span>"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Generate sample for element declaration
|
|
105
|
+
#
|
|
106
|
+
# @return [String] XML sample
|
|
107
|
+
def generate_element_sample
|
|
108
|
+
return "" if prohibited_element?
|
|
109
|
+
|
|
110
|
+
result = []
|
|
111
|
+
indent = " " * @indent_level
|
|
112
|
+
|
|
113
|
+
# Start tag with namespace
|
|
114
|
+
start_tag = "<#{element_tag}"
|
|
115
|
+
|
|
116
|
+
# Add namespace declaration for root element
|
|
117
|
+
if @indent_level.zero? && target_namespace
|
|
118
|
+
start_tag += " xmlns=\"#{target_namespace}\""
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Add attributes if complex type or has local complexType
|
|
122
|
+
type_node = nil
|
|
123
|
+
if component.at_xpath("xsd:complexType", "xsd" => XSD_NS)
|
|
124
|
+
type_node = component.at_xpath("xsd:complexType", "xsd" => XSD_NS)
|
|
125
|
+
elsif component["type"]
|
|
126
|
+
type_node = resolve_type
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if type_node
|
|
130
|
+
attrs = collect_attributes(type_node)
|
|
131
|
+
if attrs && !attrs.empty?
|
|
132
|
+
result << (indent + start_tag) unless start_tag.empty?
|
|
133
|
+
attrs.each do |attr|
|
|
134
|
+
result << "#{indent} #{attr}"
|
|
135
|
+
end
|
|
136
|
+
start_tag = ""
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Generate content
|
|
141
|
+
content = generate_element_content
|
|
142
|
+
|
|
143
|
+
if content.empty? && start_tag.end_with?('"')
|
|
144
|
+
# Self-closing tag with attributes
|
|
145
|
+
result << "#{indent}#{start_tag}/>" unless start_tag.empty?
|
|
146
|
+
elsif content.empty?
|
|
147
|
+
# Self-closing tag
|
|
148
|
+
result << "#{indent}#{start_tag}/>"
|
|
149
|
+
else
|
|
150
|
+
# Element with content
|
|
151
|
+
result << "#{indent}#{start_tag}>" if start_tag && !start_tag.empty?
|
|
152
|
+
result << content
|
|
153
|
+
result << "#{indent}</#{element_tag}>"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
add_occurrence_info(result)
|
|
157
|
+
result.join("\n")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Generate sample for complex type
|
|
161
|
+
#
|
|
162
|
+
# @return [String] XML sample
|
|
163
|
+
def generate_complex_type_sample
|
|
164
|
+
return "" unless component.name == "complexType"
|
|
165
|
+
|
|
166
|
+
type_name = component["name"]
|
|
167
|
+
return "" if circular_type?(type_name)
|
|
168
|
+
|
|
169
|
+
push_type(type_name) if type_name
|
|
170
|
+
|
|
171
|
+
result = []
|
|
172
|
+
indent = " " * @indent_level
|
|
173
|
+
|
|
174
|
+
# Show type structure
|
|
175
|
+
result << "#{indent}<!-- ComplexType: #{type_name} -->" if type_name
|
|
176
|
+
|
|
177
|
+
# Generate content based on content model
|
|
178
|
+
content_result = generate_type_content(component)
|
|
179
|
+
result << content_result unless content_result.empty?
|
|
180
|
+
|
|
181
|
+
pop_type if type_name
|
|
182
|
+
|
|
183
|
+
result.join("\n")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Generate sample for simple type
|
|
187
|
+
#
|
|
188
|
+
# @return [String] XML sample
|
|
189
|
+
def generate_simple_type_sample
|
|
190
|
+
type_name = component["name"]
|
|
191
|
+
restriction = component.at_xpath("xsd:restriction", "xsd" => XSD_NS)
|
|
192
|
+
|
|
193
|
+
return "<!-- SimpleType: #{type_name} -->" unless restriction
|
|
194
|
+
|
|
195
|
+
base_type = restriction["base"] || "string"
|
|
196
|
+
constraints = collect_simple_constraints(restriction)
|
|
197
|
+
|
|
198
|
+
if constraints.empty?
|
|
199
|
+
base_type
|
|
200
|
+
else
|
|
201
|
+
"#{base_type} (#{constraints.join(', ')})"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Generate content for element
|
|
206
|
+
#
|
|
207
|
+
# @return [String] Content text
|
|
208
|
+
def generate_element_content
|
|
209
|
+
return component["fixed"] if component["fixed"]
|
|
210
|
+
|
|
211
|
+
if component["type"]
|
|
212
|
+
type_node = resolve_type
|
|
213
|
+
unless type_node && type_node.name == "complexType"
|
|
214
|
+
return component["type"].split(":").last
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
@indent_level += 1
|
|
218
|
+
content = generate_type_content(type_node)
|
|
219
|
+
@indent_level -= 1
|
|
220
|
+
return content
|
|
221
|
+
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Local complex type
|
|
225
|
+
if (local_type = component.at_xpath("xsd:complexType",
|
|
226
|
+
"xsd" => XSD_NS))
|
|
227
|
+
@indent_level += 1
|
|
228
|
+
content = generate_type_content(local_type)
|
|
229
|
+
@indent_level -= 1
|
|
230
|
+
return content
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Local simple type
|
|
234
|
+
if (local_type = component.at_xpath("xsd:simpleType",
|
|
235
|
+
"xsd" => XSD_NS))
|
|
236
|
+
return generate_simple_type_content(local_type)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
"..."
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Generate content for complex type
|
|
243
|
+
#
|
|
244
|
+
# @param type [Nokogiri::XML::Element] Complex type node
|
|
245
|
+
# @return [String] Type content
|
|
246
|
+
def generate_type_content(type)
|
|
247
|
+
# Check for complexContent
|
|
248
|
+
if (complex_content = type.at_xpath("xsd:complexContent",
|
|
249
|
+
"xsd" => XSD_NS))
|
|
250
|
+
return generate_complex_content(complex_content)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Check for simpleContent
|
|
254
|
+
if (simple_content = type.at_xpath("xsd:simpleContent",
|
|
255
|
+
"xsd" => XSD_NS))
|
|
256
|
+
return generate_simple_content(simple_content)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Process model groups
|
|
260
|
+
%w[sequence choice all].each do |group_name|
|
|
261
|
+
if (group = type.at_xpath("xsd:#{group_name}", "xsd" => XSD_NS))
|
|
262
|
+
return generate_model_group(group)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
""
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Generate complex content (extension/restriction)
|
|
270
|
+
#
|
|
271
|
+
# @param complex_content [Nokogiri::XML::Element] complexContent node
|
|
272
|
+
# @return [String] Content
|
|
273
|
+
def generate_complex_content(complex_content)
|
|
274
|
+
parts = []
|
|
275
|
+
|
|
276
|
+
if (extension = complex_content.at_xpath("xsd:extension",
|
|
277
|
+
"xsd" => XSD_NS))
|
|
278
|
+
# Get base type content first
|
|
279
|
+
base_type_name = extension["base"]
|
|
280
|
+
if base_type_name
|
|
281
|
+
base_type = find_type(base_type_name)
|
|
282
|
+
if base_type && !circular_type?(strip_namespace(base_type_name))
|
|
283
|
+
push_type(strip_namespace(base_type_name))
|
|
284
|
+
parts << generate_type_content(base_type)
|
|
285
|
+
pop_type
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Add extension content
|
|
290
|
+
%w[sequence choice all].each do |group_name|
|
|
291
|
+
if (group = extension.at_xpath("xsd:#{group_name}",
|
|
292
|
+
"xsd" => XSD_NS))
|
|
293
|
+
parts << generate_model_group(group)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
elsif (restriction = complex_content.at_xpath("xsd:restriction",
|
|
297
|
+
"xsd" => XSD_NS))
|
|
298
|
+
# For restriction, show only the restricted content
|
|
299
|
+
%w[sequence choice all].each do |group_name|
|
|
300
|
+
if (group = restriction.at_xpath("xsd:#{group_name}",
|
|
301
|
+
"xsd" => XSD_NS))
|
|
302
|
+
parts << generate_model_group(group)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
parts.reject(&:empty?).join("\n")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Generate simple content
|
|
311
|
+
#
|
|
312
|
+
# @param simple_content [Nokogiri::XML::Element] simpleContent node
|
|
313
|
+
# @return [String] Content
|
|
314
|
+
def generate_simple_content(simple_content)
|
|
315
|
+
if (extension = simple_content.at_xpath("xsd:extension",
|
|
316
|
+
"xsd" => XSD_NS))
|
|
317
|
+
base = extension["base"]
|
|
318
|
+
return base ? strip_namespace(base) : "string"
|
|
319
|
+
elsif (restriction = simple_content.at_xpath("xsd:restriction",
|
|
320
|
+
"xsd" => XSD_NS))
|
|
321
|
+
return generate_simple_restriction(restriction)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
"string"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Generate model group (sequence/choice/all)
|
|
328
|
+
#
|
|
329
|
+
# @param group [Nokogiri::XML::Element] Model group node
|
|
330
|
+
# @return [String] Group content
|
|
331
|
+
def generate_model_group(group)
|
|
332
|
+
return "" if group["maxOccurs"] == "0"
|
|
333
|
+
|
|
334
|
+
# Check recursion depth
|
|
335
|
+
@recursion_depth += 1
|
|
336
|
+
if @recursion_depth > @max_recursion_depth
|
|
337
|
+
@recursion_depth -= 1
|
|
338
|
+
indent = " " * @indent_level
|
|
339
|
+
return "#{indent}<!-- Max recursion depth reached -->"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
result = []
|
|
343
|
+
indent = " " * @indent_level
|
|
344
|
+
group_name = group.name.capitalize
|
|
345
|
+
|
|
346
|
+
# Show group indicators for choice and occurrence > 1
|
|
347
|
+
show_group = group.name == "choice" ||
|
|
348
|
+
(group["minOccurs"] && group["minOccurs"] != "1") ||
|
|
349
|
+
(group["maxOccurs"] && group["maxOccurs"] != "1")
|
|
350
|
+
|
|
351
|
+
if show_group
|
|
352
|
+
result << "#{indent}<!-- Start #{group_name} #{format_occurs(group)} -->"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Process child elements
|
|
356
|
+
@indent_level += 1 if show_group
|
|
357
|
+
|
|
358
|
+
group.xpath("xsd:element", "xsd" => XSD_NS).each do |elem|
|
|
359
|
+
result << generate_child_element(elem)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
group.xpath("xsd:group", "xsd" => XSD_NS).each do |grp_ref|
|
|
363
|
+
result << generate_group_reference(grp_ref)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
@indent_level -= 1 if show_group
|
|
367
|
+
|
|
368
|
+
result << "#{indent}<!-- End #{group_name} -->" if show_group
|
|
369
|
+
|
|
370
|
+
@recursion_depth -= 1
|
|
371
|
+
result.reject(&:empty?).join("\n")
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Generate child element within model group
|
|
375
|
+
#
|
|
376
|
+
# @param elem [Nokogiri::XML::Element] Element node
|
|
377
|
+
# @return [String] Element sample
|
|
378
|
+
def generate_child_element(elem)
|
|
379
|
+
return "" if elem["maxOccurs"] == "0"
|
|
380
|
+
|
|
381
|
+
indent = " " * @indent_level
|
|
382
|
+
elem_name = elem["name"] || strip_namespace(elem["ref"] || "element")
|
|
383
|
+
|
|
384
|
+
# Simple content with type
|
|
385
|
+
if elem["type"]
|
|
386
|
+
type_value = strip_namespace(elem["type"])
|
|
387
|
+
occurs = format_occurs(elem)
|
|
388
|
+
return "#{indent}<#{elem_name}>#{type_value}</#{elem_name}> #{occurs}".rstrip
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Complex type
|
|
392
|
+
if (local_type = elem.at_xpath("xsd:complexType", "xsd" => XSD_NS))
|
|
393
|
+
result = []
|
|
394
|
+
result << "#{indent}<#{elem_name}>"
|
|
395
|
+
@indent_level += 1
|
|
396
|
+
content = generate_type_content(local_type)
|
|
397
|
+
result << content unless content.empty?
|
|
398
|
+
@indent_level -= 1
|
|
399
|
+
occurs = format_occurs(elem)
|
|
400
|
+
result << "#{indent}</#{elem_name}> #{occurs}".rstrip
|
|
401
|
+
return result.join("\n")
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Simple element
|
|
405
|
+
occurs = format_occurs(elem)
|
|
406
|
+
"#{indent}<#{elem_name}>...</#{elem_name}> #{occurs}".rstrip
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Generate group reference
|
|
410
|
+
#
|
|
411
|
+
# @param grp_ref [Nokogiri::XML::Element] Group reference node
|
|
412
|
+
# @return [String] Group content
|
|
413
|
+
def generate_group_reference(grp_ref)
|
|
414
|
+
ref_name = strip_namespace(grp_ref["ref"])
|
|
415
|
+
|
|
416
|
+
# Check for circular group reference
|
|
417
|
+
if @group_stack.include?(ref_name)
|
|
418
|
+
indent = " " * @indent_level
|
|
419
|
+
return "#{indent}<!-- Circular reference to group #{ref_name} -->"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
group_def = find_group(ref_name)
|
|
423
|
+
return "<!-- Group reference: #{ref_name} -->" unless group_def
|
|
424
|
+
|
|
425
|
+
# Track group to prevent circular references
|
|
426
|
+
@group_stack.push(ref_name)
|
|
427
|
+
|
|
428
|
+
# Find the model group within the group definition
|
|
429
|
+
result = ""
|
|
430
|
+
%w[sequence choice all].each do |group_name|
|
|
431
|
+
if (group = group_def.at_xpath("xsd:#{group_name}",
|
|
432
|
+
"xsd" => XSD_NS))
|
|
433
|
+
result = generate_model_group(group)
|
|
434
|
+
break
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
@group_stack.pop
|
|
439
|
+
result
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Collect attributes from type
|
|
443
|
+
#
|
|
444
|
+
# @param type [Nokogiri::XML::Element] Type node
|
|
445
|
+
# @return [Array<String>] Attribute samples
|
|
446
|
+
def collect_attributes(type)
|
|
447
|
+
return [] unless type
|
|
448
|
+
|
|
449
|
+
attrs = []
|
|
450
|
+
|
|
451
|
+
# Check simpleContent/extension for attributes
|
|
452
|
+
if (simple_content = type.at_xpath("xsd:simpleContent",
|
|
453
|
+
"xsd" => XSD_NS))
|
|
454
|
+
if (extension = simple_content.at_xpath("xsd:extension",
|
|
455
|
+
"xsd" => XSD_NS))
|
|
456
|
+
attrs.concat(collect_direct_attributes(extension))
|
|
457
|
+
elsif (restriction = simple_content.at_xpath("xsd:restriction",
|
|
458
|
+
"xsd" => XSD_NS))
|
|
459
|
+
attrs.concat(collect_direct_attributes(restriction))
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Check complexContent/extension for attributes
|
|
464
|
+
if (complex_content = type.at_xpath("xsd:complexContent",
|
|
465
|
+
"xsd" => XSD_NS))
|
|
466
|
+
if (extension = complex_content.at_xpath("xsd:extension",
|
|
467
|
+
"xsd" => XSD_NS))
|
|
468
|
+
# Get base type attributes first
|
|
469
|
+
base_type_name = extension["base"]
|
|
470
|
+
if base_type_name
|
|
471
|
+
base_type = find_type(base_type_name)
|
|
472
|
+
if base_type && !circular_type?(strip_namespace(base_type_name))
|
|
473
|
+
attrs.concat(collect_attributes(base_type))
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
attrs.concat(collect_direct_attributes(extension))
|
|
477
|
+
elsif (restriction = complex_content.at_xpath("xsd:restriction",
|
|
478
|
+
"xsd" => XSD_NS))
|
|
479
|
+
attrs.concat(collect_direct_attributes(restriction))
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Direct attributes (for types without simpleContent/complexContent)
|
|
484
|
+
attrs.concat(collect_direct_attributes(type))
|
|
485
|
+
|
|
486
|
+
attrs
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Collect direct attributes from a node
|
|
490
|
+
#
|
|
491
|
+
# @param node [Nokogiri::XML::Element] Node to search
|
|
492
|
+
# @return [Array<String>] Attribute samples
|
|
493
|
+
def collect_direct_attributes(node)
|
|
494
|
+
attrs = []
|
|
495
|
+
|
|
496
|
+
# Direct attributes
|
|
497
|
+
node.xpath("xsd:attribute", "xsd" => XSD_NS).each do |attr|
|
|
498
|
+
next if attr["use"] == "prohibited"
|
|
499
|
+
|
|
500
|
+
attr_name = attr["name"] || strip_namespace(attr["ref"] || "attr")
|
|
501
|
+
attr_value = attr["fixed"] || attr["type"] || "string"
|
|
502
|
+
attr_value = strip_namespace(attr_value)
|
|
503
|
+
|
|
504
|
+
use_indicator = attr["use"] == "required" ? "" : "?"
|
|
505
|
+
attrs << "#{attr_name}=\"#{attr_value}\" #{use_indicator}".rstrip
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Attribute groups
|
|
509
|
+
node.xpath("xsd:attributeGroup", "xsd" => XSD_NS).each do |attr_grp|
|
|
510
|
+
ref_name = strip_namespace(attr_grp["ref"])
|
|
511
|
+
grp_def = find_attribute_group(ref_name)
|
|
512
|
+
attrs.concat(collect_attributes(grp_def)) if grp_def
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
attrs
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Collect simple type constraints
|
|
519
|
+
#
|
|
520
|
+
# @param restriction [Nokogiri::XML::Element] Restriction node
|
|
521
|
+
# @return [Array<String>] Constraint descriptions
|
|
522
|
+
def collect_simple_constraints(restriction)
|
|
523
|
+
constraints = []
|
|
524
|
+
|
|
525
|
+
# Enumeration
|
|
526
|
+
enums = restriction.xpath("xsd:enumeration", "xsd" => XSD_NS)
|
|
527
|
+
if enums.any?
|
|
528
|
+
values = enums.map { |e| e["value"] }.join("|")
|
|
529
|
+
constraints << "enumeration: #{values}"
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Pattern
|
|
533
|
+
if (pattern = restriction.at_xpath("xsd:pattern", "xsd" => XSD_NS))
|
|
534
|
+
constraints << "pattern: #{pattern['value']}"
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Length constraints
|
|
538
|
+
if (min_len = restriction.at_xpath("xsd:minLength", "xsd" => XSD_NS))
|
|
539
|
+
constraints << "minLength: #{min_len['value']}"
|
|
540
|
+
end
|
|
541
|
+
if (max_len = restriction.at_xpath("xsd:maxLength", "xsd" => XSD_NS))
|
|
542
|
+
constraints << "maxLength: #{max_len['value']}"
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Range constraints
|
|
546
|
+
if (min_inc = restriction.at_xpath("xsd:minInclusive",
|
|
547
|
+
"xsd" => XSD_NS))
|
|
548
|
+
constraints << "min: #{min_inc['value']}"
|
|
549
|
+
end
|
|
550
|
+
if (max_inc = restriction.at_xpath("xsd:maxInclusive",
|
|
551
|
+
"xsd" => XSD_NS))
|
|
552
|
+
constraints << "max: #{max_inc['value']}"
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
constraints
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Generate simple restriction content
|
|
559
|
+
#
|
|
560
|
+
# @param restriction [Nokogiri::XML::Element] Restriction node
|
|
561
|
+
# @return [String] Content
|
|
562
|
+
def generate_simple_restriction(restriction)
|
|
563
|
+
base = strip_namespace(restriction["base"] || "string")
|
|
564
|
+
constraints = collect_simple_constraints(restriction)
|
|
565
|
+
|
|
566
|
+
if constraints.empty?
|
|
567
|
+
base
|
|
568
|
+
else
|
|
569
|
+
"#{base} (#{constraints.join(', ')})"
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Generate simple type content
|
|
574
|
+
#
|
|
575
|
+
# @param simple_type [Nokogiri::XML::Element] Simple type node
|
|
576
|
+
# @return [String] Content
|
|
577
|
+
def generate_simple_type_content(simple_type)
|
|
578
|
+
if (restriction = simple_type.at_xpath("xsd:restriction",
|
|
579
|
+
"xsd" => XSD_NS))
|
|
580
|
+
return generate_simple_restriction(restriction)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
"string"
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Format occurrence information
|
|
587
|
+
#
|
|
588
|
+
# @param node [Nokogiri::XML::Element] Node with occurrence attributes
|
|
589
|
+
# @return [String] Formatted occurrence
|
|
590
|
+
def format_occurs(node)
|
|
591
|
+
min = node["minOccurs"] || "1"
|
|
592
|
+
max = node["maxOccurs"] || "1"
|
|
593
|
+
|
|
594
|
+
return "" if min == "1" && max == "1"
|
|
595
|
+
|
|
596
|
+
max = "∞" if max == "unbounded"
|
|
597
|
+
"[#{min}..#{max}]"
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Add occurrence info to result
|
|
601
|
+
#
|
|
602
|
+
# @param result [Array<String>] Result array
|
|
603
|
+
def add_occurrence_info(result)
|
|
604
|
+
return if global_component?
|
|
605
|
+
|
|
606
|
+
occurs = format_occurs(component)
|
|
607
|
+
return if occurs.empty?
|
|
608
|
+
|
|
609
|
+
result[-1] = "#{result[-1]} #{occurs}" if result.any?
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Get element tag with namespace prefix if needed
|
|
613
|
+
#
|
|
614
|
+
# @return [String] Element tag
|
|
615
|
+
def element_tag
|
|
616
|
+
component["name"] || strip_namespace(component["ref"] || "element")
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Resolve type reference to type definition
|
|
620
|
+
#
|
|
621
|
+
# @return [Nokogiri::XML::Element, nil] Type node
|
|
622
|
+
def resolve_type
|
|
623
|
+
type_ref = component["type"]
|
|
624
|
+
return nil unless type_ref
|
|
625
|
+
|
|
626
|
+
find_type(type_ref)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Find type definition by name
|
|
630
|
+
#
|
|
631
|
+
# @param type_name [String] Type name
|
|
632
|
+
# @return [Nokogiri::XML::Element, nil] Type node
|
|
633
|
+
def find_type(type_name)
|
|
634
|
+
local_name = strip_namespace(type_name)
|
|
635
|
+
|
|
636
|
+
# Check complex types
|
|
637
|
+
parser.complex_types.find { |t| t["name"] == local_name } ||
|
|
638
|
+
# Check simple types
|
|
639
|
+
parser.simple_types.find { |t| t["name"] == local_name }
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Find group definition by name
|
|
643
|
+
#
|
|
644
|
+
# @param group_name [String] Group name
|
|
645
|
+
# @return [Nokogiri::XML::Element, nil] Group node
|
|
646
|
+
def find_group(group_name)
|
|
647
|
+
parser.groups.find { |g| g["name"] == group_name }
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Find attribute group definition by name
|
|
651
|
+
#
|
|
652
|
+
# @param group_name [String] Attribute group name
|
|
653
|
+
# @return [Nokogiri::XML::Element, nil] Attribute group node
|
|
654
|
+
def find_attribute_group(group_name)
|
|
655
|
+
parser.attribute_groups.find { |ag| ag["name"] == group_name }
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Check if element has complex type
|
|
659
|
+
#
|
|
660
|
+
# @return [Boolean]
|
|
661
|
+
def has_complex_type?
|
|
662
|
+
return true if component.at_xpath("xsd:complexType", "xsd" => XSD_NS)
|
|
663
|
+
return false unless component["type"]
|
|
664
|
+
|
|
665
|
+
type_node = resolve_type
|
|
666
|
+
type_node && type_node.name == "complexType"
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Check if element is prohibited
|
|
670
|
+
#
|
|
671
|
+
# @return [Boolean]
|
|
672
|
+
def prohibited_element?
|
|
673
|
+
component["maxOccurs"] == "0"
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Check if component is global
|
|
677
|
+
#
|
|
678
|
+
# @return [Boolean]
|
|
679
|
+
def global_component?
|
|
680
|
+
component.parent&.name == "schema"
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Get target namespace
|
|
684
|
+
#
|
|
685
|
+
# @return [String, nil] Target namespace
|
|
686
|
+
def target_namespace
|
|
687
|
+
schema = component.document.root
|
|
688
|
+
schema["targetNamespace"] if schema
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Strip namespace prefix from name
|
|
692
|
+
#
|
|
693
|
+
# @param name [String] Qualified name
|
|
694
|
+
# @return [String] Local name
|
|
695
|
+
def strip_namespace(name)
|
|
696
|
+
name.to_s.split(":").last
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# Check if type is circular
|
|
700
|
+
#
|
|
701
|
+
# @param type_name [String] Type name
|
|
702
|
+
# @return [Boolean]
|
|
703
|
+
def circular_type?(type_name)
|
|
704
|
+
return false unless type_name
|
|
705
|
+
|
|
706
|
+
@type_stack.include?(type_name)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Push type onto stack
|
|
710
|
+
#
|
|
711
|
+
# @param type_name [String] Type name
|
|
712
|
+
def push_type(type_name)
|
|
713
|
+
@type_stack.push(type_name) if type_name
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Pop type from stack
|
|
717
|
+
def pop_type
|
|
718
|
+
@type_stack.pop
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
end
|