xsd 2.9.1 → 2.9.3

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: 90d2e1e3950f8ada667ad7a2960940c3a4857918425c6fb54ef6c6a92702f1bc
4
- data.tar.gz: 713e112c94582f982c01708d6ca735fd667006b476cb6ef0125383c55e39644b
3
+ metadata.gz: 178b66562e4da9833513bd00a12c528231551bac936eb7b72c611c9d3fc0a2bb
4
+ data.tar.gz: e58a38469f83f232583eb23f6fbefda81a86c56edf7cbfce06b305d81d3ce622
5
5
  SHA512:
6
- metadata.gz: a804d3750423fe3c04c5d99614dac7a49679941498dacb9e9f72ef319c99da2c21ab70e94df58e38e7d0bae59561c66836c81db7df651ca68030baf363c96eab
7
- data.tar.gz: 7ed63b4341d8a27a6fc1b3df4d3c6aebd30a2a5e101bf91ccaa32b54997b47f475d98c329c406eb5802706d4361b9ef6dc119a6422922726234391d124a202b2
6
+ metadata.gz: 7c34d9b9ee21284c3bc3d3b20a6f29de7ff94ac4dae1a85aaf2998dad74a35c043b612bac4691880ddefcddda42bf0734faef1fa64a63a984c301d020f3103ea
7
+ data.tar.gz: 9d846b4eaeff11247851889a133bcb62de24dad598764778be74a81d97d50b251f0257d95801c6aae8e09be588e7676c6aae2ad24e0806f8e8895ccb055f0c2b
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-performance
3
3
  - rubocop-rake
4
4
  - rubocop-rspec
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## [2.9.3] - 2026-10-24
2
+
3
+ ### Fixed
4
+
5
+ - A schema with no target namespace that imports another namespace (an XMLDSig `Signature` referenced as
6
+ `ds:SignatureType`) now finds what it imports. `Schema#targets_namespace?` took a namespace URI for a prefix, looked
7
+ it up among the schema's prefixes, found nothing, and matched that nothing against the absent target namespace:
8
+ every reference by a prefixed name was looked for in the schema itself.
9
+
10
+ ### Changed
11
+
12
+ - Every import of the absent namespace is read from its own location. Schemas with no target namespace are each a
13
+ schema of their own (a provider's documents, every one shipping its own copies of the common types, which may
14
+ differ), so a second import of the absent namespace was not the same schema as the first one; before, it was
15
+ answered with whichever such schema had been loaded first. An import whose location is loaded already is answered
16
+ with that schema in any namespace (`XML#loaded_schema`); an import of a named namespace is still answered with a
17
+ schema known for it before its location is read.
18
+
19
+ ## [2.9.2] - 2026-10-21
20
+
21
+ ### Changed
22
+
23
+ - A property, link or child of a schema object (`name`, `type`, `max_occurs`, `complex_type`, …) is read through a
24
+ method of its own, defined when the class declares it, instead of `method_missing`, which costs far more per call.
25
+ The lookup order is the same: a method a class or an ancestor defines itself still answers first, and
26
+ `respond_to?` answers as before. Reading XML against a large schema runs about 40% faster under YJIT.
27
+ - `data_type` of an element, attribute, simple and complex type is worked out once and kept: the schema does not
28
+ change once read, and converting a document asked for it once per value.
29
+
1
30
  ## [2.9.1] - 2026-10-20
2
31
 
3
32
  ### Changed
data/Gemfile CHANGED
@@ -8,8 +8,8 @@ gemspec
8
8
  group :development do
9
9
  gem 'logger', '~> 1.5'
10
10
  gem 'rspec', '~> 3.12'
11
- gem 'rubocop', '~> 1.50'
12
- gem 'rubocop-performance', '~> 1.17'
13
- gem 'rubocop-rake', '~> 0.6'
14
- gem 'rubocop-rspec', '~> 2.10'
11
+ gem 'rubocop', '~> 1.92'
12
+ gem 'rubocop-performance', '~> 1.27'
13
+ gem 'rubocop-rake', '~> 0.7'
14
+ gem 'rubocop-rspec', '~> 3.8'
15
15
  end
data/README.md CHANGED
@@ -61,6 +61,16 @@ element.multiple_allowed? # => true
61
61
  element.required? # => false
