relaton 3.0.0.pre.alpha.1 → 3.0.0.pre.alpha.2

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.
Files changed (270) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +57 -1
  3. data/bin/console +0 -1
  4. data/lib/relaton/3gpp/bibliography.rb +82 -7
  5. data/lib/relaton/3gpp/data_fetcher.rb +51 -3
  6. data/lib/relaton/3gpp/docidentifier.rb +114 -0
  7. data/lib/relaton/3gpp/item.rb +6 -0
  8. data/lib/relaton/3gpp/parser.rb +1 -1
  9. data/lib/relaton/3gpp/processor.rb +4 -1
  10. data/lib/relaton/3gpp.rb +5 -1
  11. data/lib/relaton/adobe/bibdata.rb +8 -0
  12. data/lib/relaton/adobe/bibitem.rb +8 -0
  13. data/lib/relaton/adobe/bibliography.rb +92 -0
  14. data/lib/relaton/adobe/docidentifier.rb +49 -0
  15. data/lib/relaton/adobe/doctype.rb +14 -0
  16. data/lib/relaton/adobe/ext.rb +32 -0
  17. data/lib/relaton/adobe/item.rb +15 -0
  18. data/lib/relaton/adobe/item_base.rb +18 -0
  19. data/lib/relaton/adobe/item_data.rb +6 -0
  20. data/lib/relaton/adobe/processor.rb +45 -0
  21. data/lib/relaton/adobe/util.rb +8 -0
  22. data/lib/relaton/adobe.rb +37 -0
  23. data/lib/relaton/bib/model/address.rb +2 -2
  24. data/lib/relaton/bib/model/docidentifier.rb +24 -9
  25. data/lib/relaton/bib/model/localized_string.rb +1 -1
  26. data/lib/relaton/bib/model/structured_identifier.rb +10 -9
  27. data/lib/relaton/bib/sanitizer.rb +202 -6
  28. data/lib/relaton/bib.rb +0 -2
  29. data/lib/relaton/bipm/bibliography.rb +159 -10
  30. data/lib/relaton/bipm/data_fetcher.rb +26 -2
  31. data/lib/relaton/bipm/data_outcomes_parser.rb +3 -4
  32. data/lib/relaton/bipm/id_parser.rb +5 -4
  33. data/lib/relaton/bipm/model/structured_identifier.rb +21 -0
  34. data/lib/relaton/bipm/processor.rb +2 -2
  35. data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +2 -4
  36. data/lib/relaton/bipm/si_brochure_parser.rb +5 -3
  37. data/lib/relaton/bipm.rb +7 -1
  38. data/lib/relaton/bsi/bibliography.rb +115 -43
  39. data/lib/relaton/bsi/hit.rb +14 -0
  40. data/lib/relaton/bsi/hit_collection.rb +15 -16
  41. data/lib/relaton/bsi/model/docidentifier.rb +99 -1
  42. data/lib/relaton/bsi/processor.rb +1 -0
  43. data/lib/relaton/calconnect/bibliography.rb +12 -14
  44. data/lib/relaton/calconnect/data_fetcher.rb +77 -9
  45. data/lib/relaton/calconnect/docidentifier.rb +80 -0
  46. data/lib/relaton/calconnect/hit_collection.rb +65 -57
  47. data/lib/relaton/calconnect/model/item.rb +7 -0
  48. data/lib/relaton/calconnect/processor.rb +7 -1
  49. data/lib/relaton/calconnect.rb +11 -1
  50. data/lib/relaton/ccsds/data/fetcher.rb +17 -12
  51. data/lib/relaton/ccsds/data/parser.rb +1 -1
  52. data/lib/relaton/ccsds/hit_collection.rb +6 -1
  53. data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
  54. data/lib/relaton/ccsds/model/item.rb +2 -0
  55. data/lib/relaton/cen/bibliography.rb +75 -47
  56. data/lib/relaton/cen/hit.rb +16 -1
  57. data/lib/relaton/cen/hit_collection.rb +59 -11
  58. data/lib/relaton/cen/model/docidentifier.rb +92 -1
  59. data/lib/relaton/cen/processor.rb +13 -8
  60. data/lib/relaton/cen/scraper.rb +13 -5
  61. data/lib/relaton/cen.rb +1 -0
  62. data/lib/relaton/cie/data_fetcher.rb +215 -30
  63. data/lib/relaton/cie/processor.rb +3 -1
  64. data/lib/relaton/cie/scrapper.rb +15 -2
  65. data/lib/relaton/cie.rb +2 -1
  66. data/lib/relaton/core/data_fetcher.rb +150 -3
  67. data/lib/relaton/core/governor.rb +320 -0
  68. data/lib/relaton/core/pacer.rb +134 -0
  69. data/lib/relaton/core/processor.rb +19 -0
  70. data/lib/relaton/core/request_error.rb +14 -0
  71. data/lib/relaton/core.rb +3 -0
  72. data/lib/relaton/db/registry.rb +41 -1
  73. data/lib/relaton/doi/crossref.rb +19 -2
  74. data/lib/relaton/doi/parser.rb +109 -15
  75. data/lib/relaton/easc/bibdata.rb +8 -0
  76. data/lib/relaton/easc/bibitem.rb +8 -0
  77. data/lib/relaton/easc/bibliography.rb +95 -0
  78. data/lib/relaton/easc/docidentifier.rb +100 -0
  79. data/lib/relaton/easc/doctype.rb +14 -0
  80. data/lib/relaton/easc/ext.rb +44 -0
  81. data/lib/relaton/easc/item.rb +13 -0
  82. data/lib/relaton/easc/item_base.rb +18 -0
  83. data/lib/relaton/easc/item_data.rb +6 -0
  84. data/lib/relaton/easc/processor.rb +46 -0
  85. data/lib/relaton/easc/util.rb +8 -0
  86. data/lib/relaton/easc.rb +35 -0
  87. data/lib/relaton/ecma/bibliography.rb +93 -25
  88. data/lib/relaton/ecma/data_fetcher.rb +71 -12
  89. data/lib/relaton/ecma/docidentifier.rb +124 -0
  90. data/lib/relaton/ecma/item.rb +2 -0
  91. data/lib/relaton/ecma/memento_parser.rb +1 -1
  92. data/lib/relaton/ecma/page_fetcher.rb +15 -3
  93. data/lib/relaton/ecma/parser_common.rb +2 -2
  94. data/lib/relaton/ecma/processor.rb +4 -1
  95. data/lib/relaton/ecma/standard_parser.rb +2 -2
  96. data/lib/relaton/ecma.rb +10 -1
  97. data/lib/relaton/etsi/bibliography.rb +67 -2
  98. data/lib/relaton/etsi/data_fetcher.rb +43 -4
  99. data/lib/relaton/etsi/processor.rb +3 -1
  100. data/lib/relaton/etsi.rb +2 -1
  101. data/lib/relaton/gb/bibliography.rb +55 -29
  102. data/lib/relaton/gb/docidentifier.rb +58 -9
  103. data/lib/relaton/gb/processor.rb +3 -0
  104. data/lib/relaton/gb/scraper.rb +27 -10
  105. data/lib/relaton/gost/bibdata.rb +8 -0
  106. data/lib/relaton/gost/bibitem.rb +8 -0
  107. data/lib/relaton/gost/bibliography.rb +107 -0
  108. data/lib/relaton/gost/docidentifier.rb +80 -0
  109. data/lib/relaton/gost/doctype.rb +16 -0
  110. data/lib/relaton/gost/ext.rb +46 -0
  111. data/lib/relaton/gost/item.rb +15 -0
  112. data/lib/relaton/gost/item_base.rb +18 -0
  113. data/lib/relaton/gost/item_data.rb +6 -0
  114. data/lib/relaton/gost/processor.rb +49 -0
  115. data/lib/relaton/gost/util.rb +8 -0
  116. data/lib/relaton/gost.rb +36 -0
  117. data/lib/relaton/iala/bibdata.rb +8 -0
  118. data/lib/relaton/iala/bibitem.rb +8 -0
  119. data/lib/relaton/iala/bibliography.rb +146 -0
  120. data/lib/relaton/iala/docidentifier.rb +89 -0
  121. data/lib/relaton/iala/doctype.rb +18 -0
  122. data/lib/relaton/iala/ext.rb +32 -0
  123. data/lib/relaton/iala/item.rb +21 -0
  124. data/lib/relaton/iala/item_base.rb +18 -0
  125. data/lib/relaton/iala/item_data.rb +6 -0
  126. data/lib/relaton/iala/processor.rb +43 -0
  127. data/lib/relaton/iala/relation.rb +7 -0
  128. data/lib/relaton/iala/util.rb +8 -0
  129. data/lib/relaton/iala.rb +35 -0
  130. data/lib/relaton/iana/bibliography.rb +67 -14
  131. data/lib/relaton/iana/data_fetcher.rb +35 -5
  132. data/lib/relaton/iana/processor.rb +3 -1
  133. data/lib/relaton/iana.rb +12 -1
  134. data/lib/relaton/iec/data_fetcher.rb +7 -1
  135. data/lib/relaton/iec/hit_collection.rb +1 -1
  136. data/lib/relaton/iec/model/docidentifier.rb +9 -5
  137. data/lib/relaton/iec/model/ext.rb +2 -2
  138. data/lib/relaton/iec/processor.rb +1 -0
  139. data/lib/relaton/ieee/bibliography.rb +25 -3
  140. data/lib/relaton/ieee/data_fetcher.rb +158 -17
  141. data/lib/relaton/ieee/idams_parser.rb +18 -11
  142. data/lib/relaton/ieee/processor.rb +4 -1
  143. data/lib/relaton/ieee/rawbib_id_parser.rb +291 -86
  144. data/lib/relaton/ieee.rb +2 -1
  145. data/lib/relaton/ietf/data_fetcher.rb +295 -12
  146. data/lib/relaton/ietf/processor.rb +7 -3
  147. data/lib/relaton/ietf/rfc/entry.rb +39 -3
  148. data/lib/relaton/ietf/scraper.rb +69 -36
  149. data/lib/relaton/ietf.rb +4 -1
  150. data/lib/relaton/iho/bibliography.rb +1 -1
  151. data/lib/relaton/iho/docidentifier.rb +1 -1
  152. data/lib/relaton/index/file_io.rb +11 -11
  153. data/lib/relaton/index/file_storage.rb +6 -1
  154. data/lib/relaton/index/pool.rb +6 -1
  155. data/lib/relaton/index/shard_source.rb +201 -0
  156. data/lib/relaton/index/type.rb +63 -12
  157. data/lib/relaton/index.rb +2 -1
  158. data/lib/relaton/iso/bibliography.rb +20 -15
  159. data/lib/relaton/iso/data_fetcher.rb +3 -3
  160. data/lib/relaton/iso/data_parser.rb +17 -3
  161. data/lib/relaton/iso/hit_collection.rb +27 -15
  162. data/lib/relaton/iso/item_data.rb +22 -0
  163. data/lib/relaton/iso/model/docidentifier.rb +24 -12
  164. data/lib/relaton/iso/processor.rb +1 -0
  165. data/lib/relaton/iso/scraper.rb +19 -3
  166. data/lib/relaton/itu/bibliography.rb +9 -4
  167. data/lib/relaton/itu/data_crawler_r.rb +664 -0
  168. data/lib/relaton/itu/data_fetcher.rb +496 -50
  169. data/lib/relaton/itu/data_merge_r.rb +149 -0
  170. data/lib/relaton/itu/data_parser_r.rb +163 -89
  171. data/lib/relaton/itu/data_parser_t.rb +228 -0
  172. data/lib/relaton/itu/family_cache.rb +177 -0
  173. data/lib/relaton/itu/governor.rb +56 -0
  174. data/lib/relaton/itu/hit.rb +9 -3
  175. data/lib/relaton/itu/hit_collection.rb +258 -86
  176. data/lib/relaton/itu/model/docidentifier.rb +67 -1
  177. data/lib/relaton/itu/model/structured_identifier.rb +19 -0
  178. data/lib/relaton/itu/processor.rb +10 -4
  179. data/lib/relaton/itu/pubid.rb +27 -5
  180. data/lib/relaton/itu/recommendation_fields.rb +334 -0
  181. data/lib/relaton/itu/recommendation_parser.rb +18 -149
  182. data/lib/relaton/itu/scraper.rb +13 -3
  183. data/lib/relaton/itu.rb +2 -1
  184. data/lib/relaton/jcgm/bibdata.rb +8 -0
  185. data/lib/relaton/jcgm/bibitem.rb +8 -0
  186. data/lib/relaton/jcgm/bibliography.rb +97 -0
  187. data/lib/relaton/jcgm/data_fetcher.rb +81 -0
  188. data/lib/relaton/jcgm/docidentifier.rb +102 -0
  189. data/lib/relaton/jcgm/doctype.rb +12 -0
  190. data/lib/relaton/jcgm/ext.rb +23 -0
  191. data/lib/relaton/jcgm/item.rb +20 -0
  192. data/lib/relaton/jcgm/item_base.rb +18 -0
  193. data/lib/relaton/jcgm/item_data.rb +6 -0
  194. data/lib/relaton/jcgm/meetings_parser.rb +175 -0
  195. data/lib/relaton/jcgm/processor.rb +71 -0
  196. data/lib/relaton/jcgm/relation.rb +9 -0
  197. data/lib/relaton/jcgm/structured_identifier.rb +40 -0
  198. data/lib/relaton/jcgm/util.rb +8 -0
  199. data/lib/relaton/jcgm.rb +24 -0
  200. data/lib/relaton/jis/bibliography.rb +8 -10
  201. data/lib/relaton/jis/data_fetcher.rb +21 -19
  202. data/lib/relaton/jis/docidentifier.rb +104 -5
  203. data/lib/relaton/jis/hit.rb +18 -23
  204. data/lib/relaton/jis/hit_collection.rb +19 -18
  205. data/lib/relaton/jis/processor.rb +1 -1
  206. data/lib/relaton/jis.rb +2 -3
  207. data/lib/relaton/logger/channels/gh_issue.rb +78 -13
  208. data/lib/relaton/nist/data_fetcher.rb +63 -13
  209. data/lib/relaton/nist/docidentifier.rb +165 -0
  210. data/lib/relaton/nist/item.rb +2 -0
  211. data/lib/relaton/nist/item_base.rb +16 -0
  212. data/lib/relaton/nist/mods_parser.rb +38 -12
  213. data/lib/relaton/nist/processor.rb +2 -1
  214. data/lib/relaton/nist/relation.rb +3 -0
  215. data/lib/relaton/nist/scraper.rb +6 -3
  216. data/lib/relaton/oasis/bibliography.rb +147 -6
  217. data/lib/relaton/oasis/data_fetcher.rb +41 -5
  218. data/lib/relaton/oasis/data_parser_utils.rb +37 -3
  219. data/lib/relaton/oasis/docidentifier.rb +54 -0
  220. data/lib/relaton/oasis/item.rb +3 -0
  221. data/lib/relaton/oasis/processor.rb +7 -1
  222. data/lib/relaton/oasis.rb +14 -1
  223. data/lib/relaton/ogc/data_fetcher.rb +23 -2
  224. data/lib/relaton/ogc/docidentifier.rb +105 -0
  225. data/lib/relaton/ogc/hit_collection.rb +78 -3
  226. data/lib/relaton/ogc/processor.rb +2 -1
  227. data/lib/relaton/ogc.rb +5 -1
  228. data/lib/relaton/oiml/bibliography.rb +90 -15
  229. data/lib/relaton/oiml/docidentifier.rb +18 -3
  230. data/lib/relaton/omg/docidentifier.rb +67 -0
  231. data/lib/relaton/omg/item.rb +1 -0
  232. data/lib/relaton/omg/processor.rb +1 -0
  233. data/lib/relaton/omg/scraper.rb +61 -16
  234. data/lib/relaton/omg.rb +1 -0
  235. data/lib/relaton/plateau/bibliography.rb +10 -3
  236. data/lib/relaton/plateau/data_fetcher.rb +25 -2
  237. data/lib/relaton/plateau/handbook_parser.rb +8 -1
  238. data/lib/relaton/plateau/hit.rb +10 -2
  239. data/lib/relaton/plateau/hit_collection.rb +31 -11
  240. data/lib/relaton/plateau/processor.rb +3 -1
  241. data/lib/relaton/plateau/technical_report_parser.rb +8 -1
  242. data/lib/relaton/plateau.rb +2 -1
  243. data/lib/relaton/sdo/config.rb +34 -0
  244. data/lib/relaton/sdo/fetcher.rb +52 -0
  245. data/lib/relaton/sdo/logo.rb +95 -0
  246. data/lib/relaton/sdo/name.rb +26 -0
  247. data/lib/relaton/sdo/organization.rb +71 -0
  248. data/lib/relaton/sdo/store.rb +49 -0
  249. data/lib/relaton/sdo.rb +29 -0
  250. data/lib/relaton/version.rb +1 -1
  251. data/lib/relaton/w3c/bibliography.rb +132 -12
  252. data/lib/relaton/w3c/data_fetcher.rb +194 -16
  253. data/lib/relaton/w3c/data_parser.rb +3 -3
  254. data/lib/relaton/w3c/docidentifier.rb +48 -0
  255. data/lib/relaton/w3c/governor.rb +32 -0
  256. data/lib/relaton/w3c/item.rb +3 -0
  257. data/lib/relaton/w3c/pubid.rb +12 -0
  258. data/lib/relaton/w3c/safe_realize.rb +110 -21
  259. data/lib/relaton/w3c.rb +12 -1
  260. data/lib/relaton/xsf/bibliography.rb +61 -1
  261. data/lib/relaton/xsf/data_fetcher.rb +55 -5
  262. data/lib/relaton/xsf/docidentifier.rb +46 -0
  263. data/lib/relaton/xsf/hit_collection.rb +31 -3
  264. data/lib/relaton/xsf/item.rb +6 -0
  265. data/lib/relaton/xsf/processor.rb +1 -0
  266. data/lib/relaton/xsf.rb +5 -1
  267. data/lib/relaton.rb +42 -0
  268. metadata +135 -24
  269. data/lib/relaton/ieee/pub_id.rb +0 -161
  270. data/lib/relaton/index/id_number.rb +0 -30
