xsd 2.8.0 → 2.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40e6cbfb1356c6d21529ad7339f3ada85dbf85be27d7d0fcd3ecd59e55250192
4
- data.tar.gz: 544ea96a3cc35ac6a37f8ab8971ae7d30c1b4a89c11c80ae6475ad1fcc61a6ae
3
+ metadata.gz: 6dafeffb7f25c0134087002cfa7a33a2ae26bfb5d7aefc83dd998f73ce6551ce
4
+ data.tar.gz: 309a398a467318eb24d8912556010c11cba92f72419088b709540775dc270e00
5
5
  SHA512:
6
- metadata.gz: 6fbc961bab3eac175a05dd99a8cbb34dce5384f682def039f708407f281cbbbe75f5ec6a57a751fff2c8fbad927fd83edf2b1e8e488f255655777a0ce7cf36d0
7
- data.tar.gz: 4f8c2fa9687ed1f4647e91884abd60d63be92fcbda2475cc3b0aa3b340f9ca6ae7fbe12d91343389de1cd0616a676c45e018e88e8dcbe557d7014ce24c02d227
6
+ metadata.gz: 292b2948f27d88c4363149e41aabca8858cd3c9105b804c5e85231135f1deb9d1a2bab10a6868839721dfd866b75a6579aca43d9243ca115dd752597cbaa7d81
7
+ data.tar.gz: cdc5245afa7c4fd48064b8c6ddac6966d8e0010d8a98e23fc59b43eb3df98e2159f4912db2e4ca3c5610e7713f0af1d525b6137e1c5c74edd38b8d87896c0717
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ ## [2.9.0] - 2026-10-18
2
+
3
+ ### Changed
4
+
5
+ - A schema with no target namespace included into one that has a namespace ("chameleon" include) is seen through
6
+ a view in the including namespace (`XML#chameleon`, `Schema#chameleon?`, `#chameleon_source`). Everything it
7
+ declares reports that namespace (`namespace`, `absolute_name`, `Schema#target_namespace`), and an unqualified
8
+ name inside it resolves in that namespace. Before, its definitions reported no namespace at all.
9
+ - A schema included into two namespaces declares two sets of definitions, one per namespace: a view keeps its own
10
+ object cache (`Schema#object_cache`), where one object per node used to serve both.
11
+ - `XML#schemas_for_namespace` returns what the schemas of the namespace include as well, chameleon views among
12
+ them, and so does the lookup of a referenced name: includes of includes, and what a schema reached through an
13
+ import includes (only the schema itself, or one level of includes, was searched). A schema only seen through a
14
+ chameleon view is no longer among those of the absent namespace
15
+ - `XML#[]` and the collectors (`elements`, `complex_types`, `simple_types`, …) go through the schemas that stand
16
+ on their own (`XML#standalone_schemas`): a schema only seen through a chameleon view contributes its
17
+ definitions through the schemas including it, not a second time in the absent namespace
18
+
19
+ ### Added
20
+
21
+ - `XML#target_namespaces`: every namespace the schemas declare definitions in, includes resolved; the absent
22
+ namespace (nil) only for a schema with no namespace that nothing includes
23
+
24
+ ## [2.8.1] - 2026-10-18
25
+
26
+ ### Fixed
27
+
28
+ - An import/include location written with backslashes (`import\types.xsd`) is resolved as one written with
29
+ slashes. A `schemaLocation` is a URI reference, but schemas authored on Windows carry such locations; a custom
30
+ resource resolver is handed the location so normalized
31
+
1
32
  ## [2.8.0] - 2026-09-21
2
33
 
3
34
  ### Fixed
@@ -85,9 +85,10 @@ module XSD
85
85
  ns = prefix || ns_or_prefix
86
86
 
87
87
  if schema.targets_namespace?(ns)
88
- [schema, *schema.includes.map(&:imported_schema)]
88
+ schema.with_includes
89
89
  elsif (import = schema.import_by_namespace(ns))
90
- [import.imported_schema]
90
+ # what the imported schema includes is part of its namespace too
91
+ import.imported_schema.with_includes
91
92
  else
92
93
  raise Error, "Schema not found for namespace '#{ns}' in '#{schema.id || schema.target_namespace}'"
93
94
  end
@@ -141,14 +142,15 @@ module XSD
141
142
  # @param [Nokogiri::XML::Node] node
142
143
  # @return BaseObject
143
144
  def node_to_object(node)
144
- # check object in cache first
145
- cached = reader.object_cache[node]
145
+ # check object in cache first; a chameleon view keeps a cache of its own (see Schema#object_cache)
146
+ cache = schema.object_cache
147
+ cached = cache[node]
146
148
  return cached if cached
147
149
 
148
150
  klass = XML::CLASS_MAP[node.name]
149
151
  raise Error, "Object class not found for '#{node.name}'" unless klass
150
152
 
151
- reader.object_cache[node] = klass.new(options.merge(node: node, schema: schema))
153
+ cache[node] = klass.new(options.merge(node: node, schema: schema))
152
154
  end
153
155
 
