xsd 2.8.1 → 2.9.1

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: c4ce386992542ac50581306a903b47c6f902db010c8f02a9894287231c15c0ba
4
- data.tar.gz: c2f104eb7999dda90a472050ab09ab63659029cd3cbfede44206c67ad0765ae2
3
+ metadata.gz: 90d2e1e3950f8ada667ad7a2960940c3a4857918425c6fb54ef6c6a92702f1bc
4
+ data.tar.gz: 713e112c94582f982c01708d6ca735fd667006b476cb6ef0125383c55e39644b
5
5
  SHA512:
6
- metadata.gz: 55b27a9f4a09b0c01f60a360b04ffd018645599edd9c7d7e8223a51be831905064b08a9e5e4cfd6cb285967692f355308bbd4acc2b3e5c0c1df91778df752fad
7
- data.tar.gz: 51cebb0519a4817f94045b73e2c9609f9b9d9de40cd05c31769021e5d710e5ca47acdc159982ac8526bf0fdaeee4a621017994a97b3b9c9bd8514ce1dcfd658a
6
+ metadata.gz: a804d3750423fe3c04c5d99614dac7a49679941498dacb9e9f72ef319c99da2c21ab70e94df58e38e7d0bae59561c66836c81db7df651ca68030baf363c96eab
7
+ data.tar.gz: 7ed63b4341d8a27a6fc1b3df4d3c6aebd30a2a5e101bf91ccaa32b54997b47f475d98c329c406eb5802706d4361b9ef6dc119a6422922726234391d124a202b2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## [2.9.1] - 2026-10-20
2
+
3
+ ### Changed
4
+
5
+ - A reference by name (a `type`, `ref`, `base`, …) is looked up in an index of the schema's top-level declarations
6
+ (`Schema#declaration_node`), built on the first lookup, instead of an XPath query that scanned every child of the
7
+ schema again for each reference. Large schemas referenced thousands of times are read several times faster
8
+ there; what is found is the same.
9
+ - The specs no longer load `wasabi`, which only a commented-out example used and which does not load on Ruby 3.4
10
+ (it requires `kconv`): the suite runs on Ruby 3.4.
11
+
12
+ ## [2.9.0] - 2026-10-18
13
+
14
+ ### Changed
15
+
16
+ - A schema with no target namespace included into one that has a namespace ("chameleon" include) is seen through
17
+ a view in the including namespace (`XML#chameleon`, `Schema#chameleon?`, `#chameleon_source`). Everything it
18
+ declares reports that namespace (`namespace`, `absolute_name`, `Schema#target_namespace`), and an unqualified
19
+ name inside it resolves in that namespace. Before, its definitions reported no namespace at all.
20
+ - A schema included into two namespaces declares two sets of definitions, one per namespace: a view keeps its own
21
+ object cache (`Schema#object_cache`), where one object per node used to serve both.
22
+ - `XML#schemas_for_namespace` returns what the schemas of the namespace include as well, chameleon views among
23
+ them, and so does the lookup of a referenced name: includes of includes, and what a schema reached through an
24
+ import includes (only the schema itself, or one level of includes, was searched). A schema only seen through a
25
+ chameleon view is no longer among those of the absent namespace
26
+ - `XML#[]` and the collectors (`elements`, `complex_types`, `simple_types`, …) go through the schemas that stand
27
+ on their own (`XML#standalone_schemas`): a schema only seen through a chameleon view contributes its
28
+ definitions through the schemas including it, not a second time in the absent namespace
29
+
30
+ ### Added
31
+
32
+ - `XML#target_namespaces`: every namespace the schemas declare definitions in, includes resolved; the absent
33
+ namespace (nil) only for a schema with no namespace that nothing includes
34
+
1
35
  ## [2.8.1] - 2026-10-18
2
36
 
3
37
  ### Fixed
data/Gemfile CHANGED
@@ -12,5 +12,4 @@ group :development do
12
12
  gem 'rubocop-performance', '~> 1.17'
13
13
  gem 'rubocop-rake', '~> 0.6'
14
14
  gem 'rubocop-rspec', '~> 2.10'
15
- gem 'wasabi', '~> 4.0'
16
15
  end
@@ -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
@@ -128,9 +129,9 @@ module XSD
128
129
  # determine schema for namespace
129
130
  search_schemas = schemas_for_namespace(name_prefix)
130
131
 
131
- # find element in target schema, name is passed as an xpath variable to stay safe with any name contents
132
+ # find the declaration in the schemas of the namespace
132
133
  search_schemas.each do |s|
133
- node = s.node.at_xpath("./xs:#{node_name}[@name=$name]", XML_SCHEMA_NS, name: name_local)
134
+ node = s.declaration_node(node_name, name_local)
134
135
  return s.node_to_object(node) if node
135
136
  end