@@ -64,9 +64,12 @@ module Relaton
64
64
  # @param hit [Hash]
65
65
  # @return [Array<Bib::Docidentifier>]
66
66
  def fetch_docid(hit)
67
- ids = [Bib::Docidentifier.new(content: hit[:code], type: "NIST", primary: true)]
67
+ # All ids share the Nist::Docidentifier collection type so YAML
68
+ # serialization stays valid; the DOI is not canonical NIST pubid form,
69
+ # so the class keeps it as the raw string (see Docidentifier#content=).
70
+ ids = [Docidentifier.new(content: hit[:code], type: "NIST", primary: true)]
68
71
  doi = hit[:json]["doi"]&.split("/")&.last
69
- ids << Bib::Docidentifier.new(content: doi, type: "DOI") if doi
72
+ ids << Docidentifier.new(content: doi, type: "DOI") if doi
70
73
  ids
71
74
  end
72
75
 
@@ -262,7 +265,7 @@ module Relaton
262
265
  t = "obsoletes"
263
266
  else t = type
264
267
  end
265
- ids = [Bib::Docidentifier.new(content: ref, type: "NIST", primary: true)]
268
+ ids = [Docidentifier.new(content: ref, type: "NIST", primary: true)]
266
269
  link = [Bib::Uri.new(type: "src", content: uri)]