62
62
  ```
63
63
 
64
+ ### Imports and the absent namespace
65
+
66
+ XML Schema leaves it to the processor whether every `<xs:import>` of a namespace is read or only the first (a
67
+ `schemaLocation` is a hint); libxml2, and so Nokogiri's validator, reads the first. This reader reads them all, and
68
+ resolves a name within the include tree of the schema the name is written in. That is what a set of schemas with no
69
+ target namespace needs when one schema imports them all: every document a schema of its own, with its own copies of
70
+ common types that may differ from one copy to another. Put together into one schema they break the rule that names
71
+ are unique within a namespace, so a validator assembling one schema of them (`Schema#validate_xml`, which validates
72
+ through Nokogiri) sees only the first of them.
73
+
64
74
  ## Development
65
75
 
66
76
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
@@ -73,7 +73,7 @@ module XSD
73
73
  # @param [Boolean] deep
74
74
  # @return Nokogiri::XML::NodeSet
75
75
  def nodes(name = :*, deep = false)
76
- node.xpath("./#{deep ? '/' : ''}xs:#{name}", XML_SCHEMA_NS)
76
+ node.xpath("./#{'/' if deep}xs:#{name}", XML_SCHEMA_NS)
77
77
  end
78
78
 
79
79
  # Get schemas by namespace or prefix
@@ -279,12 +279,14 @@ module XSD
279
279
  # @param [Symbol] type
280
280
  # @param [Hash] options
281
281
  def self.property(name, type, options = {}, &block)