154
156
  # Get xml parent object
@@ -17,7 +17,12 @@ module XSD
17
17
 
18
18
  new_schema = reader.load_schema(schema_location, base: schema.location)
19
19
 
20
- # an included schema without a target namespace adopts the namespace of the including schema ("chameleon" include)
20
+ # an included schema without a target namespace adopts the namespace of the including schema ("chameleon"
21
+ # include): what it declares belongs to that namespace, so it is seen through a view that says so
22
+ if new_schema.target_namespace.nil? && !schema.target_namespace.nil?
23
+ return reader.chameleon(new_schema, schema.target_namespace)
24
+ end
25
+
21
26
  unless new_schema.target_namespace.nil? || schema.target_namespace == new_schema.target_namespace
22
27
  raise ImportError, "Schema targetNamespace '#{schema.target_namespace}' does not match included schema " \
23
28
  "targetNamespace '#{new_schema.target_namespace}'"
@@ -99,6 +99,47 @@ module XSD
99
99
  self
100
100
  end
101
101
 
102
+ # Target namespace of the schema. For a chameleon view (see XML#chameleon), the namespace of the schema
103
+ # that includes it — the one everything it declares belongs to.
104
+ # @return String, nil
105
+ def target_namespace
106
+ return options[:chameleon_namespace] if chameleon?
107
+
108
+ method_missing(:target_namespace)
109
+ end
110
+
111
+ # Is this a schema with no target namespace, seen as part of the namespace of a schema including it?
112
+ # @return Boolean
113
+ def chameleon?
114
+ options.key?(:chameleon_namespace)
115
+ end
116
+
117
+ # The schema a chameleon view is a view of
118
+ # @return Schema, nil
119
+ def chameleon_source
120
+ options[:chameleon_source]
121
+ end
122
+
123
+ # Objects made of this schema's nodes. A chameleon view keeps its own: the same node is a different
124
+ # definition in each namespace that includes it.
125
+ # @return Hash
126
+ def object_cache
127
+ chameleon? ? (@object_cache ||= {}.compare_by_identity) : reader.object_cache
128
+ end
129
+
130
+ # This schema and every schema it includes, those included in turn too
131
+ # @return Array<Schema>
132
+ def with_includes
133
+ found = [self]
134
+ found.each do |schema|
135
+ schema.includes.each do |include|
136
+ included = include.imported_schema
137
+ found << included unless found.any? { |known| known.equal?(included) }
138
+ end
139
+ end
140
+ found
141
+ end
142
+
102
143
  # Get all available elements on the current stack level, for schema same as elements
103
144
  # @return Array<Element>
104
145
  def collect_elements(*)
@@ -133,15 +174,17 @@ module XSD
133
174
  # @param [String, nil] namespace
134
175
  # @return Boolean
135
176
  def targets_namespace?(namespace)
136
- namespace == target_namespace || namespaces[namespace.nil? ? 'xmlns' : "xmlns:#{namespace}"] == target_namespace
177
+ namespace == target_namespace || namespaces[namespace.nil? ? 'xmlns' : "xmlns:#{namespace}"] == target_namespace ||
178
+ # an unqualified name in a chameleon schema names something of the namespace it was included into
179
+ (chameleon? && namespace.nil? && !namespaces.key?('xmlns'))
137
180
  end
138
181
 
139
182
  # Override map_children on schema to get objects from all loaded schemas
140
183
  # @param [Symbol] name
141
184
  # @param [Hash] cache Schema nodes already visited, guards against import/include cycles
142
185
  # @return Array<BaseObject>
143
- def map_children(name, cache = {})
144
- cache[node] = true
186
+ def map_children(name, cache = {}.compare_by_identity)
187
+ cache[self] = true
145
188
  super(name) + import_map_children(name, cache)
146
189
  end
147
190
 
@@ -154,7 +197,7 @@ module XSD
154
197
 
155
198
  (includes + imports).flat_map do |import|
156
199
  imported = import.imported_schema
157
- next [] if cache.key?(imported.node)
200
+ next [] if cache.key?(imported)
158
201
 
159
202
  imported.map_children(name, cache)
160
203
  end
data/lib/xsd/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module XSD
4
- VERSION = '2.8.0'
4
+ VERSION = '2.9.0'
5
5
  end
data/lib/xsd/xml.rb CHANGED
@@ -85,6 +85,7 @@ module XSD
85
85
  @object_cache = {}.compare_by_identity
86
86
  @schemas = []
87
87
  @schemas_by_location = {}
88
+ @chameleons = {}
88
89
  end
89
90
 
90
91
  def logger
@@ -130,7 +131,12 @@ module XSD
130
131
  new_schema
131
132
  end
132
133
 
133
- # Load schema referenced by an import/include, reusing an already loaded one when possible
134
+ # Load schema referenced by an import/include, reusing an already loaded one when possible.
135
+ #
136
+ # A location written with backslashes (`import\types.xsd`) is read as one written with slashes: a
137
+ # `schemaLocation` is a URI reference, where the separator is `/` alone, but schemas authored on Windows
138
+ # carry such locations, and the tools they were made with accept them. A custom resolver is handed the
139
+ # location so normalized.
134
140
  # @param [String] location
