pubid 2.0.0.pre.alpha.12 → 2.0.0.pre.alpha.13
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 +4 -4
- data/README.adoc +43 -1
- data/data/ieee/update_codes.yaml +17 -4
- data/data/nist/update_codes.yaml +7 -3
- data/lib/pubid/adobe/builder.rb +2 -0
- data/lib/pubid/all_parts.rb +201 -0
- data/lib/pubid/all_parts_identifier.rb +19 -0
- data/lib/pubid/amca/CLAUDE.md +47 -0
- data/lib/pubid/amca/builder.rb +3 -5
- data/lib/pubid/amca/identifiers/base.rb +10 -0
- data/lib/pubid/amca/identifiers/publication.rb +13 -0
- data/lib/pubid/amca/parser.rb +2 -1
- data/lib/pubid/amca/renderer.rb +22 -33
- data/lib/pubid/amca/urn_generator.rb +21 -2
- data/lib/pubid/amca/urn_parser.rb +36 -10
- data/lib/pubid/ansi/builder.rb +6 -0
- data/lib/pubid/api/CLAUDE.md +23 -0
- data/lib/pubid/api/builder.rb +2 -0
- data/lib/pubid/ashrae/CLAUDE.md +13 -0
- data/lib/pubid/ashrae/builder.rb +58 -14
- data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
- data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
- data/lib/pubid/ashrae/parser.rb +62 -28
- data/lib/pubid/ashrae/renderer.rb +32 -1
- data/lib/pubid/ashrae/urn_generator.rb +32 -9
- data/lib/pubid/asme/CLAUDE.md +25 -0
- data/lib/pubid/asme/builder.rb +16 -9
- data/lib/pubid/asme/components/code.rb +2 -0
- data/lib/pubid/asme/identifiers/standard.rb +6 -1
- data/lib/pubid/asme/parser.rb +41 -14
- data/lib/pubid/astm/CLAUDE.md +9 -0
- data/lib/pubid/astm/builder.rb +2 -0
- data/lib/pubid/astm/components/code.rb +2 -0
- data/lib/pubid/astm/parser.rb +4 -1
- data/lib/pubid/bipm/CLAUDE.md +11 -0
- data/lib/pubid/bipm/builder.rb +2 -0
- data/lib/pubid/bsi/CLAUDE.md +93 -0
- data/lib/pubid/bsi/builder.rb +13 -11
- data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
- data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
- data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
- data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
- data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
- data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
- data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
- data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
- data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
- data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
- data/lib/pubid/bsi/identifiers/set.rb +2 -0
- data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
- data/lib/pubid/bsi/identifiers.rb +1 -0
- data/lib/pubid/bsi/parser.rb +8 -8
- data/lib/pubid/bsi/renderer.rb +20 -20
- data/lib/pubid/bsi/urn_generator.rb +28 -18
- data/lib/pubid/builder/base.rb +27 -0
- data/lib/pubid/calconnect/builder.rb +2 -0
- data/lib/pubid/calconnect/identifier.rb +4 -0
- data/lib/pubid/ccsds/builder.rb +2 -0
- data/lib/pubid/ccsds/identifier.rb +8 -0
- data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
- data/lib/pubid/cen_cenelec/builder.rb +6 -1
- data/lib/pubid/cen_cenelec/identifier.rb +10 -27
- data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
- data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
- data/lib/pubid/cen_cenelec/parser.rb +11 -3
- data/lib/pubid/cie/CLAUDE.md +58 -0
- data/lib/pubid/cie/builder.rb +2 -0
- data/lib/pubid/cie/components/language.rb +2 -0
- data/lib/pubid/components/adoption.rb +2 -0
- data/lib/pubid/components/code.rb +2 -0
- data/lib/pubid/components/date.rb +8 -6
- data/lib/pubid/components/edition.rb +2 -0
- data/lib/pubid/components/iteration.rb +2 -0
- data/lib/pubid/components/language.rb +2 -0
- data/lib/pubid/components/locality.rb +2 -0
- data/lib/pubid/components/publisher.rb +2 -0
- data/lib/pubid/components/relationship.rb +2 -0
- data/lib/pubid/components/stage.rb +2 -0
- data/lib/pubid/components/supplement.rb +2 -0
- data/lib/pubid/components/type.rb +2 -0
- data/lib/pubid/components/typed_stage.rb +8 -0
- data/lib/pubid/csa/CLAUDE.md +41 -0
- data/lib/pubid/csa/builder.rb +2 -0
- data/lib/pubid/csa/identifier.rb +13 -1
- data/lib/pubid/csa/renderer.rb +12 -12
- data/lib/pubid/csa/single_identifier.rb +17 -0
- data/lib/pubid/doi/builder.rb +2 -0
- data/lib/pubid/easc/builder.rb +2 -0
- data/lib/pubid/ecma/CLAUDE.md +28 -0
- data/lib/pubid/ecma/builder.rb +2 -0
- data/lib/pubid/ecma/identifier.rb +7 -0
- data/lib/pubid/etsi/CLAUDE.md +34 -0
- data/lib/pubid/etsi/builder.rb +2 -0
- data/lib/pubid/etsi/components/code.rb +6 -0
- data/lib/pubid/etsi/components/version.rb +2 -0
- data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
- data/lib/pubid/evs/CLAUDE.md +58 -0
- data/lib/pubid/evs/builder.rb +2 -0
- data/lib/pubid/gb/CLAUDE.md +140 -0
- data/lib/pubid/gb/builder.rb +7 -2
- data/lib/pubid/gb/identifier.rb +5 -3
- data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
- data/lib/pubid/gb/identifiers.rb +1 -0
- data/lib/pubid/gb/renderer.rb +0 -1
- data/lib/pubid/gost/CLAUDE.md +64 -0
- data/lib/pubid/gost/builder.rb +3 -1
- data/lib/pubid/gost/identifier.rb +5 -0
- data/lib/pubid/iala/CLAUDE.md +82 -0
- data/lib/pubid/iala/builder.rb +2 -0
- data/lib/pubid/iana/CLAUDE.md +7 -0
- data/lib/pubid/iana/builder.rb +2 -0
- data/lib/pubid/identifier.rb +161 -17
- data/lib/pubid/idf/builder.rb +6 -1
- data/lib/pubid/idf/identifier.rb +5 -0
- data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
- data/lib/pubid/idf/identifiers.rb +1 -0
- data/lib/pubid/iec/CLAUDE.md +31 -0
- data/lib/pubid/iec/builder.rb +7 -1
- data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
- data/lib/pubid/iec/components/sheet.rb +2 -0
- data/lib/pubid/iec/components/trf_info.rb +2 -0
- data/lib/pubid/iec/components/vap_suffix.rb +2 -0
- data/lib/pubid/iec/identifier.rb +7 -2
- data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iec/identifiers.rb +1 -0
- data/lib/pubid/iec/renderer.rb +0 -1
- data/lib/pubid/iec/urn_generator.rb +9 -1
- data/lib/pubid/iec/urn_parser.rb +3 -2
- data/lib/pubid/ieee/CLAUDE.md +97 -0
- data/lib/pubid/ieee/builder.rb +134 -5
- data/lib/pubid/ieee/components/code.rb +2 -0
- data/lib/pubid/ieee/components/draft.rb +35 -2
- data/lib/pubid/ieee/components/typed_stage.rb +2 -0
- data/lib/pubid/ieee/identifiers/base.rb +20 -0
- data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
- data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
- data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
- data/lib/pubid/ieee/parser.rb +134 -21
- data/lib/pubid/ieee/renderer.rb +40 -7
- data/lib/pubid/ieee/urn_generator.rb +31 -0
- data/lib/pubid/ietf/CLAUDE.md +7 -0
- data/lib/pubid/ietf/builder.rb +2 -0
- data/lib/pubid/iho/builder.rb +2 -0
- data/lib/pubid/isbn/builder.rb +2 -0
- data/lib/pubid/iso/CLAUDE.md +47 -0
- data/lib/pubid/iso/builder.rb +19 -5
- data/lib/pubid/iso/components/publisher.rb +2 -0
- data/lib/pubid/iso/identifier.rb +6 -14
- data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
- data/lib/pubid/iso/identifiers.rb +1 -0
- data/lib/pubid/iso/rendering_style.rb +0 -1
- data/lib/pubid/itu/CLAUDE.md +69 -0
- data/lib/pubid/itu/builder.rb +2 -0
- data/lib/pubid/itu/components/code.rb +2 -0
- data/lib/pubid/itu/components/designation.rb +2 -0
- data/lib/pubid/itu/components/sector.rb +2 -0
- data/lib/pubid/itu/components/series.rb +2 -0
- data/lib/pubid/itu/identifiers/supplement.rb +15 -0
- data/lib/pubid/jcgm/CLAUDE.md +7 -0
- data/lib/pubid/jcgm/builder.rb +2 -0
- data/lib/pubid/jcgm/components/publisher.rb +2 -0
- data/lib/pubid/jis/builder.rb +5 -1
- data/lib/pubid/jis/identifier.rb +5 -17
- data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
- data/lib/pubid/jis/identifiers.rb +1 -0
- data/lib/pubid/jis/renderer.rb +0 -2
- data/lib/pubid/jis/urn_generator.rb +0 -1
- data/lib/pubid/nist/CLAUDE.md +56 -0
- data/lib/pubid/nist/builder.rb +2 -0
- data/lib/pubid/nist/components/edition.rb +2 -0
- data/lib/pubid/nist/components/issue_number.rb +2 -0
- data/lib/pubid/nist/components/part.rb +2 -0
- data/lib/pubid/nist/components/stage.rb +2 -0
- data/lib/pubid/nist/components/supplement.rb +2 -0
- data/lib/pubid/nist/components/translation.rb +2 -0
- data/lib/pubid/nist/components/update.rb +2 -0
- data/lib/pubid/nist/components/version.rb +2 -0
- data/lib/pubid/nist/components/volume.rb +2 -0
- data/lib/pubid/nist/identifiers/base.rb +24 -3
- data/lib/pubid/nist/parser.rb +15 -2
- data/lib/pubid/nist/preprocessor.rb +53 -2
- data/lib/pubid/oasis/CLAUDE.md +19 -0
- data/lib/pubid/oasis/builder.rb +2 -0
- data/lib/pubid/oasis/identifier.rb +19 -0
- data/lib/pubid/ogc/CLAUDE.md +34 -0
- data/lib/pubid/ogc/builder.rb +2 -0
- data/lib/pubid/ogc/identifier.rb +11 -0
- data/lib/pubid/oiml/CLAUDE.md +189 -0
- data/lib/pubid/oiml/builder.rb +20 -0
- data/lib/pubid/oiml/components/code.rb +6 -0
- data/lib/pubid/oiml/identifier.rb +13 -0
- data/lib/pubid/oiml/identifiers/annex.rb +4 -0
- data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
- data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
- data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
- data/lib/pubid/oiml/identifiers.rb +2 -0
- data/lib/pubid/oiml/parser.rb +35 -4
- data/lib/pubid/oiml/renderer.rb +23 -1
- data/lib/pubid/oiml/single_identifier.rb +4 -0
- data/lib/pubid/oiml/supplement_identifier.rb +7 -0
- data/lib/pubid/oiml/urn_generator.rb +28 -0
- data/lib/pubid/oiml.rb +5 -0
- data/lib/pubid/omg/CLAUDE.md +15 -0
- data/lib/pubid/omg/builder.rb +2 -0
- data/lib/pubid/parser/grammar.rb +23 -0
- data/lib/pubid/plateau/builder.rb +2 -0
- data/lib/pubid/plateau/identifiers/base.rb +4 -0
- data/lib/pubid/plateau/supplement_identifier.rb +14 -2
- data/lib/pubid/plateau/urn_generator.rb +7 -1
- data/lib/pubid/renderers/human_readable.rb +0 -1
- data/lib/pubid/sae/builder.rb +2 -0
- data/lib/pubid/sae/components/date.rb +2 -0
- data/lib/pubid/sae/components/type.rb +2 -0
- data/lib/pubid/subset_match.rb +197 -0
- data/lib/pubid/tgpp/CLAUDE.md +43 -0
- data/lib/pubid/tgpp/builder.rb +2 -0
- data/lib/pubid/tgpp/identifier.rb +14 -0
- data/lib/pubid/type_resolver.rb +14 -2
- data/lib/pubid/un/builder.rb +2 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid/w3c/CLAUDE.md +7 -0
- data/lib/pubid/w3c/builder.rb +2 -0
- data/lib/pubid/xsf/CLAUDE.md +11 -0
- data/lib/pubid/xsf/builder.rb +2 -0
- data/lib/pubid.rb +16 -3
- metadata +44 -2
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
# Subset match: `reference === candidate`.
|
|
5
|
+
#
|
|
6
|
+
# The match is true when every part that +reference+ states matches
|
|
7
|
+
# +candidate+. A part that +reference+ leaves nil or empty is a wildcard.
|
|
8
|
+
# relaton uses it to match a partial user reference against index rows, so
|
|
9
|
+
# the caller does not have to name the parts to ignore, as `#matches?`
|
|
10
|
+
# requires.
|
|
11
|
+
#
|
|
12
|
+
# reference = Pubid::Iso.parse("ISO 9001")
|
|
13
|
+
# dated = Pubid::Iso.parse("ISO 9001:2015")
|
|
14
|
+
# reference === dated # => true
|
|
15
|
+
# dated === reference # => false
|
|
16
|
+
#
|
|
17
|
+
# The operator is NOT symmetric: the receiver is the reference. Ruby calls
|
|
18
|
+
# `===` for `case` and `Enumerable#grep`, so `catalogue.grep(reference)`
|
|
19
|
+
# returns the entries that match the reference. RSpec calls it too: the
|
|
20
|
+
# fuzzy matchers (`include`, `match`, `contain_exactly`, `have_attributes`)
|
|
21
|
+
# and mock argument matchers (`with`) try `expected === actual` when `==` is
|
|
22
|
+
# false. So an expectation with a partial identifier on the expected side
|
|
23
|
+
# passes against a fuller one. Use `eq` when a spec needs exact equality.
|
|
24
|
+
#
|
|
25
|
+
# Rules:
|
|
26
|
+
# - The two objects must be instances of the same class. A reference never
|
|
27
|
+
# falls back to the base document of a wrapper: `BS 7273-4` does not match
|
|
28
|
+
# `BS 7273-4:2015+A1:2021`.
|
|
29
|
+
# - A default value is stated. `ISO 9001` means the published stage, so it
|
|
30
|
+
# does not match `ISO/DIS 9001`.
|
|
31
|
+
# - A collection matches by position, and the reference can be shorter:
|
|
32
|
+
# `ISO/IEC 9001` matches `ISO/IEC/IEEE 9001`, `ISO/IEEE 9001` does not.
|
|
33
|
+
# - A STRICT attribute is exempt from both wildcards: the reference always
|
|
34
|
+
# states it, so a nil value means "this document has none" and a stated
|
|
35
|
+
# collection is not a prefix. A class declares one with `subset_strict`.
|
|
36
|
+
# - A reference that sets +all_parts+ matches every part of the document:
|
|
37
|
+
# `part`, `parts`, `subpart` and `all_parts` itself are skipped.
|
|
38
|
+
# - A nested identifier or component that includes this module is compared
|
|
39
|
+
# with its own `===`. Any other value is compared with `==`.
|
|
40
|
+
# - `#==` does not change. An error from an attribute reader propagates.
|
|
41
|
+
#
|
|
42
|
+
# The match walks the attributes of the objects, not their `to_hash`, so a
|
|
43
|
+
# class can change the rule for its own attributes:
|
|
44
|
+
# - `subset_strict` names attributes the reference always states, for
|
|
45
|
+
# example an ECMA `part` or an ETSI `parts` list.
|
|
46
|
+
# - `self.subset_ignored_attributes` names attributes that `===` skips, for
|
|
47
|
+
# example a value that `from_hash` cannot restore (NIST build artifacts).
|
|
48
|
+
# - `#subset_attribute_match?` decides one attribute; an override calls
|
|
49
|
+
# `super` for the attributes it does not handle.
|
|
50
|
+
#
|
|
51
|
+
# Every identifier and every component class includes this module. A new
|
|
52
|
+
# component class must include it too; `spec/pubid/subset_match_spec.rb`
|
|
53
|
+
# fails otherwise.
|
|
54
|
+
module SubsetMatch
|
|
55
|
+
# The parts of a document, which a reference with +all_parts+ does not
|
|
56
|
+
# restrict. `all_parts` itself is here so that an all-parts reference
|
|
57
|
+
# matches a candidate that states one part.
|
|
58
|
+
ALL_PARTS_ATTRIBUTES = %i[part parts subpart all_parts].freeze
|
|
59
|
+
|
|
60
|
+
def self.included(base)
|
|
61
|
+
base.extend(ClassMethods)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Class-level hooks.
|
|
65
|
+
module ClassMethods
|
|
66
|
+
# Declare attributes that the reference always states. A nil or empty
|
|
67
|
+
# value then means "this document has none", and a stated collection
|
|
68
|
+
# must match in full instead of as a prefix.
|
|
69
|
+
#
|
|
70
|
+
# class Pubid::Ecma::Identifier < Pubid::Identifier
|
|
71
|
+
# subset_strict :part
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# A caller that does want every part of a document sets `all_parts` on
|
|
75
|
+
# the reference, or keeps using `#matches?(other, ignore:)`.
|
|
76
|
+
#
|
|
77
|
+
# @param names [Array<Symbol>] the attributes of this class
|
|
78
|
+
# @return [Array<Symbol>] the attributes this class itself declares
|
|
79
|
+
def subset_strict(*names)
|
|
80
|
+
@subset_strict_own = subset_strict_own | names.map(&:to_sym)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Array<Symbol>] the attributes that `===` compares exactly,
|
|
84
|
+
# this class's own declarations and every one it inherits
|
|
85
|
+
def subset_strict_attributes
|
|
86
|
+
inherited = if superclass.respond_to?(:subset_strict_attributes)
|
|
87
|
+
superclass.subset_strict_attributes
|
|
88
|
+
else
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
inherited | subset_strict_own
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Array<Symbol>] the attributes that `===` skips
|
|
95
|
+
def subset_ignored_attributes
|
|
96
|
+
[]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def subset_strict_own
|
|
102
|
+
@subset_strict_own ||= []
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @param other [Object] the candidate
|
|
107
|
+
# @return [Boolean] true when +other+ holds every part that self states
|
|
108
|
+
def ===(other)
|
|
109
|
+
return true if equal?(other)
|
|
110
|
+
return false unless other.instance_of?(self.class)
|
|
111
|
+
|
|
112
|
+
ignored = subset_skipped_attributes
|
|
113
|
+
self.class.attributes.each_key.all? do |name|
|
|
114
|
+
ignored.include?(name) || subset_attribute_match?(
|
|
115
|
+
name, public_send(name), other.public_send(name)
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [Array<Symbol>] the attributes this match does not compare
|
|
121
|
+
def subset_skipped_attributes
|
|
122
|
+
ignored = self.class.subset_ignored_attributes
|
|
123
|
+
return ignored unless subset_all_parts_wildcard?
|
|
124
|
+
|
|
125
|
+
ignored | ALL_PARTS_ATTRIBUTES
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# True when the reference asks for every part of the document.
|
|
129
|
+
# `Pubid::Identifier` overrides it to read its +all_parts+ attribute; a
|
|
130
|
+
# component never holds a document's parts.
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def subset_all_parts_wildcard?
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @param name [Symbol] the attribute name
|
|
137
|
+
# @param mine [Object] the value of the reference
|
|
138
|
+
# @param theirs [Object] the value of the candidate
|
|
139
|
+
# @return [Boolean] true when +theirs+ satisfies +mine+
|
|
140
|
+
def subset_attribute_match?(name, mine, theirs)
|
|
141
|
+
return SubsetMatch.exact_match?(mine, theirs) if
|
|
142
|
+
self.class.subset_strict_attributes.include?(name)
|
|
143
|
+
|
|
144
|
+
SubsetMatch.value_match?(mine, theirs)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @return [Boolean] true when +theirs+ satisfies the reference value +mine+
|
|
148
|
+
def self.value_match?(mine, theirs)
|
|
149
|
+
return true if blank?(mine)
|
|
150
|
+
|
|
151
|
+
case mine
|
|
152
|
+
when ::Array then collection_match?(mine, theirs)
|
|
153
|
+
when SubsetMatch then mine === theirs
|
|
154
|
+
else mine == theirs
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A strict attribute is compared exactly: nil and empty are the same
|
|
159
|
+
# value, so a parsed identifier still matches the index row that
|
|
160
|
+
# `from_hash` rebuilds from a hash that dropped the empty attribute; a
|
|
161
|
+
# collection must match in full, element by element.
|
|
162
|
+
#
|
|
163
|
+
# Exactness composes rather than flattening: a nested value that
|
|
164
|
+
# includes this module is matched in BOTH directions with its own
|
|
165
|
+
# `===`, so the class that owns it keeps its rule. A plain `==` would
|
|
166
|
+
# override it — `Components::TypedStage` ignores `original_abbr`
|
|
167
|
+
# (the input spelling, `Amd` against `AMD`), and CEN/CENELEC declares
|
|
168
|
+
# `typed_stage` strict.
|
|
169
|
+
# @return [Boolean]
|
|
170
|
+
def self.exact_match?(mine, theirs)
|
|
171
|
+
return true if blank?(mine) && blank?(theirs)
|
|
172
|
+
|
|
173
|
+
case mine
|
|
174
|
+
when ::Array then exact_collection_match?(mine, theirs)
|
|
175
|
+
when SubsetMatch then mine === theirs && theirs === mine
|
|
176
|
+
else mine == theirs
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# A strict collection matches in full, never as a prefix.
|
|
181
|
+
def self.exact_collection_match?(mine, theirs)
|
|
182
|
+
theirs.is_a?(::Array) && mine.size == theirs.size &&
|
|
183
|
+
mine.each_with_index.all? { |value, i| exact_match?(value, theirs[i]) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# A shorter reference collection matches the leading elements.
|
|
187
|
+
def self.collection_match?(mine, theirs)
|
|
188
|
+
theirs.is_a?(::Array) && mine.size <= theirs.size &&
|
|
189
|
+
mine.each_with_index.all? { |value, i| value_match?(value, theirs[i]) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @return [Boolean] true for nil and for an empty string or collection
|
|
193
|
+
def self.blank?(value)
|
|
194
|
+
value.nil? || Lutaml::Model::Utils.empty?(value)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 3GPP flavor notes
|
|
2
|
+
|
|
3
|
+
3GPP (`Pubid::Tgpp`) module naming and partial references.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/tgpp/` or `spec/pubid/tgpp/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **Digit-leading flavor names (`3gpp`/`Pubid::Tgpp`)**: a relaton flavor whose name starts with a digit can't be a Ruby constant, so the module is spelled out (`Pubid::Tgpp`, files under `lib/pubid/tgpp/`) while the **external contract stays the digit form** everywhere it's observed: the registry key (`Pubid::Registry.register(:"3gpp", …)`), the polymorphic `_type` (`pubid:3gpp:…`), and the URN namespace (`urn:3gpp:…`). Three overrides bridge the gap — (1) `Identifier.polymorphic_name` post-processes the auto-derived name (`super&.sub(/\Apubid:tgpp:/, "pubid:3gpp:")`) so `_type`/`TGPP_TYPE_MAP` agree; (2) `UrnParser#flavor_name` returns `"3gpp"` so `strip_namespace` accepts `urn:3gpp:`; (3) the export layer registers `:tgpp` (NOT `:"3gpp"`) in `Export::Exporter::FLAVORS` because `FlavorExporter` maps the symbol via `Pubid.const_get(camelize(sym))`, and it reads fixtures from `spec/fixtures/tgpp/` — so this flavor's on-disk paths (`spec/pubid/tgpp/`, `spec/fixtures/tgpp/`) follow the module, not the digit name. `PREFIXES = ["3GPP"]` (the printed docidentifier is `3GPP TS …`; the parser also accepts the bare index form). Any future digit-leading flavor mirrors this split.
|
|
8
|
+
|
|
9
|
+
- **3GPP partial references — both trailing qualifiers are optional**: a 3GPP identifier is `[3GPP ]<TR|TS> <NN.NNN>[suffix][-part…][:<release>][/<version>]`. The release segment was already `.maybe` (a handful of legacy records omit it, e.g. `TS 29.215/2.0.0`) but the **version was mandatory**, so the bare reference a person actually types — `3GPP TS 23.207` — failed to parse. Every one of the 88,464 published `relaton-data-3gpp` index rows carries both, so the corpus never exercised the gap; it blocks the *consumer*, which must parse a user query to search that index. The fix is the ETSI three-step pattern verbatim — parser `(str("/") >> version).maybe`, builder `data[:version]&.to_s` (never `""`, so the no-defaults `to_hash` drops it), renderer appending each qualifier only when present — plus the URN pair. `type` and `number_core` stay required, and `version` still demands three dotted digit groups, so `TS`, `TS foo`, `TS 23.207/4.0`, `TS 23.207/` and `TS 23.207:` are all still rejected. **URN decision: only TRAILING empty segments are dropped, never interior ones.** A bare reference gets the clean `urn:3gpp:ts:23.207` instead of a malformed `urn:3gpp:ts:23.207::`, while the pre-existing release-less form keeps the URN it has always published (`TS 29.215/2.0.0` → `urn:3gpp:ts:29.215::2.0.0`, an interior empty segment `UrnParser` already reads back). Generator and parser are exact inverses for all four present/absent combinations. **One guard the optionality forced**: the `release` rule is deliberately generic (`match["^/:"].repeat(1)`), so once the version became optional a mistyped separator — `TS 23.207:2.0.0` — silently parsed with the *version* misfiled as the release, where it was structurally impossible before. `release` now carries a `(version_core >> any.absent?).absent?` lookahead, rejecting a trailing segment shaped exactly like a version. It is anchored at **end of input**, so a real release still wins whenever a version follows (`TS 23.207:Release 2000/9.0.0`); the empirical warrant is that **none** of the 27 distinct releases in the 88,464 published rows contains a dot at all. **Landmine in the trailing-strip loop**: a bare `Array#any?` tests element *truthiness*, so `[nil, nil].any?` is `false` and strips nothing — the loop guard must be `until segments.empty? || !blank?(segments.last)`. **Nothing about the stored shape changed**: `attribute :version, :string` and the `key_value` map stay as they are, only `nil` becomes reachable, so no index migration follows and no relaton index needs regenerating. `number` was already a plain `:string` and already the relaton bsearch key — there is no IANA/BIPM/W3C-style `root.number` defect here. Verified over the whole published corpus: `to_s`, `to_urn` and the URN round-trip are **byte-identical on all 88,464 rows** before and after, with 0 empty `root.number`. 3GPP's `spec/pubid/partial_ref_spec.rb` entry moved from the laggard `omits: []` to `{ ref: "3GPP TS 23.207", omits: %i[release version] }`, leaving **AMCA as the only remaining laggard**. The new bare/partial forms are NORMALIZING for the `3GPP ` prefix (`3GPP TS 23.207` renders back as `TS 23.207`, since `with_publisher` defaults to false to match the relaton index id), so per the `CIPM/2005-06(REV)` precedent they live in `spec/pubid/tgpp/identifier_spec.rb`, **not** in the byte-exact `spec/fixtures/tgpp/identifiers/pass/`. **Pre-existing and out of scope:** `Pubid.parse` resolves only `:urn` and `:mr_string` inputs and raises for a human string in *every* flavor, so `Pubid.parse("3GPP TS 23.207")` does not route — consumers call `Pubid::Tgpp::Identifier.parse` (or pass `pubid_class:`) directly. (hand-off: tgpp-bare-reference-parse; consumer follow-up: relaton__relaton__3gpp-consume-index-v2.)
|
|
10
|
+
|
|
11
|
+
## `all_parts_edition_keys` missed `release`
|
|
12
|
+
|
|
13
|
+
`Identifier.all_parts_edition_keys` defaults to `%i[date year edition
|
|
14
|
+
version]`. 3GPP's edition/version identity is split across two
|
|
15
|
+
attributes: `version` is in the default list, but `release` (e.g.
|
|
16
|
+
`"REL-4"`) is not, so `"3GPP TS 23.207:REL-4/4.0.0".to_all_parts` kept the
|
|
17
|
+
release token and failed to match `"3GPP TS 23.207:REL-5/5.0.0"` under
|
|
18
|
+
`#===`. Fixed with `Pubid::Tgpp::Identifier.all_parts_edition_keys` (`super
|
|
19
|
+
+ %i[release]`) — unlike OGC's `year`, nothing else 3GPP declares needs to
|
|
20
|
+
come OUT of the default list, so this is a pure addition. Locked by
|
|
21
|
+
`spec/pubid/tgpp/all_parts_spec.rb` and
|
|
22
|
+
`spec/pubid/all_parts_edition_keys_audit_spec.rb`.
|
|
23
|
+
|
|
24
|
+
## Related rule in the root file
|
|
25
|
+
|
|
26
|
+
3GPP's `parts` attribute produced the rule that a `collection: true` attribute needs `initialize_empty: true`. That rule applies to every flavor, so it stays in the root `CLAUDE.md`, not here.
|
|
27
|
+
|
|
28
|
+
## Subset match: strict attributes
|
|
29
|
+
|
|
30
|
+
Read `docs/SUBSET_MATCH.md` first. `===` reads a nil part of the reference as
|
|
31
|
+
a wildcard, which is wrong for the attributes below: the flavor models a nil
|
|
32
|
+
value as "this document has none". They are declared with `subset_strict`, so
|
|
33
|
+
`===` compares them exactly and a stated collection is not a prefix. A caller
|
|
34
|
+
that does want every part of a document sets `all_parts` on the reference, or
|
|
35
|
+
keeps `#matches?(other, ignore:)`.
|
|
36
|
+
|
|
37
|
+
- **`suffix` and `parts` are strict** (`lib/pubid/tgpp/identifier.rb`).
|
|
38
|
+
`TS 29.198 === TS 29.198-04-1` and `TR 00.01 === TR 00.01U` are both
|
|
39
|
+
false. `parts` is the flavor's `collection: true, initialize_empty: true`
|
|
40
|
+
attribute, so a part-less identifier holds `[]` on both construction
|
|
41
|
+
paths and the strict comparison treats that `[]` and a nil alike — the
|
|
42
|
+
same asymmetry the `initialize_empty` note above is about, seen from the
|
|
43
|
+
matching side. relaton had two committed specs breaking on this.
|
data/lib/pubid/tgpp/builder.rb
CHANGED
|
@@ -27,6 +27,20 @@ module Pubid
|
|
|
27
27
|
# Three-part version string, e.g. "2.0.0".
|
|
28
28
|
attribute :version, :string
|
|
29
29
|
|
|
30
|
+
# 3GPP's edition/version identity is split across `release` and
|
|
31
|
+
# `version`. `version` is already in the default
|
|
32
|
+
# `%i[date year edition version]` list; `release` is not, so it
|
|
33
|
+
# survived without_parts untouched and leaked into an "all parts"
|
|
34
|
+
# identity built via #to_all_parts.
|
|
35
|
+
def self.all_parts_edition_keys
|
|
36
|
+
super + %i[release]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A nil `suffix` and an empty `parts` mean the document has neither:
|
|
40
|
+
# `TS 29.198` is not `TS 29.198-04-1`, and `TR 00.01` is not
|
|
41
|
+
# `TR 00.01U`. A stated list is not a prefix either.
|
|
42
|
+
subset_strict :suffix, :parts
|
|
43
|
+
|
|
30
44
|
# Polymorphic type map for lutaml::Model key_value (de)serialization.
|
|
31
45
|
# Keys are the `pubid:3gpp:…` names produced by polymorphic_name (below).
|
|
32
46
|
TGPP_TYPE_MAP = {
|
data/lib/pubid/type_resolver.rb
CHANGED
|
@@ -15,6 +15,9 @@ module Pubid
|
|
|
15
15
|
class TypeResolver
|
|
16
16
|
TYPE_PREFIX = "pubid:"
|
|
17
17
|
SEGMENT_SEPARATOR = ":"
|
|
18
|
+
# Types of classes that belong to no flavor, so they carry no flavor
|
|
19
|
+
# segment. Class names are strings, so the lookup triggers the autoload.
|
|
20
|
+
SHARED_TYPES = { "pubid:all-parts" => "Pubid::AllPartsIdentifier" }.freeze
|
|
18
21
|
|
|
19
22
|
class << self
|
|
20
23
|
# @param type [String, nil] Polymorphic _type, e.g. "pubid:iso:technical-report".
|
|
@@ -23,6 +26,17 @@ module Pubid
|
|
|
23
26
|
def resolve(type)
|
|
24
27
|
return nil unless type.is_a?(String)
|
|
25
28
|
|
|
29
|
+
shared_class(type) || flavor_class(type)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def shared_class(type)
|
|
35
|
+
name = SHARED_TYPES[type]
|
|
36
|
+
name && Object.const_get(name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def flavor_class(type)
|
|
26
40
|
flavor_name = flavor_segment(type)
|
|
27
41
|
return nil unless flavor_name
|
|
28
42
|
|
|
@@ -43,8 +57,6 @@ module Pubid
|
|
|
43
57
|
nil
|
|
44
58
|
end
|
|
45
59
|
|
|
46
|
-
private
|
|
47
|
-
|
|
48
60
|
# The registered flavor whose module constant gives +segment+.
|
|
49
61
|
# Identifier.polymorphic_name takes the segment from the module name
|
|
50
62
|
# ("Pubid::CenCenelec" -> "cencenelec"), and a flavor can be registered
|
data/lib/pubid/un/builder.rb
CHANGED
data/lib/pubid/version.rb
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# W3C flavor notes
|
|
2
|
+
|
|
3
|
+
W3C slug attribute and index key.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/w3c/` or `spec/pubid/w3c/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **W3C renamed its slug attribute `code` → `number` (index key)**: a W3C identifier is a flat slug plus an optional verbatim date, and the slug used to live in `attribute :code, :string` while the `number` inherited from `::Pubid::Identifier` (a `Components::Code`) was never set. `Relaton::Index::Type#candidates_by_number` sorts and bsearches every row on `id.root.number.to_s`, so all **16,336** published `relaton-data-w3c` rows shared the empty key `""` and the binary search degenerated to a linear scan — silently, with no error. The slug *is* the key, so the attribute was **renamed** rather than shadowed by a derived reader: `attribute :number, :string` on `Pubid::W3c::Identifier`, `map "number", to: :number` in the `key_value` block, and the three readers moved (`Builder#build`'s `new(number:)`, `Renderer#render`, `UrnGenerator#generate`). **`code` is dropped with no alias** — one name for one value. Serialized vocabulary is `{_type, number}` / `{_type, number, date}`; `to_s` and the URN shape (`urn:w3c:<type>:<slug>[:<date>]`) are byte-identical, verified over the whole published corpus (0 empty/mismatched keys, 0 round-trip failures, 1933 buckets — was 1). **Why the declaration sits on the shared base here, when IEEE/IETF/IANA put theirs on the leaves**: this still redefines the parent's `attribute :number, Components::Code` as `:string` on a class the 11 concrete `Identifiers::*` inherit from — the recorded landmine — but the mechanism shows the hazard is load-order, not inheritance: lutaml's `inherited` hook **deep-dups** the parent's attribute table into each subclass at class-definition time (`serialize/initialization.rb#initialize_attrs`), so a subclass holds a **snapshot**, not a live view. A base-level override is therefore safe iff every subclass body opens *after* the base body has run — and Ruby resolves the superclass constant (autoloading `w3c/identifier.rb` and running it to completion) before opening any leaf body. W3C's base is **one file, one class body, never reopened**, so this holds in every load order; the `date` override of the parent's `Components::Date` has relied on the same property since the flavor landed. IEEE is the counter-shape: its `identifier.rb` defines nothing and the class body lives in `identifiers/base.rb`, so the base is reachable through two paths and a leaf can snapshot it half-built. **Split this base across two files and the landmine comes back.** `spec/pubid/w3c/root_number_spec.rb` carries the structural tripwire (the base *and* all 11 leaves must resolve `number` to `Lutaml::Model::Type::String`) and is only meaningful under the full `bundle exec rake`. **relaton note**: there is no alias and no published W3C pubid index yet, so nothing needs migrating — but `relaton/relaton`'s in-flight migration off the hand-rolled `Relaton::W3c::PubId` must read `id.number`, and its `DataFetcher`-built index must be crawled *after* this lands or every row keys on `""` again. **Known gap, deliberately NOT fixed here and recorded as a separate hand-off (w3c-mr-string):** `to_mr_string` **raises `NoMethodError` for every dated W3C id** — the base `mr_year` calls `date.year`, but W3C's `date` is a plain `:string` (opaque digit runs of varying width; the flavor already works around this for its public `#year` reader, but not for `mr_year`), and `mr_type`/`mr_publisher` are always nil (`Builder` never sets `typed_stage`; the publisher is a constant). This is pre-existing on `main`, untouched by the rename, and is a *second* identity surface — `to_slug` is an output **filename**. Measured over the 16,336 published rows: a year-only `mr_year` gives **1,936 collisions**; the whole verbatim `date` plus BIPM-style charset sanitisation (the corpus needs `/` and `+`) gives **16,336 distinct, filename-safe slugs, 0 collisions**. (hand-off: w3c-index-number; consumer follow-up: relaton__relaton__w3c-pubid-number-rename.)
|
data/lib/pubid/w3c/builder.rb
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# XSF flavor notes
|
|
2
|
+
|
|
3
|
+
XSF named documents (`XEP README`, `XEP xxxx`).
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/xsf/` or `spec/pubid/xsf/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **The XEP number is not always a number**: the whole XSF grammar is four rules, and the number used to be `match["0-9"].repeat(1)`. The XMPP extensions repository publishes two documents whose name is a word — the editor README (`xep-README.html`) and the template (`xep-xxxx.html`) — and both reach pubid as **real primary docids**, `XEP README` and `XEP xxxx`. They are rows of the published `relaton-data-xsf/index-v1.yaml`, backed by `data/xep-readme.yaml` (title "XEP Editor README") and `data/xep-xxxx.yaml` (title "XEP Template"). The digits-only rule rejected both at char 5, which cost two repositories a permanent workaround: `Relaton::Xsf::DataFetcher` carried `NON_DOCUMENTS = ["XEP README", "XEP xxxx"]` to swallow the parse failure in `add_to_index`, and `relaton-data-xsf/build_index_v1.rb` documented that its `index-v1` is deliberately **two rows longer** than `index-v2` (520 against 518) — one unparseable row makes `Relaton::Index` declare the whole file corrupt, delete it, and hand back an empty index, so the two rows could never enter v2. The grammar now admits the two names beside the digits: `(digits | special_number).as(:number)`, with `SPECIAL_NUMBERS = %w[README xxxx]` on `Pubid::Xsf::Parser`.
|
|
8
|
+
- **Two literals, not a slug rule, and the exact case only.** `XEP foo`, `XEP readme`, `XEP XXXX`, `XEP xxx` and `XEP xxxxx` all still raise `Pubid::Errors::ParseError`, pinned one by one in `spec/pubid/xsf/identifier_spec.rb`. Widening the number to any alphanumeric run would have been one character shorter and would have made XSF an anything-goes flavor after its prefix — the shape `Pubid.parse` probes for with `accepts_anything?` and then refuses to use as a fallback. A case-insensitive match would have made the parse *normalizing* (`XEP readme` → `XEP README`), so `to_s` would no longer equal the input and the fixture round-trip spec would need the `!input!rendered` marker for a form the corpus does not contain.
|
|
9
|
+
- **Nothing outside the parser changed, and that is the reason to store the name in `number`.** `number` is already `attribute :number, :string` on `Pubid::Xsf::Identifier` (a string to keep the zero padding), so `"README"` stores as it arrives; `Renderer#render` interpolates it, so `to_s` gives back `XEP README`; `UrnGenerator` gives `urn:xsf:xep:README` and `UrnParser` rebuilds the printed string and re-parses it, so the URN round-trip closes; `key_value` already maps `number`, so `from_hash(to_hash) == parse`; and `root.number` is non-empty, so the index key is sound for both. A separate attribute for the name would have broken every one of those.
|
|
10
|
+
- **relaton note**: once this ships, `Relaton::Xsf::DataFetcher` can drop `NON_DOCUMENTS` and the rescue branch that reads it, `relaton-data-xsf` needs its `index-v2` regenerated to carry all 520 rows, and the `build_index_v1.rb` comment asserting the two-row difference is then wrong. (hand-off: `relaton__relaton__xsf-named-documents-are-parseable`.)
|
|
11
|
+
- **Pre-existing gaps, untouched here.** XSF's MR slug carries no publisher segment — `Pubid::Xsf.parse("XEP 0001").to_slug == "0001"` — so it collides with any other flavor's bare-number slug; `lib/pubid/iana/CLAUDE.md` records XSF among the slug-only flavors that would surface this if `mr_string_spec` were made registry-driven. The fixture corpus is one hand-written file, `spec/fixtures/xsf/identifiers/pass/xep.txt`: XSF has no `identifiers/full/` directory and no `validation:classify` entry, so that file is edited by hand — the root `CLAUDE.md` rule against hand-editing `pass/` covers the classifier-driven flavors, which XSF is not. There is no `fail/` corpus at all.
|
data/lib/pubid/xsf/builder.rb
CHANGED
data/lib/pubid.rb
CHANGED
|
@@ -143,7 +143,10 @@ module Pubid
|
|
|
143
143
|
autoload :Parser, "pubid/parser"
|
|
144
144
|
autoload :Components, "pubid/components"
|
|
145
145
|
autoload :BundledIdentifier, "pubid/bundled_identifier"
|
|
146
|
+
autoload :AllParts, "pubid/all_parts"
|
|
147
|
+
autoload :AllPartsIdentifier, "pubid/all_parts_identifier"
|
|
146
148
|
autoload :Identifier, "pubid/identifier"
|
|
149
|
+
autoload :SubsetMatch, "pubid/subset_match"
|
|
147
150
|
autoload :IdentifierMetadata, "pubid/identifier_metadata"
|
|
148
151
|
autoload :Rendering, "pubid/rendering"
|
|
149
152
|
autoload :Renderers, "pubid/renderers"
|
|
@@ -486,15 +489,25 @@ module Pubid
|
|
|
486
489
|
private_class_method :routing_table
|
|
487
490
|
|
|
488
491
|
# True when +string+ starts with +prefix+ at a token boundary, so "ISO" does
|
|
489
|
-
# not claim "ISOFIX" and "BS" does not claim "BSI".
|
|
490
|
-
#
|
|
492
|
+
# not claim "ISOFIX" and "BS" does not claim "BSI". A boundary is the end
|
|
493
|
+
# of the string or any non-alphanumeric character, so a registered prefix
|
|
494
|
+
# can be followed by a space ("ISO 9001") or by a slash attaching a type
|
|
495
|
+
# token ("ISO/TR 25901-1:2016").
|
|
496
|
+
#
|
|
497
|
+
# Public (not +@api private+) so a flavor's own prefix routing - GOST's
|
|
498
|
+
# foreign-adoption lookup, currently the only caller outside this file -
|
|
499
|
+
# can reuse the exact boundary rule {parse_by_prefix} uses, rather than
|
|
500
|
+
# keeping a second copy that can drift out of sync.
|
|
501
|
+
#
|
|
502
|
+
# @param string [String]
|
|
503
|
+
# @param prefix [String]
|
|
504
|
+
# @return [Boolean]
|
|
491
505
|
def self.prefix_match?(string, prefix)
|
|
492
506
|
return false unless string.start_with?(prefix)
|
|
493
507
|
|
|
494
508
|
rest = string[prefix.length]
|
|
495
509
|
rest.nil? || !/[A-Za-z0-9]/.match?(rest)
|
|
496
510
|
end
|
|
497
|
-
private_class_method :prefix_match?
|
|
498
511
|
|
|
499
512
|
def self.detect_flavor_from_urn(urn)
|
|
500
513
|
# urn:iso:std:... → "iso"
|