267
270
  bib = ItemData.new(formattedref: Bib::Formattedref.new(content: ref), source: link, docidentifier: ids)
268
271
  Relation.new(type: t, description: descr, bibitem: bib)
@@ -7,11 +7,18 @@ module Relaton
7
7
  module Bibliography
8
8
  ENDPOINT = "https://raw.githubusercontent.com/relaton/relaton-data-oasis/refs/heads/v2/"
9
9
 
10
+ # The publisher token every OASIS printed id starts with, and the one
11
+ # thing `Pubid::Oasis`'s grammar requires. See #parse_ref.
12
+ PREFIX = "OASIS "
13
+
14
+ # The components a reference may omit. `number` (the specification name)
15
+ # is deliberately absent — see #ignored.
16
+ OPTIONAL = %i[version stage part label].freeze
17
+
10
18
  class << self
11
19
  def search(text, _year = nil, _opts = {}) # rubocop:disable Metrics/MethodLength
12
20
  Util.info "Fetching from Relaton repository ...", key: text
13
- /^(?:OASIS\s)?(?<code>.+)/ =~ text
14
- row = find_index_entry(code)
21
+ row = find_index_entry(text)
15
22
  unless row
16
23
  Util.info "Not found.", key: text
17
24
  return
@@ -33,11 +40,145 @@ module Relaton
33
40
 