136
137
 
@@ -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,63 @@ 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
+ # The node of a top-level declaration of this schema, by its kind and name — what a reference by name looks up
131
+ # (BaseObject#object_by_name). The children are indexed in one pass on the first lookup: a query per lookup
132
+ # scans all of them again, and a large schema is looked up in once for every reference to it.
133
+ # @param [Symbol, String] node_name kind of declaration: complexType, simpleType, element, group, ...
134
+ # @param [String] name local name
135
+ # @return Nokogiri::XML::Element, nil
136
+ def declaration_node(node_name, name)
137
+ @declaration_nodes ||= node.element_children.each_with_object({}) do |child, index|
138
+ next unless child.namespace&.href == XML_SCHEMA
139
+
140
+ # the first one, as a query would find it
141
+ index[[child.name, child['name']]] ||= child
142
+ end
143
+ @declaration_nodes[[node_name.to_s, name]]
144
+ end
145
+
146
+ # This schema and every schema it includes, those included in turn too
147
+ # @return Array<Schema>
148
+ def with_includes
149
+ found = [self]
150
+ found.each do |schema|
151
+ schema.includes.each do |include|
152
+ included = include.imported_schema
153
+ found << included unless found.any? { |known| known.equal?(included) }
154
+ end
155
+ end
156
+ found
157
+ end
158
+
102
159
  # Get all available elements on the current stack level, for schema same as elements
103
160
  # @return Array<Element>
104
161
  def collect_elements(*)
@@ -133,15 +190,17 @@ module XSD
133
190
  # @param [String, nil] namespace
134
191
  # @return Boolean
135
192
  def targets_namespace?(namespace)
136
- namespace == target_namespace || namespaces[namespace.nil? ? 'xmlns' : "xmlns:#{namespace}"] == target_namespace
193
+ namespace == target_namespace || namespaces[namespace.nil? ? 'xmlns' : "xmlns:#{namespace}"] == target_namespace ||
194
+ # an unqualified name in a chameleon schema names something of the namespace it was included into
195
+ (chameleon? && namespace.nil? && !namespaces.key?('xmlns'))
137
196
  end
138
197
 
139
198
  # Override map_children on schema to get objects from all loaded schemas
140
199
  # @param [Symbol] name
141
200
  # @param [Hash] cache Schema nodes already visited, guards against import/include cycles
142
201
  # @return Array<BaseObject>
143
- def map_children(name, cache = {})
144
- cache[node] = true
202
+ def map_children(name, cache = {}.compare_by_identity)
203
+ cache[self] = true
145
204
  super(name) + import_map_children(name, cache)
146
205
  end
147
206
 
@@ -154,7 +213,7 @@ module XSD
154
213
 
155
214
  (includes + imports).flat_map do |import|
156
215
  imported = import.imported_schema
157
- next [] if cache.key?(imported.node)
216
+ next [] if cache.key?(imported)
158
217
 
159
218
  imported.map_children(name, cache)
160
219
  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.1'
4
+ VERSION = '2.9.1'
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
@@ -163,15 +164,55 @@ module XSD
163
164
  schemas.first
164
165
  end
165
166
 
166
- # 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
167
169
  # @param [String, nil] namespace
168
170
  # @return Array<Schema>
169
171
  def schemas_for_namespace(namespace)
170
- 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) } }
171
212
  end
172
213
 
173
214
  def [](*args)
174
- schemas.each do |schema|
215
+ standalone_schemas.each do |schema|
175
216
  item = schema[*args]
176
217
  return item if item
177
218
  end
@@ -232,7 +273,7 @@ module XSD
232
273
  end
233
274
 
234
275
  def collect(name, *args)
235
- schemas.flat_map { |schema| schema.send(name, *args) }.uniq
276
+ standalone_schemas.flat_map { |schema| schema.send(name, *args) }.uniq
236
277
  end
237
278
 
238
279
  def default_logger
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.1
4
+ version: 2.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - d.arkhipov
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-10-18 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: builder
@@ -119,7 +118,6 @@ metadata:
119
118
  homepage_uri: https://github.com/ekzo-dev/ruby-xsd
120
119
  source_code_uri: https://github.com/ekzo-dev/ruby-xsd
121
120
  changelog_uri: https://github.com/ekzo-dev/ruby-xsd/blob/main/CHANGELOG.md
122
- post_install_message:
123
121
  rdoc_options: []
124
122
  require_paths:
125
123
  - lib
@@ -134,8 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
132
  - !ruby/object:Gem::Version
135
133
  version: '0'
136
134
  requirements: []
137
- rubygems_version: 3.5.5
138
- signing_key:
135
+ rubygems_version: 3.6.9
139
136
  specification_version: 4
140
137
  summary: The Ruby XSD library is an XML Schema implementation for Ruby.
141
138
  test_files: []