schemaforge 0.1.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.
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchemaForge
4
+ class Resolver
5
+ def self.resolve(doc)
6
+ new(doc).resolve
7
+ end
8
+
9
+ def initialize(doc)
10
+ @doc = doc
11
+ @target_ns = doc.target_namespace
12
+ @promoted = []
13
+ @existing_names = doc.types.filter_map(&:name).to_set
14
+ end
15
+
16
+ def resolve
17
+ new_types = @doc.types.map { |t| promote_within(t, parent_name: t.name) }
18
+ new_elements = @doc.elements.map { |e| promote_in_element(e, parent_name: e.name) }
19
+ @doc = Schema::Document.new(
20
+ target_namespace: @doc.target_namespace,
21
+ namespaces: @doc.namespaces,
22
+ types: new_types + @promoted,
23
+ elements: new_elements,
24
+ groups: @doc.groups,
25
+ attribute_groups: @doc.attribute_groups,
26
+ imports: @doc.imports,
27
+ includes: @doc.includes
28
+ )
29
+ expand_groups_and_flatten_extensions
30
+ @types_by_qname = build_index
31
+ check_all_type_refs
32
+ @doc
33
+ end
34
+
35
+ private
36
+
37
+ def index_groups
38
+ @doc.groups.to_h { |g| [[@target_ns, g.name], g] }
39
+ end
40
+
41
+ def index_attribute_groups
42
+ @doc.attribute_groups.to_h { |ag| [[@target_ns, ag.name], ag] }
43
+ end
44
+
45
+ def expand_groups_and_flatten_extensions
46
+ groups_idx = index_groups
47
+ ag_idx = index_attribute_groups
48
+
49
+ new_types = @doc.types.map { |t| expand_in_type(t, groups_idx, ag_idx) }
50
+ new_types = new_types.map { |t| flatten_extension(t, new_types, ag_idx) }
51
+
52
+ @doc = Schema::Document.new(
53
+ target_namespace: @doc.target_namespace,
54
+ namespaces: @doc.namespaces,
55
+ types: new_types,
56
+ elements: @doc.elements,
57
+ groups: @doc.groups,
58
+ attribute_groups: @doc.attribute_groups,
59
+ imports: @doc.imports,
60
+ includes: @doc.includes
61
+ )
62
+ end
63
+
64
+ def expand_in_type(type, groups_idx, ag_idx)
65
+ return type unless type.is_a?(Schema::ComplexType)
66
+
67
+ new_attributes = type.attributes + ag_refs_to_attributes(type.attribute_groups, ag_idx)
68
+ new_content = expand_groups_in_content(type.content, groups_idx)
69
+
70
+ Schema::ComplexType.new(
71
+ name: type.name, anonymous: type.anonymous, base: type.base, derivation: type.derivation,
72
+ attributes: new_attributes, attribute_groups: [],
73
+ content: new_content, mixed: type.mixed, doc: type.doc
74
+ )
75
+ end
76
+
77
+ def ag_refs_to_attributes(refs, ag_idx)
78
+ refs.flat_map do |ref|
79
+ ag = ag_idx[[ref.ref.namespace, ref.ref.local_name]]
80
+ raise ResolveError.new("unresolved attributeGroup ref: #{ref.ref.local_name}", source: nil) unless ag
81
+
82
+ ag.attributes + ag_refs_to_attributes(ag.attribute_groups, ag_idx)
83
+ end
84
+ end
85
+
86
+ def expand_groups_in_content(content, groups_idx)
87
+ case content
88
+ when Schema::Sequence
89
+ Schema::Sequence.new(items: content.items.flat_map do |i|
90
+ expand_particle(i, groups_idx)
91
+ end, min_occurs: content.min_occurs, max_occurs: content.max_occurs)
92
+ when Schema::Choice
93
+ Schema::Choice.new(items: content.items.flat_map do |i|
94
+ expand_particle(i, groups_idx)
95
+ end, min_occurs: content.min_occurs, max_occurs: content.max_occurs)
96
+ else
97
+ content
98
+ end
99
+ end
100
+
101
+ def expand_particle(particle, groups_idx)
102
+ case particle
103
+ when Schema::GroupRef
104
+ g = groups_idx[[particle.ref.namespace, particle.ref.local_name]]
105
+ raise ResolveError.new("unresolved group ref: #{particle.ref.local_name}", source: nil) unless g
106
+
107
+ case g.content
108
+ when Schema::Sequence, Schema::Choice then g.content.items
109
+ else []
110
+ end
111
+ when Schema::Sequence, Schema::Choice
112
+ [expand_groups_in_content(particle, groups_idx)]
113
+ else
114
+ [particle]
115
+ end
116
+ end
117
+
118
+ def flatten_extension(type, all_types, ag_idx)
119
+ return type unless type.is_a?(Schema::ComplexType) && type.derivation == :extension && type.base
120
+
121
+ base = all_types.find { |t| t.is_a?(Schema::ComplexType) && t.name == type.base.local_name }
122
+ return type unless base # base is in another namespace or built-in (simpleContent extension)
123
+
124
+ base_flat = flatten_extension(base, all_types, ag_idx)
125
+ Schema::ComplexType.new(
126
+ name: type.name, anonymous: type.anonymous, base: type.base, derivation: type.derivation,
127
+ attributes: base_flat.attributes + type.attributes,
128
+ attribute_groups: type.attribute_groups,
129
+ content: merge_content(base_flat.content, type.content),
130
+ mixed: type.mixed,
131
+ doc: type.doc
132
+ )
133
+ end
134
+
135
+ def merge_content(base_content, derived_content)
136
+ return derived_content if base_content.nil?
137
+ return base_content if derived_content.nil?
138
+ if base_content.is_a?(Schema::Sequence) && derived_content.is_a?(Schema::Sequence)
139
+ return Schema::Sequence.new(items: base_content.items + derived_content.items, min_occurs: 1,
140
+ max_occurs: 1)
141
+ end
142
+
143
+ derived_content
144
+ end
145
+
146
+ def build_index
147
+ idx = {}
148
+ @doc.types.each { |t| idx[[@doc.target_namespace, t.name]] = t }
149
+ idx
150
+ end
151
+
152
+ # ----- Promotion -----
153
+
154
+ def promote_within(type, parent_name:)
155
+ case type
156
+ when Schema::ComplexType
157
+ new_attributes = type.attributes.map { |a| promote_in_attribute(a, parent_name: parent_name) }
158
+ new_content = promote_in_content(type.content, parent_name: parent_name)
159
+ Schema::ComplexType.new(
160
+ name: type.name, anonymous: type.anonymous, base: type.base, derivation: type.derivation,
161
+ attributes: new_attributes, attribute_groups: type.attribute_groups,
162
+ content: new_content, mixed: type.mixed, doc: type.doc
163
+ )
164
+ else
165
+ type
166
+ end
167
+ end
168
+
169
+ def promote_in_element(elem, parent_name:)
170
+ if elem.inline_type
171
+ promoted_name = synthesize_name(parent_name)
172
+ promoted = rename(elem.inline_type, promoted_name)
173
+ promoted = promote_within(promoted, parent_name: promoted_name)
174
+ @promoted << promoted
175
+ Schema::Element.new(
176
+ name: elem.name,
177
+ type_ref: Schema::TypeRef.new(namespace: @target_ns, local_name: promoted_name),
178
+ inline_type: nil,
179
+ min_occurs: elem.min_occurs, max_occurs: elem.max_occurs,
180
+ nillable: elem.nillable, default: elem.default, fixed: elem.fixed, doc: elem.doc
181
+ )
182
+ else
183
+ elem
184
+ end
185
+ end
186
+
187
+ def promote_in_attribute(attr, parent_name:)
188
+ if attr.inline_type
189
+ promoted_name = synthesize_name(parent_name, "#{pascal_case(attr.name)}Attr")
190
+ promoted = rename(attr.inline_type, promoted_name)
191
+ promoted = promote_within(promoted, parent_name: promoted_name)
192
+ @promoted << promoted
193
+ Schema::Attribute.new(
194
+ name: attr.name,
195
+ type_ref: Schema::TypeRef.new(namespace: @target_ns, local_name: promoted_name),
196
+ inline_type: nil,
197
+ use: attr.use, default: attr.default, fixed: attr.fixed, doc: attr.doc
198
+ )
199
+ else
200
+ attr
201
+ end
202
+ end
203
+
204
+ def promote_in_content(content, parent_name:)
205
+ case content
206
+ when Schema::Sequence
207
+ Schema::Sequence.new(
208
+ items: content.items.map { |i| promote_in_particle(i, parent_name: parent_name) },
209
+ min_occurs: content.min_occurs, max_occurs: content.max_occurs
210
+ )
211
+ when Schema::Choice
212
+ Schema::Choice.new(
213
+ items: content.items.map { |i| promote_in_particle(i, parent_name: parent_name) },
214
+ min_occurs: content.min_occurs, max_occurs: content.max_occurs
215
+ )
216
+ else
217
+ content
218
+ end
219
+ end
220
+
221
+ def promote_in_particle(particle, parent_name:)
222
+ case particle
223
+ when Schema::Element
224
+ promote_in_element(particle, parent_name: "#{parent_name}_#{pascal_case(particle.name)}")
225
+ when Schema::Sequence, Schema::Choice
226
+ promote_in_content(particle, parent_name: parent_name)
227
+ else
228
+ particle
229
+ end
230
+ end
231
+
232
+ def rename(type, new_name)
233
+ case type
234
+ when Schema::ComplexType
235
+ Schema::ComplexType.new(
236
+ name: new_name, anonymous: false, base: type.base, derivation: type.derivation,
237
+ attributes: type.attributes, attribute_groups: type.attribute_groups,
238
+ content: type.content, mixed: type.mixed, doc: type.doc
239
+ )
240
+ when Schema::SimpleType
241
+ Schema::SimpleType.new(name: new_name, anonymous: false, body: type.body, doc: type.doc)
242
+ end
243
+ end
244
+
245
+ def synthesize_name(*parts)
246
+ base = parts.compact.map { |p| pascal_case(p) }.join("_")
247
+ candidate = base
248
+ counter = 1
249
+ while @existing_names.include?(candidate)
250
+ counter += 1
251
+ candidate = "#{base}_#{counter}"
252
+ end
253
+ @existing_names << candidate
254
+ candidate
255
+ end
256
+
257
+ def pascal_case(name)
258
+ return "" if name.nil?
259
+
260
+ name.to_s.gsub(/[^A-Za-z0-9]+/, "_").split("_").map { |s| s[0].to_s.upcase + s[1..].to_s }.join
261
+ end
262
+
263
+ # ----- Validation walk (TypeRef checks) -----
264
+
265
+ def check_all_type_refs
266
+ @doc.types.each { |t| walk(t) }
267
+ @doc.elements.each { |e| walk_element(e) }
268
+ end
269
+
270
+ def walk(node)
271
+ case node
272
+ when Schema::ComplexType
273
+ check_ref(node.base) if node.base
274
+ node.attributes.each { |a| walk_attribute(a) }
275
+ walk_content(node.content) if node.content
276
+ when Schema::SimpleType
277
+ walk_simple_body(node.body) if node.body
278
+ end
279
+ end
280
+
281
+ def walk_content(node)
282
+ case node
283
+ when Schema::Sequence, Schema::Choice
284
+ node.items.each { |item| walk_particle(item) }
285
+ end
286
+ end
287
+
288
+ def walk_particle(node)
289
+ case node
290
+ when Schema::Element then walk_element(node)
291
+ when Schema::Sequence, Schema::Choice then walk_content(node)
292
+ when Schema::GroupRef, Schema::Any then nil
293
+ end
294
+ end
295
+
296
+ def walk_element(elem)
297
+ check_ref(elem.type_ref) if elem.type_ref
298
+ end
299
+
300
+ def walk_attribute(attr)
301
+ check_ref(attr.type_ref) if attr.type_ref
302
+ end
303
+
304
+ def walk_simple_body(body)
305
+ case body
306
+ when Schema::Restriction then check_restriction(body)
307
+ when Schema::List then check_list(body)
308
+ when Schema::Union then check_union(body)
309
+ end
310
+ end
311
+
312
+ def check_restriction(body)
313
+ check_ref(body.base) if body.base
314
+ end
315
+
316
+ def check_list(body)
317
+ check_ref(body.item_type) if body.item_type.is_a?(Schema::TypeRef)
318
+ end
319
+
320
+ def check_union(body)
321
+ body.member_types.each { |member| check_ref(member) if member.is_a?(Schema::TypeRef) }
322
+ end
323
+
324
+ def check_ref(ref)
325
+ return if ref.nil?
326
+ return if BuiltinTypes.builtin?(ref.namespace, ref.local_name)
327
+ return if @types_by_qname.key?([ref.namespace, ref.local_name])
328
+
329
+ raise ResolveError.new("unresolved type reference: {#{ref.namespace}}#{ref.local_name}", source: nil)
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchemaForge
4
+ module Schema
5
+ Document = Data.define(
6
+ :target_namespace, :namespaces, :types, :elements,
7
+ :groups, :attribute_groups, :imports, :includes
8
+ )
9
+
10
+ ComplexType = Data.define(
11
+ :name, :anonymous, :base, :derivation,
12
+ :attributes, :attribute_groups, :content, :mixed, :doc
13
+ )
14
+
15
+ SimpleType = Data.define(:name, :anonymous, :body, :doc)
16
+
17
+ Element = Data.define(
18
+ :name, :type_ref, :inline_type,
19
+ :min_occurs, :max_occurs, :nillable, :default, :fixed, :doc
20
+ )
21
+
22
+ Attribute = Data.define(
23
+ :name, :type_ref, :inline_type, :use, :default, :fixed, :doc
24
+ )
25
+
26
+ Sequence = Data.define(:items, :min_occurs, :max_occurs)
27
+ Choice = Data.define(:items, :min_occurs, :max_occurs)
28
+
29
+ Group = Data.define(:name, :content)
30
+ GroupRef = Data.define(:ref, :min_occurs, :max_occurs)
31
+ AttributeGroup = Data.define(:name, :attributes, :attribute_groups)
32
+ AttributeGroupRef = Data.define(:ref)
33
+
34
+ Restriction = Data.define(:base, :facets)
35
+ Extension = Data.define(:base, :content, :attributes, :attribute_groups)
36
+
37
+ List = Data.define(:item_type)
38
+ Union = Data.define(:member_types)
39
+
40
+ TypeRef = Data.define(:namespace, :local_name)
41
+
42
+ Any = Data.define(:namespace, :process_contents, :min_occurs, :max_occurs)
43
+
44
+ Import = Data.define(:namespace, :schema_location)
45
+ Include = Data.define(:schema_location)
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchemaForge
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module SchemaForge
6
+ module Writer
7
+ module_function
8
+
9
+ def write(output_dir, source_path, content)
10
+ FileUtils.mkdir_p(output_dir)
11
+ base = File.basename(source_path, ".*")
12
+ target = File.join(output_dir, "#{base}.rb")
13
+ File.write(target, content)
14
+ target
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schema_forge/version"
4
+ require_relative "schema_forge/errors"
5
+ require_relative "schema_forge/builtin_types"
6
+ require_relative "schema_forge/schema"
7
+ require_relative "schema_forge/parser"
8
+ require_relative "schema_forge/loader"
9
+ require_relative "schema_forge/resolver"
10
+ require_relative "schema_forge/naming"
11
+ require_relative "schema_forge/hooks"
12
+ require_relative "schema_forge/generator"
13
+ require_relative "schema_forge/writer"
14
+
15
+ module SchemaForge
16
+ module_function
17
+
18
+ def generate(input:, output:, module_name: "Schema", hooks: nil, fetch_remote: true)
19
+ each_xsd(input) do |path|
20
+ source = generate_one(path, module_name: module_name, hooks: hooks, fetch_remote: fetch_remote)
21
+ Writer.write(output, path, source)
22
+ end
23
+ end
24
+
25
+ def generate_string(input:, module_name: "Schema", hooks: nil, fetch_remote: true)
26
+ paths = enumerate_xsd(input)
27
+ raise ArgumentError, "generate_string requires a single XSD file, got: #{input}" if paths.size != 1
28
+
29
+ generate_one(paths.first, module_name: module_name, hooks: hooks, fetch_remote: fetch_remote)
30
+ end
31
+
32
+ def each_xsd(input, &)
33
+ enumerate_xsd(input).each(&)
34
+ end
35
+
36
+ def enumerate_xsd(input)
37
+ if File.directory?(input)
38
+ Dir[File.join(input, "*.xsd")]
39
+ else
40
+ [input]
41
+ end
42
+ end
43
+
44
+ def generate_one(path, module_name:, hooks:, fetch_remote:)
45
+ doc = Loader.load(path, fetch_remote: fetch_remote)
46
+ resolved = Resolver.resolve(doc)
47
+ Generator.new(module_name: module_name, hooks: hooks).emit(resolved)
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schemaforge
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - igbanam
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: nokogiri
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.16'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.16'
26
+ description: Parses XSD files and emits Ruby Data.define value objects with runtime
27
+ validation.
28
+ email:
29
+ - xigbanam@gmail.com
30
+ executables:
31
+ - schemaforge
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE.txt
36
+ - README.md
37
+ - exe/schemaforge
38
+ - lib/schema_forge.rb
39
+ - lib/schema_forge/builtin_types.rb
40
+ - lib/schema_forge/errors.rb
41
+ - lib/schema_forge/generator.rb
42
+ - lib/schema_forge/hooks.rb
43
+ - lib/schema_forge/loader.rb
44
+ - lib/schema_forge/naming.rb
45
+ - lib/schema_forge/parser.rb
46
+ - lib/schema_forge/resolver.rb
47
+ - lib/schema_forge/schema.rb
48
+ - lib/schema_forge/version.rb
49
+ - lib/schema_forge/writer.rb
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ rubygems_mfa_required: 'true'
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.2.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 4.0.10
69
+ specification_version: 4
70
+ summary: Generate Ruby Data.define value objects from XSD schemas.
71
+ test_files: []