34
41
  private
35
42
 
36
- def find_index_entry(code)
37
- index = Relaton::Index.find_or_create(
38
- :oasis, url: "#{ENDPOINT}#{INDEXFILE}.zip"
43
+ #
44
+ # The pubid `index-v2`. `pubid_class:` is what makes `Relaton::Index`
45
+ # deserialize the rows into identifiers, sort them by
46
+ # `id.root.number`, and let `Type#search` bsearch; `file:` names the
47
+ # local cache, and `Processor#remove_index_file` must name the same
48
+ # one, or `Db#clear` leaves this cache in place.
49
+ #
50
+ # @return [Relaton::Index::Type]
51
+ #
52
+ def index
53
+ Relaton::Index.find_or_create(
54
+ :oasis, url: "#{ENDPOINT}#{INDEXFILE}.zip",
55
+ file: "#{INDEXFILE}.yaml",
56
+ pubid_class: ::Pubid::Oasis::Identifier
39
57
  )
40
- index.search(code).min_by { |r| r[:id] }
58
+ end
59
+
60
+ #
61
+ # Find the row that best answers a reference.
62
+ #
63
+ # **Pass the pubid, not the string.** `Type#search_candidates` narrows
64
+ # only when the argument is not a `String`, so passing the reference
65
+ # text would disable the binary search however the index was built.
66
+ #
67
+ # Selection is `Type#search`'s default with no block — pubid's
68
+ # asymmetric subset match (`Pubid::SubsetMatch`): the reference on the
69
+ # left, and a component it omits (`version`, `stage`, `part`, `label`)
70
+ # matches any value, which is what `#ranking_key` then orders.
71
+ #
72
+ # A reference pubid cannot parse finds nothing, and there is no
73
+ # substring fallback. `#parse_ref` supplies the publisher token, so
74
+ # after normalization the only unparseable inputs are a blank string
75
+ # and one over pubid's 1000-character cap — and a substring scan for
76
+ # `""` matches every row in the index, which is worse than a miss.
77
+ # This is also the v1 -> v2 semantic change IANA recorded: v1 matched
78
+ # by substring, so `OASIS amqp` resolved to some `amqp-core` record;
79
+ # v2 matches identifiers, so a partial name no longer resolves.
80
+ #
81
+ # @param text [String] the reference as the caller wrote it
82
+ # @return [Hash, nil] the index row, or nil
83
+ #
84
+ def find_index_entry(text)
85
+ pubid = parse_ref text
86
+ return unless pubid
87
+
88
+ rows = index.search(pubid)
89
+ rows.max_by { |row| ranking_key pubid, row[:id] }
90
+ end
91
+
92
+ #
93
+ # An OASIS printed id carries the publisher token, and pubid rejects a
94
+ # bare slug — but a caller may well write one ("mqtt-v5.0"), and
95
+ # `Db#fetch` hands the reference through verbatim. Supplying the token
96
+ # is normalization, not identification, so it happens here rather than
97
+ # in the grammar.
98
+ #
99
+ # The token is stripped **case-insensitively** and re-added in its
100
+ # canonical form. pubid's grammar matches the literal `OASIS `, so a
101
+ # lowercase `oasis stix` would otherwise keep its own token, be given a
102
+ # second one, and search for the specification named `oasis stix`.
103
+ # Everything after the token stays verbatim: an OASIS slug is
104
+ # case-sensitive (`STIX`, `amqp-core`, `OpenDocument`).
105
+ #
106
+ # An unrecognized reference **raises**; like ISO, ETSI and 3GPP we let it
107
+ # propagate. relaton-cli rescues `Pubid::Errors::Error` and renders
108
+ # `"..." is not a recognized standards identifier`
109
+ # (`gems/relaton-cli/lib/relaton/cli/command.rb:324`), and `Db#fetch`
110
+ # logs it through the `StandardError` arm at `lib/relaton/db.rb:122`.
111
+ # Rescuing here would collapse "this identifier is malformed" into "no
112
+ # such document", leaving a caller unable to tell them apart.
113
+ #
114
+ # @param text [String]
115
+ # @return [Pubid::Oasis::Identifier]
116
+ # @raise [Pubid::Errors::ParseError]
117
+ #
118
+ def parse_ref(text)
119
+ slug = text.to_s.strip.sub(/\A#{Regexp.escape PREFIX}/i, "")
120
+ ::Pubid::Oasis::Identifier.parse "#{PREFIX}#{slug}"
121
+ end
122
+
123
+ #
124
+ # The components the reference left out, which the row may therefore
125
+ # carry freely. `#find_index_entry` no longer needs this list — pubid's
126
+ # `===` reads the omission from the reference itself — but
127
+ # `#ranking_key` still counts the components the caller did not ask for.
128
+ # `number` — the specification name — is never optional: it is the whole
129
+ # identity of an OASIS record and the key the index bsearches on.
130
+ #
131
+ # @param pubid [Pubid::Oasis::Identifier]
132
+ # @return [Array<Symbol>]
133
+ #
134
+ def ignored(pubid)
135
+ OPTIONAL.select { |attr| pubid.public_send(attr).nil? }
136
+ end
137
+
138
+ #
139
+ # Order the matched rows. Ignoring a component means "don't care", so a
140
+ # loose reference matches the more specific rows too and this key
141
+ # decides between them:
142
+ #
143
+ # 1. an **exact printed id** always wins. Five published records are a
144
+ # bare specification name that also has versioned siblings — `OASIS
145
+ # EDXL`, `OData`, `OSLC`, `SAML`, `WSS` — and each is a real
146
+ # document in its own right. Without this key the "newest version"
147
+ # rule below answers a request for the bare record with its newest
148
+ # sibling; three of the five did exactly that. Published ids are
149
+ # unique (0 duplicates in 605 rows), so at most one row scores here;
150
+ # 2. then the **newest version**, segments compared as integers —
151
+ # `v2.1` beats `v1.2.1`, and `v10` beats `v9`, which a string
152
+ # compare gets wrong;
153
+ # 3. then the **least specific** row, i.e. the fewest components the
154
+ # reference did not ask for — a bare `OASIS STIX` wants `STIX-v2.1`,
155
+ # not `STIX-v2.1-CS02`, and `OASIS STIX-v1.2.1-CS01` wants that
156
+ # record itself, not its `-Pt3-Core` part;
157
+ # 4. then the **later stage revision** (`CS02` over `CS01`). Only the
158
+ # digits are compared: OASIS stage letters have no ranking this code
159
+ # is entitled to invent;
160
+ # 5. then the printed id, so the result never depends on index order.
161
+ #
162
+ # Checked against every published row: all 605 resolve to their own
163
+ # record, with and without the publisher token.
164
+ #
165
+ # @param pubid [Pubid::Oasis::Identifier] the parsed reference
166
+ # @param id [Pubid::Oasis::Identifier] a matched row's id
167
+ # @return [Array]
168
+ #
169
+ def ranking_key(pubid, id)
170
+ exact = id.to_s == pubid.to_s ? 1 : 0
171
+ extra = ignored(pubid).count { |attr| id.public_send(attr) }
172
+ [exact, digits(id.version), -extra, digits(id.stage), id.to_s]
173
+ end
174
+
175
+ # Integer segments of a version or stage token, for numeric ordering
176
+ # ("v1.2.1" -> [1, 2, 1], "CS02" -> [2], nil -> []).
177
+ #
178
+ # @param token [String, nil]
179
+ # @return [Array<Integer>]
180
+ def digits(token)
181
+ token.to_s.scan(/\d+/).map(&:to_i)
41
182
  end
42
183
 
43
184
  def fetch_yaml(uri)
@@ -48,9 +48,13 @@ module Relaton
48
48
  end
49
49
  end
50
50
 
51
+ # `pubid_class:` on the producer too: FileIO#save only calls `to_hash`
52
+ # for instances of it, so without it the crawl writes v1-shaped rows
53
+ # under a v2 name, silently.
51
54
  def index
52
55
  @index ||= Relaton::Index.find_or_create(
53
- :oasis, file: "#{INDEXFILE}.yaml"
56
+ :oasis, file: "#{INDEXFILE}.yaml",
57
+ pubid_class: ::Pubid::Oasis::Identifier
54
58
  )
55
59
  end
56
60
 
@@ -68,16 +72,48 @@ module Relaton
68
72
 
69
73
  def save_doc(doc) # rubocop:disable Metrics/AbcSize
70
74
  id = doc.docidentifier.find(&:primary) || doc.docidentifier.first
71
- file = output_file(id.content)
75
+ # Distinct docids can sanitize to one filename; take a path of our own
76
+ # rather than overwriting the other document (Core#unique_output_file).
77
+ file = unique_output_file(id.content)
72
78
  if @files.include? file
79
+ # Same reserved path == same docid: a genuine duplicate. Checked FIRST,
80
+ # because a disambiguated path stays != output_file forever.
73
81
  Util.warn "File #{file} already exists. Document: #{id.content}"
74
- else
75
- @files << file
82
+ elsif file != output_file(id.content)
83
+ Util.warn "File #{output_file id.content} already exists. " \
84
+ "Document: #{id.content}. Writing #{file} instead."
76
85
  end
77
- index.add_or_update id.content, file
86
+ @files << file
87
+ add_to_index id, file
78
88
  File.write file, serialize(doc), encoding: "UTF-8"
79
89
  end
80
90
 
91
+ #
92
+ # Index the document, or record why it could not be indexed.
93
+ #
94
+ # An id pubid cannot parse is recorded in `@errors` — the inherited
95
+ # `report_errors` logs a String value as the message — and the row is
96
+ # skipped rather than indexed unparsed: `Relaton::Index` rejects the WHOLE
97
+ # index if a single row fails to deserialize, and its sort calls
98
+ # `.root.number` on every id. The data file is still written by the
99
+ # caller, so the document is unindexed, never lost. (The ECMA/W3C/3GPP
100
+ # shape.)
101
+ #
102
+ # `DataParserUtils#record_unparseable_id` already recorded this id when it
103
+ # was built, under the SAME key — the id string. This entry overwrites it
104
+ # with the same sentence plus the output file, so a saved document reports
105
+ # one line, the more informative one. Keep the two keys identical.
106
+ #
107
+ # @param id [Relaton::Oasis::Docidentifier] primary document identifier
108
+ # @param file [String] path the document is written to
109
+ #
110
+ def add_to_index(id, file)
111
+ return index.add_or_update(id.pubid, file) if id.pubid
112
+
113
+ @errors[id.content.to_s] =
114
+ "Unparseable primary id `#{id.content}` was not indexed (#{file})"
115
+ end
116
+
81
117
  def to_xml(bib)
82
118
  bib.to_xml(bibdata: true)
83
119
  end
@@ -240,16 +240,50 @@ module Relaton
240
240
  #
241
241
  # Parse document identifier.
242
242
  #
243
- # @return [Array<Bib::Docidentifier>] document identifier
243
+ # @return [Array<Relaton::Oasis::Docidentifier>] document identifier
244
244
  #
245
245
  def parse_docid
246
246
  id = "OASIS #{parse_docnumber}"
247
- result = [Bib::Docidentifier.new(type: "OASIS", content: id,
248
- primary: true)]
247
+ docid = Docidentifier.new(type: "OASIS", content: id, primary: true)
248
+ record_unparseable_id docid
249
+ result = [docid]
249
250
  @errors[:docid] &&= result.empty?
250
251
  result
251
252
  end
252
253
 
254
+ #
255
+ # Surface an id pubid could not parse through the shared error-reporting
256
+ # machinery, so the crawl's "Error fetching documents" GitHub issue names
257
+ # it instead of dropping it silently in the action log.
258
+ #
259
+ # `Core::DataFetcher#report_errors` logs a **String** value verbatim as
260
+ # the message (a boolean means "this field failed for every record" and
261
+ # takes its message from the key), and the parsers share the fetcher's own
262
+ # `@errors` hash, so an entry written here reaches the issue unchanged.
263
+ #
264
+ # **Why here and not only in the fetcher.** W3C, 3GPP and ISO record this
265
+ # at index time, where the output file is known. OASIS records it at build
266
+ # time as well, because this is the one place EVERY id is built — the part
267
+ # ids that only ever become a relation's `formattedref`
268
+ # (`DataParser#parse_relation`, `DataPartParser#parse_relation`) never
269
+ # reach `DataFetcher#save_doc`, so an index-time hook alone cannot see
270
+ # them. `DataFetcher#add_to_index` keys its own entry on the same id
271
+ # string, so a document that IS saved reports once, through the fetcher's
272
+ # message, which additionally names the file.
273
+ #
274
+ # The docidentifier is returned either way: the document is still written,
275
+ # it is only left out of the index — `Relaton::Index` rejects the WHOLE
276
+ # index if a single row fails to deserialize.
277
+ #
278
+ # @param docid [Relaton::Oasis::Docidentifier] the id just built
279
+ #
280
+ def record_unparseable_id(docid)
281
+ return if docid.pubid
282
+
283
+ content = docid.content.to_s
284
+ @errors[content] = "Unparseable primary id `#{content}` was not indexed"
285
+ end
286
+
253
287
  #
254
288
  # Parse document type.
255
289
  #
@@ -0,0 +1,54 @@
1
+ module Relaton
2
+ module Oasis
3
+ # Structured OASIS document identifier. The docid string is the printed
4
+ # form, which carries the "OASIS " publisher token
5
+ # (e.g. "OASIS OSLC-CoreShapes-3.0-PS01-Pt8"), and it is parsed into a
6
+ # `Pubid::Oasis::Identifier` kept in `@pubid` while the lutaml `content`
7
+ # attribute stays a plain string for serialization.
8
+ #
9
+ # Parsing is **soft**: `content=` lazily requires pubid and rescues
10
+ # `LoadError`/`StandardError`, so a missing pubid gem or non-OASIS content
11
+ # leaves `@pubid` nil rather than raising. A reference without the publisher
12
+ # token ("amqp-core") is not an OASIS printed id, so it leaves `@pubid` nil
13
+ # too; `DataFetcher` always builds the token-carrying form.
14
+ #
15
+ # ## Why the three mutators stay no-ops
16
+ #
17
+ # `Pubid::Oasis::Renderer` echoes the verbatim slug held in `original`, so
18
+ # clearing `part`, `version` or `stage` cannot change the printed id — the
19
+ # mutation would be invisible, and rewriting `original` would invent a
20
+ # reference form OASIS does not publish. OASIS slugs are free-form with an
21
+ # inconsistent internal structure, and no OASIS citation drops a part or a
22
+ # version; there is no "all parts" or "most recent" spelling to render.
23
+ #
24
+ # So this class deliberately does NOT override `remove_part!`,
25
+ # `remove_date!` or `to_all_parts!`. Since `dfbd26c72` the `Bib::Docidentifier`
26
+ # defaults are no-ops rather than `NotImplementedError` raises, so
27
+ # `Bib::ItemData#to_all_parts` and `#to_most_recent_reference` return the
28
+ # item unchanged instead of blowing up. That is the honest OASIS answer.
29
+ # (Contrast `Relaton::Ogc::Docidentifier`, whose components really do
30
+ # render, so its mutators re-render the content.)
31
+ class Docidentifier < Bib::Docidentifier
32
+ attr_reader :pubid
33
+
34
+ def initialize(attrs = {}, options = {})
35
+ pubid = attrs.is_a?(Hash) ? attrs.delete(:pubid) : nil
36
+ attrs[:content] ||= pubid.to_s if pubid
37
+ super
38
+ @pubid = pubid if pubid
39
+ end
40
+
41
+ def content=(value)
42
+ super
43
+ return unless value
44
+
45
+ @pubid = begin
46
+ require "pubid"
47
+ ::Pubid::Oasis::Identifier.parse(value.to_s)
48
+ rescue LoadError, StandardError
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,4 @@
1
+ require_relative "docidentifier"
1
2
  require_relative "ext"
2
3
 
3
4
  module Relaton
@@ -5,6 +6,8 @@ module Relaton
5
6
  class Item < Bib::Item
6
7
  model ItemData
7
8
 
9
+ attribute :docidentifier, Docidentifier, collection: true,
10
+ initialize_empty: true
8
11
  attribute :ext, Ext
9
12
  end
10
13
  end
@@ -11,6 +11,7 @@ module Relaton
11
11
  @defaultprefix = %r{^OASIS\s}
12
12
  @idtype = "OASIS"
13
13
  @datasets = %w[oasis-open]
14
+ @pubid_flavor = :Oasis
14
15
  end
15
16
 
16
17
  # @param code [String]
@@ -59,9 +60,14 @@ module Relaton
59
60
  #
60
61
  # Remove index file
61
62
  #
63
+ # `url: true` names the cached file. No `pubid_class:`: `Type#remove_file`
64
+ # deletes the file and never reads the index.
65
+ #
62
66
  def remove_index_file
63
67
  require_relative "../oasis"
64
- Relaton::Index.find_or_create(:oasis, file: "#{INDEXFILE}.yaml").remove_file
68
+ Relaton::Index.find_or_create(
69
+ :oasis, url: true, file: "#{INDEXFILE}.yaml"
70
+ ).remove_file
65
71
  end
66
72
  end
67
73
  end
data/lib/relaton/oasis.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Not lazy: DataFetcher names ::Pubid::Oasis::Identifier as the index
4
+ # `pubid_class:`, and Docidentifier parses every docid through it.
5
+ # (The IANA/IHO/IALA/OGC/ECMA form; spec/relaton/lazy_loading_spec.rb guards
6
+ # that this file is not itself loaded when a Db is built.)
7
+ require "pubid"
3
8
  require "relaton/index"
4
9
  require "relaton/bib"
5
10
  require_relative "version"
@@ -12,7 +17,15 @@ require_relative "oasis/bibliography"
12
17
 
13
18
  module Relaton
14
19
  module Oasis
15
- INDEXFILE = "index-v1"
20
+ # The pubid index, both written and read: rows are
21
+ # `Pubid::Oasis::Identifier` hashes (`_type: pubid:oasis:standard`), built
22
+ # and read with `pubid_class: ::Pubid::Oasis::Identifier`.
23
+ #
24
+ # This gem does not produce `index-v1` any more; `relaton-data-oasis`'s
25
+ # crawler derives it from the v2 rows for relaton v2 consumers (the
26
+ # IANA/BIPM/W3C/ECMA shape).
27
+ INDEXFILE = "index-v2".freeze
28
+
16
29
  class Error < StandardError; end
17
30
  # Your code goes here...
18
31
 
@@ -23,7 +23,12 @@ module Relaton
23
23
  end
24
24
 
25
25
  def index
26
- @index ||= Relaton::Index.find_or_create :ogc, file: "#{INDEXFILE}.yaml"
26
+ # `pubid_class:` on the producer too: FileIO#save only calls `to_hash`
27
+ # when the value is an instance of it, so without it the crawl writes
28
+ # v1-shaped rows under a v2 name, silently.
29
+ @index ||= Relaton::Index.find_or_create(
30
+ :ogc, file: "#{INDEXFILE}.yaml", pubid_class: ::Pubid::Ogc::Identifier
31
+ )
27
32
  end
28
33
 
29
34
  def fetch(_source = nil) # rubocop:disable Metrics/AbcSize
@@ -60,10 +65,26 @@ module Relaton
60
65
 
61
66
  @docids << docid
62
67
  file = file_name bib
63
- index.add_or_update docid, file
68
+ index.add_or_update index_id(docid), file
64
69
  File.write file, serialize(bib), encoding: "UTF-8"
65
70
  end
66
71
 
72
+ #
73
+ # The index key. Stored as a `Pubid::Ogc::Identifier` so the published
74
+ # index-v2 rows are pubid hashes the consumer can narrow on. A docid
75
+ # pubid rejects is logged and indexed as the plain string, so one bad
76
+ # record never aborts a crawl.
77
+ #
78
+ # @param docid [String]
79
+ # @return [Pubid::Ogc::Identifier, String]
80
+ #
81
+ def index_id(docid)
82
+ ::Pubid::Ogc::Identifier.parse docid
83
+ rescue StandardError => e
84
+ Util.warn "Failed to parse pubid `#{docid}`: #{e.message}"
85
+ docid
86
+ end
87
+
67
88
  def file_name(bib)
68
89
  name = bib.docidentifier[0].content.upcase.gsub(/[\s:.]/, "_")
69
90
  "#{@output}/#{name}.#{@ext}"
@@ -1,6 +1,111 @@
1
1
  module Relaton
2
2
  module Ogc
3
+ # Structured OGC document identifier. The docid string is the canonical
4
+ # printed form, which carries no publisher token (e.g. "19-025r1"), and it
5
+ # is parsed into a `Pubid::Ogc::Identifier` kept in `@pubid` while the
6
+ # lutaml `content` attribute stays a plain string for serialization.
7
+ #
8
+ # Parsing is **soft**: `content=` lazily requires pubid and rescues
9
+ # `LoadError`/`StandardError`, so a missing pubid gem or non-OGC content
10
+ # leaves `@pubid` nil rather than raising.
11
+ #
12
+ # Before this class had a body it inherited `Bib::Docidentifier`, whose
13
+ # `remove_part!` / `remove_date!` / `to_all_parts!` each raise
14
+ # `NotImplementedError` — and `Bib::ItemData` broadcasts all three to every
15
+ # docidentifier, so `#to_all_parts` and `#to_most_recent_reference` raised
16
+ # on **every** OGC item. `NotImplementedError` descends from `ScriptError`,
17
+ # so a caller's `rescue => e` did not even catch it.
3
18
  class Docidentifier < Bib::Docidentifier
19
+ attr_reader :pubid
20
+
21
+ # Capture the inherited (LocalizedMarkedUpString) content setter before
22
+ # overriding #content=, so #refresh_content! can write the re-rendered
23
+ # string back WITHOUT re-parsing (a re-parse would rebuild @pubid from the
24
+ # string and discard in-place mutations such as `all_parts = true`).
25
+ alias_method :store_content, :content=
26
+
27
+ def initialize(attrs = {}, options = {})
28
+ pubid = attrs.is_a?(Hash) ? attrs.delete(:pubid) : nil
29
+ attrs[:content] ||= pubid.to_s if pubid
30
+ super
31
+ @pubid = pubid if pubid
32
+ end
33
+
34
+ def content=(value)
35
+ super
36
+ return unless value
37
+
38
+ @pubid = begin
39
+ require "pubid"
40
+ ::Pubid::Ogc::Identifier.parse(value)
41
+ rescue LoadError, StandardError
42
+ nil
43
+ end
44
+ end
45
+
46
+ # `Pubid::Ogc::Identifier` models only `year`/`number`/`revision`, so the
47
+ # mapping is OGC-specific:
48
+ #
49
+ # - **`remove_date!` → clears `revision`.** OGC carries no date component;
50
+ # `revision` is its version discriminator — the same component a
51
+ # reference may omit, which `HitCollection#best_match` then treats as a
52
+ # wildcard — so clearing it yields
53
+ # the version-agnostic ("most recent") reference: `12-128r19` →
54
+ # `12-128`.
55
+ # - **`year` is never cleared.** It looks date-like but is half the
56
+ # document number (`12-128`), not a publication qualifier; dropping it
57
+ # would leave `-128`, which identifies nothing.
58
+ # - **`remove_part!` → clears the (unused) `part`/`subpart` attributes.**
59
+ # A no-op for the rendered string today: OGC has no part component.
60
+ # Implemented anyway so it never raises, and it starts working
61
+ # automatically if pubid-ogc ever models one. (The IALA precedent.)
62
+ # - **`to_all_parts!` → both, then wraps `@pubid` in pubid's `AllParts`.**
63
+ # `content` stays the plain stripped id — OGC's own renderer has no
64
+ # "(all parts)" marker, and `content` is cached from the pre-wrap
65
+ # pubid on purpose. `#pubid` itself, read directly, now answers
66
+ # `all_parts? == true` and renders WITH pubid's generic marker
67
+ # (`#to_s`), since it's the wrapper.
68
+ #
69
+ # All three no-op safely when `@pubid` is nil, so `Bib::ItemData`'s
70
+ # `#to_all_parts` / `#to_most_recent_reference` never raise on OGC items.
71
+ #
72
+ # `refresh_content!` renders a bare `to_s`: the OGC printed form has no
73
+ # publisher token and the stored `content` does not carry one either, so
74
+ # unlike 3GPP this must NOT pass `with_publisher: true`.
75
+
76
+ def remove_part!
77
+ clear_attr!(:part)
78
+ end
79
+
80
+ def remove_date!
81
+ clear_attr!(:revision)
82
+ end
83
+
84
+ def to_all_parts!
85
+ return if !@pubid || @pubid.all_parts?
86
+
87
+ # `#exclude` (no args) rebuilds a full independent copy — `remove_part!`
88
+ # / `remove_date!` mutate `@pubid` in place, so a bare reference here
89
+ # would lose the original revision to that mutation too.
90
+ original = @pubid.exclude
91
+ remove_part!
92
+ remove_date!
93
+ @pubid = original.to_all_parts
94
+ end
95
+
96
+ private
97
+
98
+ def clear_attr!(attr)
99
+ return unless @pubid && @pubid.respond_to?("#{attr}=")
100
+
101
+ @pubid.public_send("#{attr}=", nil)
102
+ @pubid.subpart = nil if attr == :part && @pubid.respond_to?(:subpart=)
103
+ refresh_content!
104
+ end
105
+
106
+ def refresh_content!
107
+ store_content(@pubid.to_s) if @pubid
108
+ end
4
109
  end
5
110
  end
6
111
  end