135
141
  # @param [String, nil] namespace
136
142
  # @param [String, nil] base Location of the referencing schema
@@ -138,6 +144,7 @@ module XSD
138
144
  def load_schema(location, namespace: nil, base: nil)
139
145
  raise ImportError, 'Schema location must be provided' if location.nil? || location.empty?
140
146
 
147
+ location = location.tr('\\', '/')
141
148
  expanded = expand_location(location, base)
142
149
  return @schemas_by_location[expanded] if @schemas_by_location.key?(expanded)
143
150
 
@@ -157,15 +164,55 @@ module XSD
157
164
  schemas.first
158
165
  end
159
166
 
160
- # Get schemas by namespace
167
+ # Get schemas by namespace: those that target it, and what they include — a chameleon schema (one with no
168
+ # target namespace of its own) as a view in this namespace
161
169
  # @param [String, nil] namespace
162
170
  # @return Array<Schema>
163
171
  def schemas_for_namespace(namespace)
164
- schemas.select { |schema| schema.target_namespace == namespace }
172
+ found = []
173
+ # a schema only seen through a chameleon view is not one of the absent namespace
174
+ standalone_schemas.select { |schema| schema.target_namespace == namespace }.each do |schema|
175
+ schema.with_includes.each { |included| found << included unless found.any? { |known| known.equal?(included) } }
176
+ end
177
+ found
178
+ end
179
+
180
+ # A schema with no target namespace, seen as part of the namespace of a schema that includes it ("chameleon"
181
+ # include): the same document, with everything it declares in that namespace. One view per schema and
182
+ # namespace, so a schema included into two namespaces declares two sets of definitions.
183
+ # @param [Schema] source
184
+ # @param [String] namespace
185
+ # @return Schema
186
+ def chameleon(source, namespace)
187
+ @chameleons[[source.object_id, namespace]] ||=
188
+ Schema.new(source.options.merge(chameleon_namespace: namespace, chameleon_source: source))
189
+ end
190
+
191
+ # The chameleon views made so far
192
+ # @return Array<Schema>
193
+ def chameleons
194
+ @chameleons.values
195
+ end
196
+
197
+ # Every namespace the schemas declare definitions in, includes resolved. A schema with no target namespace
198
+ # stands for the absent namespace (nil) only when no schema includes it: otherwise what it declares is part
199
+ # of the namespaces including it.
200
+ # @return Array<String, nil>
201
+ def target_namespaces
202
+ (standalone_schemas + chameleons).map(&:target_namespace).uniq
203
+ end
204
+
205
+ # The schemas that stand on their own, includes resolved: all but those only seen through a chameleon view,
206
+ # whose definitions belong to the namespaces including them
207
+ # @return Array<Schema>
208
+ def standalone_schemas
209
+ schemas.each(&:with_includes)
210
+ sources = chameleons.map(&:chameleon_source)
211
+ schemas.reject { |schema| sources.any? { |source| source.equal?(schema) } }
165
212
  end
166
213
 
167
214
  def [](*args)
168
- schemas.each do |schema|
215
+ standalone_schemas.each do |schema|
169
216
  item = schema[*args]
170
217
  return item if item
171
218
  end
@@ -226,7 +273,7 @@ module XSD
226
273
  end
227
274
 
228
275
  def collect(name, *args)
229
- schemas.flat_map { |schema| schema.send(name, *args) }.uniq
276
+ standalone_schemas.flat_map { |schema| schema.send(name, *args) }.uniq
230
277
  end
231
278
 
232
279
  def default_logger
data/xsd.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/xsd/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'xsd'
7
+ spec.version = XSD::VERSION
8
+ spec.authors = ['d.arkhipov']
9
+ spec.email = ['d.arkhipov@ekzo.dev']
10
+
11
+ spec.summary = 'The Ruby XSD library is an XML Schema implementation for Ruby.'
12
+ spec.description =
13
+ 'The Ruby XSD library is an XML Schema implementation for Ruby.Provides easy and flexible access to XSD information'
14
+
15
+ spec.homepage = 'https://github.com/ekzo-dev/ruby-xsd'
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 2.7.0'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+ spec.metadata['changelog_uri'] = 'https://github.com/ekzo-dev/ruby-xsd/blob/main/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
28
+ end
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ spec.add_dependency 'builder', '~> 3.2'
35
+ spec.add_dependency 'nokogiri', '~> 1.11'
36
+ spec.add_dependency 'rake', '~> 13.0'
37
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.0
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - d.arkhipov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-21 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: builder
@@ -111,6 +111,7 @@ files:
111
111
  - lib/xsd/shared/simple_typed.rb
112
112
  - lib/xsd/version.rb
113
113
  - lib/xsd/xml.rb
114
+ - xsd.gemspec
114
115
  homepage: https://github.com/ekzo-dev/ruby-xsd
115
116
  licenses:
116
117
  - MIT