pubid 2.0.0.pre.alpha.12 → 2.0.0.pre.alpha.14
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/data/parg/tables/bipm_groups.yaml +14 -0
- data/data/parg/tables/bipm_type_codes.yaml +5 -0
- data/data/parg/tables/bipm_type_names_en.yaml +6 -0
- data/data/parg/tables/bipm_type_names_fr.yaml +6 -0
- data/data/parg/tables/directives_supplements_typed_stages.yaml +3 -0
- data/data/parg/tables/directives_typed_stages.yaml +5 -0
- data/data/parg/tables/idf_typed_stages.yaml +27 -0
- data/data/parg/tables/idf_typed_stages_supplements.yaml +2 -0
- data/data/parg/tables/iec_typed_stages.yaml +130 -0
- data/data/parg/tables/iso_publishers.yaml +4 -0
- data/data/parg/tables/organizations.yaml +12 -0
- data/data/parg/tables/tc_types.yaml +42 -0
- data/data/parg/tables/typed_stages.yaml +114 -0
- data/data/parg/tables/typed_stages_supplements.yaml +64 -0
- data/data/parg/tables/wg_types.yaml +21 -0
- data/lib/pubid/adobe/builder.rb +2 -0
- data/lib/pubid/adobe/identifier.rb +11 -1
- 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 +11 -1
- 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/ansi/identifier.rb +1 -1
- data/lib/pubid/api/CLAUDE.md +23 -0
- data/lib/pubid/api/builder.rb +2 -0
- data/lib/pubid/api/identifier.rb +1 -1
- data/lib/pubid/api/parser.rb +8 -4
- data/lib/pubid/ashrae/CLAUDE.md +13 -0
- data/lib/pubid/ashrae/builder.rb +58 -14
- data/lib/pubid/ashrae/identifiers/base.rb +10 -1
- data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
- data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
- data/lib/pubid/ashrae/parser.rb +80 -39
- 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/identifier.rb +1 -1
- 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/identifier.rb +1 -1
- 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/bipm/identifier.rb +1 -1
- 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/single_identifier.rb +1 -3
- 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 +5 -1
- data/lib/pubid/ccsds/builder.rb +2 -0
- data/lib/pubid/ccsds/identifier.rb +9 -1
- 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 +12 -28
- 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 +20 -5
- 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/cie/identifier.rb +1 -1
- data/lib/pubid/cie/parser.rb +9 -2
- 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/conformance/checks.rb +1 -1
- data/lib/pubid/csa/CLAUDE.md +41 -0
- data/lib/pubid/csa/builder.rb +2 -0
- data/lib/pubid/csa/identifier.rb +19 -3
- data/lib/pubid/csa/parser.rb +25 -8
- 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/doi/identifier.rb +1 -1
- data/lib/pubid/easc/builder.rb +2 -0
- data/lib/pubid/easc/identifier.rb +10 -1
- data/lib/pubid/ecma/CLAUDE.md +28 -0
- data/lib/pubid/ecma/builder.rb +2 -0
- data/lib/pubid/ecma/identifier.rb +8 -1
- 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/base.rb +1 -1
- 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/evs.rb +1 -1
- data/lib/pubid/gb/CLAUDE.md +140 -0
- data/lib/pubid/gb/builder.rb +7 -2
- data/lib/pubid/gb/identifier.rb +6 -4
- 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 +16 -1
- data/lib/pubid/gost/parser.rb +8 -1
- data/lib/pubid/iala/CLAUDE.md +82 -0
- data/lib/pubid/iala/builder.rb +2 -0
- data/lib/pubid/iala/identifier.rb +10 -1
- data/lib/pubid/iana/CLAUDE.md +7 -0
- data/lib/pubid/iana/builder.rb +2 -0
- data/lib/pubid/iana/identifier.rb +1 -1
- data/lib/pubid/identifier.rb +161 -17
- data/lib/pubid/idf/builder.rb +11 -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 +8 -3
- data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iec/identifiers.rb +1 -0
- data/lib/pubid/iec/parser.rb +9 -4
- 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 +194 -27
- 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 +21 -1
- data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
- data/lib/pubid/ieee/identifiers/joint_development.rb +66 -23
- data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
- data/lib/pubid/ieee/parser.rb +156 -29
- data/lib/pubid/ieee/renderer.rb +42 -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/ietf/identifiers/base.rb +1 -1
- data/lib/pubid/iho/builder.rb +2 -0
- data/lib/pubid/isbn/builder.rb +2 -0
- data/lib/pubid/isbn/identifier.rb +1 -1
- 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 +10 -15
- 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/normalizer.rb +4 -1
- data/lib/pubid/iso/rendering_style.rb +0 -1
- data/lib/pubid/itu/CLAUDE.md +115 -0
- data/lib/pubid/itu/builder.rb +26 -4
- 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/base.rb +11 -18
- data/lib/pubid/itu/identifiers/radio_regulations.rb +27 -0
- data/lib/pubid/itu/identifiers/special_publication.rb +48 -14
- data/lib/pubid/itu/identifiers/standard_serialization.rb +2 -0
- data/lib/pubid/itu/identifiers/supplement.rb +15 -0
- data/lib/pubid/itu/identifiers.rb +1 -0
- data/lib/pubid/itu/parser.rb +108 -22
- data/lib/pubid/itu/urn_generator.rb +9 -2
- 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/jcgm.rb +1 -1
- data/lib/pubid/jis/builder.rb +5 -1
- data/lib/pubid/jis/identifier.rb +6 -18
- 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 +3 -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 +39 -7
- data/lib/pubid/nist/parser.rb +24 -2
- data/lib/pubid/nist/preprocessor.rb +53 -2
- data/lib/pubid/nist/urn_parser.rb +10 -1
- data/lib/pubid/oasis/CLAUDE.md +19 -0
- data/lib/pubid/oasis/builder.rb +2 -0
- data/lib/pubid/oasis/identifier.rb +20 -1
- data/lib/pubid/ogc/CLAUDE.md +34 -0
- data/lib/pubid/ogc/builder.rb +2 -0
- data/lib/pubid/ogc/identifier.rb +12 -1
- 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 +6 -1
- data/lib/pubid/omg/CLAUDE.md +15 -0
- data/lib/pubid/omg/builder.rb +2 -0
- data/lib/pubid/omg/identifier.rb +1 -1
- data/lib/pubid/parg/artifact.rb +46 -0
- data/lib/pubid/parg/backend.rb +92 -0
- data/lib/pubid/parg.rb +8 -0
- data/lib/pubid/parser/grammar.rb +23 -0
- data/lib/pubid/pg.rb +8 -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/plateau.rb +1 -2
- 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/sae/identifiers/base.rb +1 -1
- 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 +15 -1
- data/lib/pubid/type_resolver.rb +14 -2
- data/lib/pubid/un/builder.rb +2 -0
- data/lib/pubid/un/identifier.rb +1 -1
- 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/w3c/identifier.rb +1 -1
- data/lib/pubid/xsf/CLAUDE.md +11 -0
- data/lib/pubid/xsf/builder.rb +2 -0
- data/lib/pubid/xsf/identifier.rb +1 -1
- data/lib/pubid.rb +17 -3
- metadata +78 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pubid"
|
|
4
|
+
module Pubid
|
|
5
|
+
# Every part of one document, e.g. "ISO 9000 (all parts)".
|
|
6
|
+
#
|
|
7
|
+
# The class that includes this module holds part identifiers of one
|
|
8
|
+
# document in `identifiers`, in natural order and without duplicates. A
|
|
9
|
+
# member with no part stands for the whole document; it is permitted only
|
|
10
|
+
# as the sole member.
|
|
11
|
+
#
|
|
12
|
+
# A flavor includes it in a subclass of its own `Identifier`, so the
|
|
13
|
+
# all-parts identifier of a flavor is an identifier of that flavor
|
|
14
|
+
# (`Pubid::Iso::Identifiers::AllParts < Pubid::Iso::Identifier`). The
|
|
15
|
+
# subclass names its own suffix or URN; everything else comes from here.
|
|
16
|
+
# A flavor with no such class uses {Pubid::AllPartsIdentifier}.
|
|
17
|
+
#
|
|
18
|
+
# `==` compares the members. `===` compares the document only: the first
|
|
19
|
+
# member without its part and its edition (see #identity).
|
|
20
|
+
module AllParts
|
|
21
|
+
SUFFIX = " (all parts)"
|
|
22
|
+
|
|
23
|
+
# The members hold the whole identity, so the hash carries nothing else.
|
|
24
|
+
# Without the key_value block the serializer reads every inherited
|
|
25
|
+
# attribute, and the derived #number below would be written too.
|
|
26
|
+
def self.included(base)
|
|
27
|
+
base.attribute :identifiers, ::Pubid::Identifier, polymorphic: true,
|
|
28
|
+
collection: true,
|
|
29
|
+
initialize_empty: true
|
|
30
|
+
declare_key_value(base)
|
|
31
|
+
base.extend(ClassMethods)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# lutaml MERGES a subclass block onto the flavor's own block, so the
|
|
35
|
+
# derived #number below would be written as a top-level key. A no-op
|
|
36
|
+
# converter keeps it out.
|
|
37
|
+
def self.declare_key_value(base)
|
|
38
|
+
base.key_value do
|
|
39
|
+
map "_type", to: :_type
|
|
40
|
+
map "identifiers", to: :identifiers
|
|
41
|
+
map "number", with: { to: :number_to_kv, from: :number_from_kv }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module ClassMethods
|
|
46
|
+
# lutaml's from_hash builds the object with no attributes and assigns
|
|
47
|
+
# `identifiers` after #initialize, so it normalizes here too.
|
|
48
|
+
def from_hash(data, options = {})
|
|
49
|
+
super.tap do |id|
|
|
50
|
+
id.send(:normalize_identifiers!) if id.is_a?(::Pubid::AllParts)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(attrs = {}, options = {})
|
|
56
|
+
super
|
|
57
|
+
normalize_identifiers!
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The members hold the number, so it is neither written nor read.
|
|
61
|
+
def number_to_kv(_model, _doc); end
|
|
62
|
+
|
|
63
|
+
def number_from_kv(_model, _value); end
|
|
64
|
+
|
|
65
|
+
# @param other [Object] a candidate identifier
|
|
66
|
+
# @return [Boolean] true when +other+ is any part of the same document,
|
|
67
|
+
# in any edition
|
|
68
|
+
def ===(other)
|
|
69
|
+
return false unless other.is_a?(::Pubid::Identifier) && identity
|
|
70
|
+
|
|
71
|
+
identity === document_of(other)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Another all-parts identifier with +other+ added. The receiver does not
|
|
75
|
+
# change.
|
|
76
|
+
# @param other [Pubid::Identifier] a part of the same document, or
|
|
77
|
+
# another all-parts identifier of it
|
|
78
|
+
# @return [Pubid::Identifier] an all-parts identifier of the same class
|
|
79
|
+
# @raise [ArgumentError] for a different document, an identifier with no
|
|
80
|
+
# part, or a part that is already a member
|
|
81
|
+
def +(other)
|
|
82
|
+
members = other.is_a?(::Pubid::AllParts) ? other.identifiers : [other]
|
|
83
|
+
members.each { |member| validate_member!(member) }
|
|
84
|
+
self.class.new(identifiers: identifiers + members)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_s(**opts)
|
|
88
|
+
return "" unless identity
|
|
89
|
+
|
|
90
|
+
annotate_plain_render(
|
|
91
|
+
"#{identity.to_s(**opts.except(:annotated))}#{self.class::SUFFIX}",
|
|
92
|
+
**opts,
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The all-parts attributes are empty, so the flavor renderers cannot
|
|
97
|
+
# print it. Only the human form exists yet; `to_mr_string` and `to_slug`
|
|
98
|
+
# come through here and raise too.
|
|
99
|
+
def render(format: :human, **opts)
|
|
100
|
+
return to_s(**opts) if format == :human
|
|
101
|
+
|
|
102
|
+
raise NotImplementedError,
|
|
103
|
+
"an all-parts identifier has no #{format} form yet"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_urn
|
|
107
|
+
raise NotImplementedError, "an all-parts identifier has no URN yet"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_all_parts
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def root
|
|
115
|
+
identifiers.first&.root
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The document number, e.g. "9000". The index keys on it.
|
|
119
|
+
def number
|
|
120
|
+
identity&.number
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# True here and false for every other identifier, so a caller that reads
|
|
124
|
+
# the old flag still works.
|
|
125
|
+
# rubocop:disable Naming/PredicateMethod
|
|
126
|
+
def all_parts
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
# rubocop:enable Naming/PredicateMethod
|
|
130
|
+
|
|
131
|
+
def all_parts?
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def base_document
|
|
136
|
+
identifiers.first&.base_document
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
protected
|
|
140
|
+
|
|
141
|
+
# The document: the first member without its part, its subpart and its
|
|
142
|
+
# edition. #document_of reads it from another all-parts identifier, so it
|
|
143
|
+
# is protected and not private.
|
|
144
|
+
# @return [Pubid::Identifier, nil]
|
|
145
|
+
def identity
|
|
146
|
+
first = identifiers.first
|
|
147
|
+
first && document_of(first)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
# The document of +id+: +id+ without its part and its edition.
|
|
153
|
+
def document_of(id)
|
|
154
|
+
return id.identity if id.is_a?(::Pubid::AllParts)
|
|
155
|
+
|
|
156
|
+
id.without_parts(*id.class.all_parts_edition_keys)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def validate_member!(id)
|
|
160
|
+
unless id.is_a?(::Pubid::Identifier)
|
|
161
|
+
raise ArgumentError, "not an identifier: #{id.inspect}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
raise ArgumentError, "#{id} is not a part of #{self}" unless self === id
|
|
165
|
+
raise ArgumentError, "#{id} has no part" unless id.part?
|
|
166
|
+
return unless identifiers.any? { |member| same_member?(member, id) }
|
|
167
|
+
|
|
168
|
+
raise ArgumentError, "#{id} is already a member of #{self}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def same_member?(one, other)
|
|
172
|
+
one.normalized_copy == other.normalized_copy
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Sort, remove duplicates, and drop the whole-document member when a
|
|
176
|
+
# part is present. `==` and `to_hash` depend on this order.
|
|
177
|
+
def normalize_identifiers!
|
|
178
|
+
members = unique_members
|
|
179
|
+
parts = members.select(&:part?)
|
|
180
|
+
self.identifiers = sorted(parts.empty? ? members : parts)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def unique_members
|
|
184
|
+
identifiers.each_with_object([]) do |member, acc|
|
|
185
|
+
acc << member unless acc.any? { |kept| same_member?(kept, member) }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# One member needs no sort, and #to_s raises on an abstract class.
|
|
190
|
+
def sorted(members)
|
|
191
|
+
return members if members.size < 2
|
|
192
|
+
|
|
193
|
+
members.sort_by { |member| natural_key(member.to_s) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# "ISO 9000-10" sorts after "ISO 9000-2".
|
|
197
|
+
def natural_key(str)
|
|
198
|
+
str.split(/(\d+)/).map { |s| s.match?(/\A\d+\z/) ? [0, s.to_i] : [1, s] }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pubid"
|
|
4
|
+
module Pubid
|
|
5
|
+
# The all-parts identifier of a flavor that has no class of its own.
|
|
6
|
+
# A flavor that prints "(all parts)" or has a series URN gives its own
|
|
7
|
+
# class instead: see {Pubid::AllParts} and `Identifier.all_parts_class`.
|
|
8
|
+
class AllPartsIdentifier < Identifier
|
|
9
|
+
include AllParts
|
|
10
|
+
|
|
11
|
+
# This class belongs to no flavor, so its name carries no flavor segment
|
|
12
|
+
# and {TypeResolver} resolves it directly.
|
|
13
|
+
def self.polymorphic_name
|
|
14
|
+
return super unless self == ::Pubid::AllPartsIdentifier
|
|
15
|
+
|
|
16
|
+
"pubid:all-parts"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# AMCA flavor notes
|
|
2
|
+
|
|
3
|
+
AMCA index key and MR slug.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/amca/` or `spec/pubid/amca/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
## From the root note "AMCA / ASME / ASTM index key (`root.number`): three flavors, three different shapes"
|
|
8
|
+
|
|
9
|
+
**(1) AMCA — flat `:string` columns on the leaves, and a latent `==` bug fixed with them.** `code` is deleted and each of the three leaves declares `attribute :number, :string`. An earlier draft kept the inherited `Components::Code` instead (AMCA's own `Components::Code` resolved to `Pubid::Components::Code` — there is no `lib/pubid/amca/components/code.rb` — so nothing needed retyping, the CSA case); that works too, and the serialized bytes are identical either way, but the flat string matches the other flavors here and lets the custom `key_value` converters go entirely. **The converters were what hid a real bug.** `year` was declared `Components::Date`, yet the builder assigns a String and lutaml does not cast it — so the **parse** path held a String while **from_hash** produced a `Components::Date`. `to_hash` and `to_s` agreed, because `year_to_kv` normalised both shapes to a scalar, so the relaton index gate (`from_hash(to_hash) == to_hash`) passed — while **`parse(x) == from_hash(parse(x).to_hash)` was false for every AMCA id**, breaking `#matches?`, which is built on `==`. `year` is now `:string` too, both `with:` converters are gone (every mapped attribute is a plain scalar, and the canonical `to_hash` already drops empty and default values), and a spec asserts round-trip **equality**, not just hash equality — the stronger check that would have caught it. **Lesson: a `with:` converter that normalises two possible runtime shapes is a signal that the attribute type is wrong, not a fix.** The serialized key moved `code` → `number` (no consumer). **AMCA's `Publication` and `Interpretation` also lost hand-written keyword `initialize` methods** that assigned ivars directly and bypassed lutaml — costing them their `_type` (so `from_hash` could not route back), their `publisher` default, and the serialization of `revision`/`interpretation_code`, which were `attr_reader`s `to_hash` dropped. Both are now ordinary lutaml attributes; the builder already called `new(**attributes)` exactly as it does for `Standard`, which never had the problem. This is the AIEE/IRE/NESC migration applied again.
|
|
10
|
+
|
|
11
|
+
## Revision, rendering and URN (hand-off `asme-bpvc-and-amca-residue`)
|
|
12
|
+
|
|
13
|
+
**The publication revision never reached the object.** The grammar captured only `01` of `Rev. 01-23`, and `Builder#build_publication` read it under `parsed[:revision]`, where the grammar did not put it. The grammar now captures the whole `01-23` as `:revision`, so `revision`, `to_hash`, `to_s` and the URN (`rev.01-23`) carry it.
|
|
14
|
+
|
|
15
|
+
**The renderer lost text.** It printed `AMCA Publication 211 -22` (a space before the year), `AMCA 99 – JW` (no `Interp`), and `(2010)` for `(R2010)`. Every parseable AMCA fixture id now renders as it was written, except the real normalizations (`ANSI/AMCA 220-21` → `ANSI/AMCA Standard 220-21`).
|
|
16
|
+
|
|
17
|
+
**Every AMCA URN held a Ruby Hash.** `urn_type` interpolated the `type` metadata Hash (`copub.amca:{key: :publication, ...}`); it now reads `type[:key]`, the source `mr_type` reads. The URN also carries the interpretation code (`interp.jw`), so the interpretations of one standard no longer share a URN. `UrnParser` reads every token back (type, copublisher, interpretation code, revision, reaffirmation), so all 50 parseable fixture ids come back `==` from their URN; before, it rebuilt a bare `AMCA <number>-<year>` Standard.
|
|
18
|
+
|
|
19
|
+
**Not fixed, pre-existing:** `AMCA 204 – 1` and `ANSI/AMCA Standard 210-25 / ASHRAE 51-25` do not parse. `ANSI/AMCA 210-16 /ASHRAE 51-16` parses but silently drops `/ASHRAE 51-16`, because `additional_copublisher` has no capture. The `suffix` rule of `standard_identifier` has no capture either, so `suffix` is never set on a `Standard`.
|
|
20
|
+
|
|
21
|
+
## `all_parts_edition_keys`: Publication's `revision`, then the shared `reaffirmed`
|
|
22
|
+
|
|
23
|
+
`Identifier.all_parts_edition_keys` defaults to `%i[date year edition
|
|
24
|
+
version]`. A first pass fixed `Identifiers::Publication#revision` (the
|
|
25
|
+
`Rev. 01-23` token), a separate discriminator that list never covered, so
|
|
26
|
+
`"AMCA Publication 211-22 (Rev. 01-23)".to_all_parts` did not collapse
|
|
27
|
+
onto another revision of the same publication under `#to_all_parts`/`#===`.
|
|
28
|
+
Fixed with `Identifiers::Publication.all_parts_edition_keys` (`super +
|
|
29
|
+
%i[revision]`), declared on `Publication` itself rather than the shared
|
|
30
|
+
`Pubid::Amca::Identifier` base — `Standard`/`Interpretation` don't carry
|
|
31
|
+
`revision`.
|
|
32
|
+
|
|
33
|
+
Code review of that same change found a second, wider gap it had missed:
|
|
34
|
+
`reaffirmed` (the `"(R2010)"` year) is declared on the shared
|
|
35
|
+
`Pubid::Amca::Identifier` base (`identifiers/base.rb`), not on
|
|
36
|
+
`Publication`, and `Renderer#render_base` reads it for **every** leaf —
|
|
37
|
+
`Standard` as well as `Publication` — so `"AMCA 210-16 (R2010)"` failed to
|
|
38
|
+
collapse onto `"AMCA 210-16 (R2015)"` even though neither is a
|
|
39
|
+
`Publication`. Fixed on the shared base itself:
|
|
40
|
+
`Pubid::Amca::Identifier.all_parts_edition_keys` is `super +
|
|
41
|
+
%i[reaffirmed]`; `Publication`'s own override chains through `super` and
|
|
42
|
+
picks it up automatically. **Lesson**: a discriminator declared on a
|
|
43
|
+
shared base needs the override on that base, not repeated per leaf — and,
|
|
44
|
+
per the IEEE entry above, finding one missed attribute is not proof the
|
|
45
|
+
audit is complete. Not part of the original hand-off's audit list (which
|
|
46
|
+
didn't cover AMCA); found during code review of the OGC/3GPP/NIST fix.
|
|
47
|
+
Locked by `spec/pubid/all_parts_edition_keys_audit_spec.rb`.
|
data/lib/pubid/amca/builder.rb
CHANGED
|
@@ -154,11 +154,7 @@ module Pubid
|
|
|
154
154
|
|
|
155
155
|
# Extract revision (Rev. 01-23)
|
|
156
156
|
if parsed[:revision]
|
|
157
|
-
|
|
158
|
-
if revision_data && revision_data[:revision_year]
|
|
159
|
-
attributes[:revision] =
|
|
160
|
-
extract_value(revision_data[:revision_year])
|
|
161
|
-
end
|
|
157
|
+
attributes[:revision] = extract_value(parsed[:revision])
|
|
162
158
|
end
|
|
163
159
|
|
|
164
160
|
Identifiers::Publication.new(**attributes)
|
|
@@ -174,3 +170,5 @@ module Pubid
|
|
|
174
170
|
end
|
|
175
171
|
end
|
|
176
172
|
end
|
|
173
|
+
|
|
174
|
+
Pubid::Amca::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -17,7 +17,7 @@ module Pubid
|
|
|
17
17
|
raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
parsed =
|
|
20
|
+
parsed = Pubid::Parg::Backend.parse(:amca, identifier)
|
|
21
21
|
Builder.build(parsed)
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -87,6 +87,16 @@ module Pubid
|
|
|
87
87
|
def mr_year
|
|
88
88
|
year&.to_s
|
|
89
89
|
end
|
|
90
|
+
|
|
91
|
+
# `reaffirmed` (the "(R2010)" year, read by Renderer#render_base — used
|
|
92
|
+
# for Standard as well as Publication) is a discriminator the default
|
|
93
|
+
# %i[date year edition version] list misses, shared by every leaf that
|
|
94
|
+
# inherits this class. Declared here, not per-leaf, so Standard's own
|
|
95
|
+
# reaffirmations collapse too; Publication's revision (see
|
|
96
|
+
# publication.rb) is layered on top via `super`.
|
|
97
|
+
def self.all_parts_edition_keys
|
|
98
|
+
super + %i[reaffirmed]
|
|
99
|
+
end
|
|
90
100
|
end
|
|
91
101
|
end
|
|
92
102
|
end
|
|
@@ -28,6 +28,19 @@ module Pubid
|
|
|
28
28
|
def self.type
|
|
29
29
|
{ key: :publication, title: "Publication", short: nil }
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
# `revision` is Publication's edition/version discriminator and is
|
|
33
|
+
# not in the default %i[date year edition version] list, so
|
|
34
|
+
# #to_all_parts/#=== failed to collapse two revisions of the same
|
|
35
|
+
# publication (e.g. "AMCA Publication 211-22 (Rev. 01-23)" vs
|
|
36
|
+
# "... (Rev. 02-23)"). Declared here, not on the shared
|
|
37
|
+
# Pubid::Amca::Identifier base, because `revision` is
|
|
38
|
+
# Publication-only — Standard and Interpretation don't carry it.
|
|
39
|
+
# `super` also picks up `reaffirmed`, added on the shared base
|
|
40
|
+
# (identifiers/base.rb) since Standard carries that one too.
|
|
41
|
+
def self.all_parts_edition_keys
|
|
42
|
+
super + %i[revision]
|
|
43
|
+
end
|
|
31
44
|
end
|
|
32
45
|
end
|
|
33
46
|
end
|
data/lib/pubid/amca/parser.rb
CHANGED
|
@@ -62,7 +62,8 @@ module Pubid
|
|
|
62
62
|
|
|
63
63
|
# Publication revision (Rev. 01-23)
|
|
64
64
|
rule(:revision) do
|
|
65
|
-
lparen >> str("Rev") >> dot.maybe >> space >>
|
|
65
|
+
lparen >> str("Rev") >> dot.maybe >> space >>
|
|
66
|
+
(digits >> dash >> digits).as(:revision) >> rparen
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
# Interpretation code (JW, KB, RG, AW, AH, or just a number)
|
data/lib/pubid/amca/renderer.rb
CHANGED
|
@@ -29,53 +29,42 @@ module Pubid
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
31
|
def render_base(id)
|
|
32
|
-
parts = []
|
|
33
|
-
parts << id.copublisher if id.copublisher
|
|
34
32
|
t = id.class.respond_to?(:type) ? id.class.type : nil
|
|
35
|
-
if t.is_a?(Hash) && t[:title]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
parts << id.number.to_s
|
|
39
|
-
parts << "-#{id.year}" if id.year
|
|
40
|
-
|
|
41
|
-
result = parts.compact.join(" ")
|
|
42
|
-
|
|
43
|
-
if id.copublisher&.include?("/") && id.year
|
|
44
|
-
type_title = t.is_a?(Hash) ? t[:title].to_s : ""
|
|
45
|
-
result = "#{id.copublisher} #{type_title} #{id.number}-#{id.year}"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
result += " (#{id.reaffirmed})" if id.reaffirmed
|
|
49
|
-
|
|
33
|
+
title = t[:title].to_s if t.is_a?(Hash) && t[:title]
|
|
34
|
+
result = document(id, title)
|
|
35
|
+
result += " (R#{id.reaffirmed})" if id.reaffirmed
|
|
50
36
|
result
|
|
51
37
|
end
|
|
52
38
|
|
|
39
|
+
# The revision and the reaffirmation are separate optional groups in
|
|
40
|
+
# the grammar, so either or both can appear.
|
|
53
41
|
def render_publication(id)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
parts << "-#{id.year}" if id.year
|
|
59
|
-
parts << " (Rev. #{id.revision})" if id.revision
|
|
60
|
-
parts << " (#{id.reaffirmed})" if id.reaffirmed && !id.revision
|
|
61
|
-
|
|
62
|
-
parts.join(" ").squeeze(" ")
|
|
42
|
+
result = document(id, "Publication")
|
|
43
|
+
result += " (Rev. #{id.revision})" if id.revision
|
|
44
|
+
result += " (R#{id.reaffirmed})" if id.reaffirmed
|
|
45
|
+
result
|
|
63
46
|
end
|
|
64
47
|
|
|
65
48
|
def render_interpretation(id)
|
|
66
|
-
|
|
67
|
-
parts << id.copublisher if id.copublisher
|
|
68
|
-
parts << id.number.to_s
|
|
49
|
+
result = [id.copublisher, id.number.to_s].compact.join(" ")
|
|
69
50
|
|
|
70
51
|
if id.interpretation_code
|
|
71
|
-
|
|
52
|
+
result += " #{id.interpretation_code} Interp"
|
|
72
53
|
elsif id.year
|
|
73
|
-
|
|
54
|
+
result += " – #{id.year}"
|
|
55
|
+
else
|
|
56
|
+
result += " Interp"
|
|
74
57
|
end
|
|
75
58
|
|
|
76
|
-
|
|
59
|
+
result += " #{id.suffix}" if id.suffix
|
|
60
|
+
result
|
|
61
|
+
end
|
|
77
62
|
|
|
78
|
-
|
|
63
|
+
# "AMCA Standard 803-02": the year joins the number with a bare dash.
|
|
64
|
+
def document(id, title)
|
|
65
|
+
result = [id.copublisher, title, id.number.to_s].compact.join(" ")
|
|
66
|
+
result += "-#{id.year}" if id.year
|
|
67
|
+
result
|
|
79
68
|
end
|
|
80
69
|
end
|
|
81
70
|
end
|
|
@@ -27,9 +27,26 @@ module Pubid
|
|
|
27
27
|
"copub.#{identifier.copublisher.to_s.downcase}" if identifier.copublisher
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Identity-bearing: "AMCA 99 JW Interp" and "AMCA 99 KB Interp" are
|
|
31
|
+
# different documents.
|
|
32
|
+
def urn_interpretation
|
|
33
|
+
return nil unless identifier.respond_to?(:interpretation_code)
|
|
34
|
+
|
|
35
|
+
code = identifier.interpretation_code
|
|
36
|
+
"interp.#{code.downcase}" if code
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def urn_revision
|
|
40
|
+
revision = identifier.revision if identifier.respond_to?(:revision)
|
|
41
|
+
"rev.#{revision}" if revision
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `type` is a metadata Hash; interpolating it put a Ruby Hash literal
|
|
45
|
+
# into every AMCA URN. The key is the source Identifier#mr_type reads.
|
|
30
46
|
def urn_type
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
return nil unless identifier.class.respond_to?(:type)
|
|
48
|
+
|
|
49
|
+
identifier.class.type[:key]&.to_s
|
|
33
50
|
end
|
|
34
51
|
|
|
35
52
|
def generate
|
|
@@ -37,6 +54,8 @@ module Pubid
|
|
|
37
54
|
parts << urn_number if urn_number
|
|
38
55
|
parts << urn_year if urn_year
|
|
39
56
|
parts << urn_suffix if urn_suffix
|
|
57
|
+
parts << urn_interpretation if urn_interpretation
|
|
58
|
+
parts << urn_revision if urn_revision
|
|
40
59
|
parts << urn_reaffirmed if urn_reaffirmed
|
|
41
60
|
parts << urn_copublisher if urn_copublisher
|
|
42
61
|
|
|
@@ -5,23 +5,49 @@ module Pubid
|
|
|
5
5
|
# Parses AMCA URNs back into identifiers.
|
|
6
6
|
#
|
|
7
7
|
# UrnGenerator emits:
|
|
8
|
-
# urn:amca:{number}:{year}[:{
|
|
8
|
+
# urn:amca:{number}[:{year}][:{keyed tokens}][:{type}]
|
|
9
9
|
#
|
|
10
|
-
# The
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# The keyed tokens are `interp.<code>`, `rev.<revision>`,
|
|
11
|
+
# `reaff.<year>` and `copub.<publisher>`; the type is the key of the
|
|
12
|
+
# identifier class. The parser rebuilds the printed form from them and
|
|
13
|
+
# parses that, so the identifier comes back as the same class.
|
|
13
14
|
#
|
|
14
15
|
# Examples:
|
|
15
|
-
# - urn:amca:210:08
|
|
16
|
+
# - urn:amca:210:08 → AMCA 210-08
|
|
17
|
+
# - urn:amca:211:22:rev.01-23:copub.amca:publication
|
|
18
|
+
# → AMCA Publication 211-22 (Rev. 01-23)
|
|
19
|
+
# - urn:amca:99:interp.jw:copub.amca:interpretation → AMCA 99 JW Interp
|
|
16
20
|
class UrnParser < Pubid::UrnParser::Base
|
|
21
|
+
TYPE_TITLES = {
|
|
22
|
+
"standard" => "Standard",
|
|
23
|
+
"publication" => "Publication",
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
17
26
|
def parse_urn(urn)
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
number, *rest = split_parts(strip_namespace(urn))
|
|
28
|
+
year = rest.shift if rest.first&.match?(/\A\d+\z/)
|
|
29
|
+
keyed = rest.grep(/\./).to_h { |token| token.split(".", 2) }
|
|
30
|
+
type = (rest - keyed.map { |k, v| "#{k}.#{v}" }).last
|
|
31
|
+
|
|
32
|
+
flavor_parse(printed(number, year, keyed, type))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
def printed(number, year, keyed, type)
|
|
38
|
+
publisher = keyed.fetch("copub", "amca").upcase
|
|
39
|
+
return interpretation(publisher, number, keyed) if type == "interpretation"
|
|
40
|
+
|
|
41
|
+
text = [publisher, TYPE_TITLES[type], number].compact.join(" ")
|
|
23
42
|
text += "-#{year}" if year
|
|
24
|
-
|
|
43
|
+
text += " (Rev. #{keyed['rev']})" if keyed["rev"]
|
|
44
|
+
text += " (R#{keyed['reaff']})" if keyed["reaff"]
|
|
45
|
+
text
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def interpretation(publisher, number, keyed)
|
|
49
|
+
code = keyed["interp"]&.upcase
|
|
50
|
+
[publisher, number, code, "Interp"].compact.join(" ")
|
|
25
51
|
end
|
|
26
52
|
end
|
|
27
53
|
end
|
data/lib/pubid/ansi/builder.rb
CHANGED
|
@@ -45,6 +45,10 @@ module Pubid
|
|
|
45
45
|
value.to_s.split(",").map do |lang|
|
|
46
46
|
Components::Language.new(code: lang.strip)
|
|
47
47
|
end
|
|
48
|
+
when :all_parts
|
|
49
|
+
# The shared Grammar marks the tree; the prepended AllPartsWrap
|
|
50
|
+
# routes the wrap, so the marker itself carries no component.
|
|
51
|
+
nil
|
|
48
52
|
else
|
|
49
53
|
raise ArgumentError, "Unknown parameter type: #{type}"
|
|
50
54
|
end
|
|
@@ -52,3 +56,5 @@ module Pubid
|
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
end
|
|
59
|
+
|
|
60
|
+
Pubid::Ansi::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# API flavor notes
|
|
2
|
+
|
|
3
|
+
API rendering repairs and the `number` retype residue.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/api/` or `spec/pubid/api/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
## From the root note "`number`/`part`/`subpart` retyped to `:string` — tranche 1 of 3 (ansi, api, bsi, cen_cenelec, idf, jcgm)"
|
|
8
|
+
|
|
9
|
+
**The six flavors have no fixture net of their own**: every one of their `fixtures_spec.rb` files reports **0 examples** (the `../../../fixtures` glob has one `..` too many; api and idf also use an uppercase directory — hand-off `ten-dead-fixture-specs`), and ANSI's sole assertion is `expect(fixtures.size).to be > 0`. Fixing those globs would drag in unrelated stale-fixture failures, so the new spec's corpus sweep reads the same files directly — the ashrae/csa/etsi/oiml precedent. **One pre-existing defect repaired in passing**: `API COS 1-07/RP 75, 4th edition` put a raw `Parslet::Slice` into `root.number` (the hand-off warned it would look like a migration failure); `Api::Builder#cast` now returns `value.to_s`, so it is a real String. **One severe API defect repaired here — the ONLY deliberate `to_s` change in this branch.** `Pubid::Api::Identifier.parse("API RP 500").to_s` was `"API"`: **163 of API's 193 corpus ids rendered as the bare publisher**, and since `to_s` is the document number *and* the output filename, all 163 collapsed onto one another. Two independent **dead reads**, both found by pulling on the `code` thread this retype exposed: (1) `Api::Renderer#code_portion` read an `id.code` attribute that **nothing ever assigns** — the parser emits no `:code` key and `Builder#cast` never sets one — while the number the builder does populate sits in `number`; (2) the type token was gated on `id.class.attributes.key?(:type_string)`, but `type_string` is a plain **method** on each leaf, never a lutaml attribute, so that guard was always false. The fix is two reads (`number`, and `respond_to?(:type_string)`), plus deleting the `code` attribute and the three dead `code_portion` methods (`SingleIdentifier`'s and `Mpms`'s were never called at all — the renderer has its own). **`render_mpms` never consulted `code_portion`, which is exactly why MPMS was the one form that always rendered correctly.** Byte-exact `to_s` goes from **29/193 to 190/193**, and the 3 residuals are precisely the normalizing parses the generated fixtures already mark `!input!rendered`. **The strongest evidence that this restores intent rather than inventing output: regenerating the fixtures (`rake "validation:classify[api]"`) changed exactly ONE line** — a stale `!… !API` corrected to `!… !API 75-07`. The corpus had recorded the correct renderings all along; the code had drifted away from them silently, because API's `fixtures_spec.rb` matches no files and reports 0 examples and its other specs are `respond_to?(:parse)` stubs. Re-adding a `code` attribute would silently reopen the defect, so `spec/pubid/api/rendering_gaps_spec.rb` asserts it is absent from the whole hierarchy. **Note `maybe(:code)` in `urn_generator/base.rb#urn_number` is safe** — `maybe` guards on `attributes.key?`, so the removed attribute simply never matches. Measured over the corpus, the repair is **purely additive**: of 193 ids, 163 grew and **0 shrank**, and every new rendering *extends* the old one (`to_urn`, `to_hash` and `root.number` are untouched) — the ASHRAE invariant, which is stronger than byte-identity for a fix. **An MR gap this branch pinned was closed mid-flight by PR #354, and the pin is what caught it** — a worked example of why these "assert the CURRENT behaviour" blocks earn their place. When written, all **30** MPMS ids slugged to the bare `"api"` because MPMS kept its locator in a `chapter` attribute and had no `number` at all. #354 then moved the chapter into the inherited `number` (it *is* the document number) and this branch retyped that `number` to `:string`; **composed, they close the collapse** — 0 ids now slug to bare `"api"`, and `API MPMS CH 12.2` keys `"12"` as a String. Merging `main` turned the pin red exactly as designed, and it was rewritten to assert the repair. **A narrower gap remains and is re-pinned**: the MR slug is built from `number` alone, so the sections of one chapter still share it (`CH 14.4` and `CH 14.6` both → `api.14`, 11 colliding slugs corpus-wide, and `to_slug` is a filename). Closing that needs an `Mpms#mr_number_with_part` appending section/subsection (the BIPM/OIML shape). **Merge note for whoever does tranche 2/3**: #354's `Builder#handle_key` routes the MPMS `:chapter` parse key into the `number` *attribute*, and `assign_attributes` hands `handle_key` the **already-cast** value — so the `cast` arm must be `when :number, :chapter then value.to_s`, not a `Components::Code`, or MPMS silently regains a boxed number. (hand-off: retype-number-to-string; remaining: tranche 2 = CSA, tranche 3 = ISO and NIST on separate branches, then flip `identifier.rb:136-138` and delete the leaf `:string` redeclarations and the twelve tripwire blocks.)
|
|
10
|
+
|
|
11
|
+
## From the root note "Parse-failure error contract — uniform across every flavor"
|
|
12
|
+
|
|
13
|
+
**`Identifier.parse` was the one public parse in the gem that could return `nil`.** `lib/pubid/api/identifier.rb` opened with
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
return nil if input.start_with?("#") # "Filter out comments"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
a **fixture-file concern living in a public API**. A caller passing a `#`-leading string got `nil` back and discovered it as a `NoMethodError` on `.to_s`, far from the cause. `lib/pubid/csa/identifier.rb` even carried a comment noting that every flavor "but `api`" honoured the raise-or-return-an-identifier contract, so this was known and unfixed.
|
|
20
|
+
|
|
21
|
+
Deleting it cost nothing: **both** API fixture loaders already reject `#` lines themselves (`spec/pubid/api/fixtures_spec.rb`, `spec/pubid/api/root_number_spec.rb`), so nothing depended on the nil. A comment line is not an identifier and now raises `Parslet::ParseFailed` like any other unparseable string. The `rescue Parslet::ParseFailed => e; raise e` that followed it was a no-op and went too, and API gained the input guards it had never had (it was one of the 14 flavors an over-long string reached the parser in).
|
|
22
|
+
|
|
23
|
+
**Rule this illustrates:** a filter that exists for *how the test corpus is stored* belongs in the corpus loader, never in `parse`.
|
data/lib/pubid/api/builder.rb
CHANGED
data/lib/pubid/api/identifier.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Pubid
|
|
|
34
34
|
# this was the one public parse in the gem that could return nil, which
|
|
35
35
|
# a caller reading `id.to_s` sees as a NoMethodError far from the cause.
|
|
36
36
|
# Both API fixture loaders already drop `#` lines themselves.
|
|
37
|
-
tree =
|
|
37
|
+
tree = Pubid::Parg::Backend.parse(:api, Api::Parser.normalize_input(input))
|
|
38
38
|
Builder.new.build(tree)
|
|
39
39
|
end
|
|
40
40
|
end
|
data/lib/pubid/api/parser.rb
CHANGED
|
@@ -129,11 +129,15 @@ module Pubid
|
|
|
129
129
|
|
|
130
130
|
root(:identifier)
|
|
131
131
|
|
|
132
|
-
#
|
|
133
|
-
|
|
132
|
+
# Pre-parse ingestion normalization (R2): every parse path feeds
|
|
133
|
+
# the grammar the same normalized string.
|
|
134
|
+
def self.normalize_input(input)
|
|
134
135
|
# Normalize MPMP typo to MPMS
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
input.gsub("API MPMP", "API MPMS")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def parse(input)
|
|
140
|
+
super(self.class.normalize_input(input))
|
|
137
141
|
end
|
|
138
142
|
end
|
|
139
143
|
end
|