282
- properties[to_underscore(name)] = {
282
+ method = to_underscore(name)
283
+ properties[method] = {
283
284
  name: name,
284
285
  type: type,
285
286
  resolve: block,
286
287
  **options
287
288
  }
289
+ define_member_reader(method)
288
290
  end
289
291
 
290
292
  # Define new object child
@@ -292,10 +294,12 @@ module XSD
292
294
  # @param [Symbol, Array<Symbol>] type
293
295
  # @param [Hash] options
294
296
  def self.child(name, type, options = {})
295
- children[to_underscore(name)] = {
297
+ method = to_underscore(name)
298
+ children[method] = {
296
299
  type: type,
297
300
  **options
298
301
  }
302
+ define_member_reader(method)
299
303
  end
300
304
 
301
305
  # Define new object child
@@ -303,16 +307,53 @@ module XSD
303
307
  # @param [Symbol] type
304
308
  # @param [Hash] options
305
309
  def self.link(name, type, options = {})
306
- links[to_underscore(name)] = {
310
+ method = to_underscore(name)
311
+ links[method] = {
307
312
  type: type,
308
313
  **options
309
314
  }
315
+ define_member_reader(method)
310
316
  end
311
317
 
318
+ # A property, link or child is read through a method of its own: reaching it through method_missing costs far
319
+ # more per call, and an object is asked for its members all the time. The method goes in a module of the class's
320
+ # own, so that a method the class defines itself still wins, and is not defined where a method of that name
321
+ # exists already — an ancestor's own keeps answering, as it did when the name went to method_missing.
322
+ # @param [Symbol] method
323
+ def self.define_member_reader(method)
324
+ return if method_defined?(method) || private_method_defined?(method)
325
+
326
+ member_readers.define_method(method) do |*args|
327
+ value = read_member(method, args)
328
+ raise NoMethodError.new("undefined method '#{method}' for an instance of #{self.class}", method) if value.equal?(NOT_A_MEMBER)
329
+
330
+ value
331
+ end
332
+ end
333
+
334
+ # The module a class's member readers are defined in
335
+ # @return Module
336
+ def self.member_readers
337
+ @member_readers ||= Module.new.tap { |readers| include(readers) }
338
+ end
339
+
340
+ # What #read_member answers when the name is none of the object's members
341
+ NOT_A_MEMBER = Object.new.freeze
342
+
312
343
  # Lookup for properties
313
344
  # @param [Symbol] method
314
345
  # @param [Array] args
315
346
  def method_missing(method, *args)
347
+ value = read_member(method, args)
348
+ value.equal?(NOT_A_MEMBER) ? super : value
349
+ end
350
+
351
+ # The value of a property, link or child, cached: from the node, through a reference, or by its name in the
352
+ # schemas. A member reader and method_missing both come here.
353
+ # @param [Symbol] method
354
+ # @param [Array] args
355
+ # @return Object, NOT_A_MEMBER
356
+ def read_member(method, args)
316
357
  # check cache first, nil and false results are cached as well
317
358
  return @cache[method] if @cache.key?(method)
318
359
 
@@ -343,7 +384,7 @@ module XSD
343
384
  return @cache[method] = result
344
385
  end
345
386
 
346
- super
387
+ NOT_A_MEMBER
347
388
  end
348
389
 
349
390
  # Does object has property/link/child?
@@ -368,7 +409,7 @@ module XSD
368
409
  # @param [String, nil] string
369
410
  # @return String, nil
370
411
  def nil_if_empty(string)
371
- string&.empty? ? nil : string
412
+ string && string.empty? ? nil : string
372
413
  end
373
414
 
374
415
  private
@@ -6,6 +6,8 @@ module XSD
6
6
  # extension (both simpleContent and complexContent)
7
7
  # https://www.w3schools.com/xml/el_attribute.asp
8
8
  class Attribute < BaseObject
9
+ prepend CachedDataType
10
+
9
11
  TYPE_PROPERTY = :type
10
12
 
11
13
  include SimpleTyped
@@ -6,6 +6,7 @@ module XSD
6
6
  # Parent elements: element, redefine, schema
7
7
  # https://www.w3schools.com/xml/el_complextype.asp
8
8
  class ComplexType < BaseObject
9
+ prepend CachedDataType
9
10
  include AttributeContainer
10
11
  include Named
11
12
 
@@ -5,6 +5,8 @@ module XSD
5
5
  # Parent elements: schema, choice, all, sequence, group
6
6
  # https://www.w3schools.com/xml/el_element.asp
7
7
  class Element < BaseObject
8
+ prepend CachedDataType
9
+
8
10
  TYPE_PROPERTY = :type
9
11
 
10
12
  include MinMaxOccurs
@@ -15,12 +15,21 @@ module XSD
15
15
  # @return String, nil
16
16
  property :schemaLocation, :string
17
17
 
18
- # Get imported schema
18
+ # Get imported schema: the one at the import's location when that one is loaded already; otherwise one known for
19
+ # the namespace, added manually or by another import. Not for the absent namespace: schemas with no target
20
+ # namespace are each a schema of its own (a provider's documents, every one with its own copies of the common
21
+ # types), so every import of it is read from its own location.
19
22
  # @return Schema
20
23
  def imported_schema
21
- # a schema for the namespace may already be known, either added manually or by another import
22
- known_schema = reader.schemas_for_namespace(namespace).find { |s| !s.equal?(schema) }
23
- return known_schema if known_schema
24
+ if schema_location
25
+ located = reader.loaded_schema(schema_location, base: schema.location)
26
+ return located if located
27
+ end
28
+
29
+ unless namespace.nil?
30
+ known_schema = reader.schemas_for_namespace(namespace).find { |s| !s.equal?(schema) }
31
+ return known_schema if known_schema
32
+ end
24
33
 
25
34
  unless schema_location
26
35
  raise ImportError,
@@ -62,14 +62,14 @@ module XSD
62
62
  def collect_elements(include_base = false)
63
63
  # Don't include elements from base by default, that will lead to element duplicates because
64
64
  # elements in complex restriction MUST be all listen in restricting type
65
- super(include_base)
65
+ super
66
66
  end
67
67
 
68
68
  # Get all available attributes on the current stack level, optionally including base type attributes
69
69
  # @param [Boolean] include_base
70
70
  # @return Array<Attribute>
71
71
  def collect_attributes(include_base = true)
72
- result = super(include_base)
72
+ result = super
73
73
  # Filter restricted attributes to avoid duplicates from restricting and restricted type
74
74
  include_base ? result.to_h { |item| [item.name, item] }.values : result
75
75
  end
@@ -190,7 +190,10 @@ module XSD
190
190
  # @param [String, nil] namespace
191
191
  # @return Boolean
192
192
  def targets_namespace?(namespace)
193
- namespace == target_namespace || namespaces[namespace.nil? ? 'xmlns' : "xmlns:#{namespace}"] == target_namespace ||
193
+ key = namespace.nil? ? 'xmlns' : "xmlns:#{namespace}"
194
+ # a prefix names a namespace only when the schema declares it: looking up a namespace URI as a prefix finds
195
+ # nothing, and nothing must not pass for the absent target namespace of a schema that has none
196
+ namespace == target_namespace || (namespaces.key?(key) && namespaces[key] == target_namespace) ||
194
197
  # an unqualified name in a chameleon schema names something of the namespace it was included into
195
198
  (chameleon? && namespace.nil? && !namespaces.key?('xmlns'))
196
199
  end
@@ -7,6 +7,7 @@ module XSD
7
7
  # https://www.w3schools.com/xml/el_simpletype.asp
8
8
  class SimpleType < BaseObject
9
9
  include Named
10
+ prepend CachedDataType
10
11
 
11
12
  # Specifies a name for the element. This attribute is required if the simpleType element is a child of the
12
13
  # schema element, otherwise it is not allowed
@@ -9,7 +9,7 @@ module XSD
9
9
  # @!attribute member_types
10
10
  # @return Array<String>
11
11
  property :memberTypes, :array, default: [] do |union|
12
- union.node['memberTypes']&.split(' ')
12
+ union.node['memberTypes']&.split
13
13
  end
14
14
 
15
15
  # Nested simple and built-in types
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XSD
4
+ # A definition's data type follows from the schema, which does not change once read: it is worked out on first use
5
+ # and kept. Reading XML casts every value by it, so it is asked for once per value otherwise.
6
+ module CachedDataType
7
+ def data_type
8
+ return @data_type if defined?(@data_type)
9
+
10
+ @data_type = super
11
+ end
12
+ end
13
+ 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.9.1'
4
+ VERSION = '2.9.3'
5
5
  end
data/lib/xsd/xml.rb CHANGED
@@ -21,7 +21,7 @@ module XSD
21
21
  elsif Pathname.new(location).absolute?
22
22
  File.read(location)
23
23
  else
24
- raise ImportError, "Failed to locate import '#{location}'#{namespace ? " for namespace '#{namespace}'" : ''}"
24
+ raise ImportError, "Failed to locate import '#{location}'#{" for namespace '#{namespace}'" if namespace}"
25
25
  end
26
26
  end
27
27
 
@@ -158,6 +158,14 @@ module XSD
158
158
  @schemas_by_location[expanded] = add_schema_xml(xml, location: expanded)
159
159
  end
160
160
 
161
+ # The schema already read from a location, relative to a base as an import or an include names it
162
+ # @param [String] location
163
+ # @param [String, nil] base Location of the referencing schema
164
+ # @return Schema, nil
165
+ def loaded_schema(location, base: nil)
166
+ @schemas_by_location[expand_location(location.tr('\\', '/'), base)]
167
+ end
168
+
161
169
  # Get first added (considered primary) schema
162
170
  # @return Schema, nil
163
171
  def schema
@@ -256,7 +264,7 @@ module XSD
256
264
  elsif File.file?(location)
257
265
  File.read(location)
258
266
  else
259
- raise ImportError, "Failed to locate import '#{location}'#{namespace ? " for namespace '#{namespace}'" : ''}"
267
+ raise ImportError, "Failed to locate import '#{location}'#{" for namespace '#{namespace}'" if namespace}"
260
268
  end
261
269
  end
262
270
 
data/lib/xsd.rb CHANGED
@@ -10,6 +10,7 @@ require 'xsd/shared/element_container'
10
10
  require 'xsd/shared/based'
11
11
  require 'xsd/shared/simple_typed'
12
12
  require 'xsd/shared/complex_typed'
13
+ require 'xsd/shared/cached_data_type'
13
14
  require 'xsd/objects/facet'
14
15
  require 'xsd/objects/attribute'
15
16
  require 'xsd/objects/attribute_group'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.1
4
+ version: 2.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - d.arkhipov
@@ -102,6 +102,7 @@ files:
102
102
  - lib/xsd/objects/unique.rb
103
103
  - lib/xsd/shared/attribute_container.rb
104
104
  - lib/xsd/shared/based.rb
105
+ - lib/xsd/shared/cached_data_type.rb
105
106
  - lib/xsd/shared/complex_typed.rb
106
107
  - lib/xsd/shared/element_container.rb
107
108
  - lib/xsd/shared/min_max_occurs.rb