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
@@ -49,14 +49,14 @@ module Relaton::Bsi
49
49
  #
50
50
  # Filter the search results for a BSI standard.
51
51
  #
52
- # @param [MatchData] code_parts parts of document identifier
52
+ # @param [Pubid::Bsi::Identifier] query the parsed query reference
53
53
  #
54
54
  # @return [self] filtered search results
55
55
  #
56
- def filter_hits!(code_parts)
57
- hits = filter code_parts
58
- hits = filter code_parts, skip_rest: true if hits.empty?
59
- hits = filter code_parts, drop_amd: true if hits.empty?
56
+ def filter_hits!(query)
57
+ hits = filter query
58
+ hits = filter query, skip_rest: true if hits.empty?
59
+ hits = filter query, drop_amd: true if hits.empty?
60
60
  @array = hits
61
61
  self
62
62
  end
@@ -93,20 +93,19 @@ module Relaton::Bsi
93
93
  end
94
94
 
95
95
  #
96
- # Select hits that match the document identifier.
96
+ # Select hits that match the query reference.
97
97
  #
98
- # @param [MatchData] code_parts parts of document identifier
99
- # @param [Boolean] drop_amd drop amendments and corrigendums
100
- # @param [Boolean] skip_rest skip rest suffix of document identifier
98
+ # @param [Pubid::Bsi::Identifier] query the parsed query reference
99
+ # @param [Boolean] drop_amd match base documents only (ignore supplements)
100
+ # @param [Boolean] skip_rest ignore the free-text suffix (ExComm / Flex version)
101
101
  #
102
- def filter(code_parts, drop_amd: false, skip_rest: false) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
102
+ def filter(query, drop_amd: false, skip_rest: false)
103
103
  @array.select do |i|
104
- code = drop_amd ? i.hit[:code].sub(/\+[AC]\d+.*$/, "") : i.hit[:code]
105
- cp = Bibliography.code_parts code
106
- match = cp[:code] == code_parts[:code] && cp[:a] == code_parts[:a] &&
107
- (!code_parts[:y] || cp[:y] == code_parts[:y]) &&
108
- (skip_rest || cp[:rest] == code_parts[:rest])
109
- i.hit[:code] = code if drop_amd && match
104
+ hit = i.pubid or next false
105
+ match = Bibliography.same_reference?(query, hit, skip_rest: skip_rest, drop_amd: drop_amd)
106
+ # When matching base documents only, drop the amendment from the hit's
107
+ # rendered id too, so the returned reference is the base document.
108
+ i.hit[:code] = hit.base_document.to_s if drop_amd && match
110
109
  match
111
110
  end
112
111
  end
@@ -1,8 +1,106 @@
1
+ require_relative "../../iso/type/pubid"
2
+
1
3
  module Relaton
2
4
  module Bsi
5
+ # BSI document identifier. Keeps the parsed `Pubid::Bsi::Identifier` as the
6
+ # single stored value of `content` (via `Iso::Type::Pubid`, which preserves
7
+ # the instance on the way in and stringifies it on the way out), so there is
8
+ # one source of truth for the identifier — `#pubid` returns it and `#content`
9
+ # renders it to a string. Non-BSI identifiers (e.g. ISBN) and anything pubid
10
+ # can't parse are stored verbatim as plain strings.
3
11
  class Docidentifier < Bib::Docidentifier
12
+ attribute :content, Iso::Type::Pubid
13
+
14
+ def initialize(arg = nil, **kwargs)
15
+ arg.is_a?(Hash) ? super(arg) : super(**kwargs)
16
+ # Content may have been set before type during lutaml init. Re-run the
17
+ # setter so the type-dependent parse (e.g. the ISBN bypass) applies.
18
+ raw = arg.is_a?(Hash) ? (arg["content"] || arg[:content]) : kwargs[:content]
19
+ self.content = raw if raw
20
+ end
21
+
22
+ alias_method :original_content=, :content=
23
+ alias_method :original_content, :content
24
+
25
+ # Store the parsed pubid instance (or the raw string for ISBN / anything
26
+ # pubid can't parse) as the single source of truth.
27
+ def content=(value)
28
+ send(:original_content=, parse_pubid(value) || value)
29
+ end
30
+
31
+ # The rendered identifier — pubid instances are stringified.
32
+ # @return [String, nil]
33
+ def content
34
+ original_content&.to_s
35
+ end
36
+
37
+ # The parsed identifier, when the content is a BSI reference. BSI has no
38
+ # dedicated pubid `AllParts` subclass, so `#to_all_parts!` stores the
39
+ # generic `Pubid::AllPartsIdentifier` wrapper here — not a
40
+ # `Pubid::Bsi::Identifier` — hence the second branch.
41
+ # @return [Pubid::Bsi::Identifier, Pubid::AllParts, nil]
42
+ def pubid
43
+ c = original_content
44
+ c if c.is_a?(::Pubid::Bsi::Identifier) || c.is_a?(::Pubid::AllParts)
45
+ end
46
+
47
+ def to_s
48
+ content.to_s
49
+ end
50
+
51
+ # Strip the publication date to build a most-recent (undated) reference.
52
+ # pubid's `exclude` propagates into nested identifiers (so a consolidated
53
+ # base date is dropped while the amendment is kept); `:month` is excluded
54
+ # alongside `:date` because Flex stores the month separately.
4
55
  def remove_date!
5
- content&.sub!(/:\d{4}(?:-\d{2})?/, "")
56
+ return unless pubid
57
+
58
+ self.content = pubid.exclude(:date, :month)
59
+ end
60
+
61
+ # Strip the part (and subpart) to build a whole-standard reference.
62
+ # pubid's `exclude` returns a new instance and propagates into nested
63
+ # identifiers, so the part is dropped even on adopted (BS EN ISO …) and
64
+ # consolidated (…+A1:…) ids while any amendment is kept.
65
+ def remove_part!
66
+ return unless pubid
67
+
68
+ self.content = pubid.exclude(:part, :subpart)
69
+ end
70
+
71
+ # Reduce to the all-parts form by wrapping `pubid` AS-IS (not a
72
+ # part/date-stripped copy): `to_all_parts`'s own identity computation
73
+ # already strips part/date for rendering (`#to_s`/`#===`), and wrapping
74
+ # the original keeps `identifiers` holding the real identifier this
75
+ # reference came from. BSI has no dedicated pubid `AllParts` subclass
76
+ # (unlike ISO/IEC), so the wrapper is the generic
77
+ # `Pubid::AllPartsIdentifier` — its `#to_s` DOES print a "(all parts)"
78
+ # marker, unlike BSI's own renderer, which never had one. Because BSI
79
+ # stores its parsed pubid as the single source of `content`
80
+ # (`Iso::Type::Pubid`), `content` and `#pubid` can't diverge here the
81
+ # way they do for flavors with a separately cached content string:
82
+ # both now show the marker (see `#pubid` above, widened to accept it).
83
+ def to_all_parts!
84
+ return if !pubid || pubid.all_parts?
85
+
86
+ self.content = pubid.to_all_parts
87
+ end
88
+
89
+ private
90
+
91
+ # @return [Pubid::Bsi::Identifier, nil]
92
+ def parse_pubid(value)
93
+ case value
94
+ when ::Pubid::Bsi::Identifier then value
95
+ when String
96
+ return nil if type == "ISBN"
97
+
98
+ begin
99
+ ::Pubid::Bsi::Identifier.parse(value)
100
+ rescue StandardError
101
+ nil
102
+ end
103
+ end
6
104
  end
7
105
  end
8
106
  end
@@ -5,6 +5,7 @@ module Relaton::Bsi
5
5
  def initialize
6
6
  @short = :relaton_bsi
7
7
  @prefix = "BSI"
8
+ @pubid_flavor = :Bsi # global prefixes sourced from Pubid::Bsi.prefixes
8
9
  @defaultprefix = %r{^(BSI|BS|PD)\s}
9
10
  @idtype = "BSI"
10
11
  end
@@ -20,26 +20,21 @@ module Relaton::Calconnect
20
20
  # @option opts [TrueClass, FalseClass] :bibdata
21
21
  #
22
22
  # @return [RelatonCalconnect::CcBibliographicItem]
23
- def get(ref, year = nil, opts = {}) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
24
- code = ref
25
-
26
- if year.nil?
27
- /^(?<code1>[^\s]+(?:\s\w+)?\s[\d-]+):?(?<year1>\d{4})?/ =~ ref
28
- unless code1.nil?
29
- code = code1
30
- year = year1
31
- end
32
- end
33
-
23
+ # The reference is no longer split by regex before searching. pubid parses
24
+ # `CC/DIR 10005:2019` whole, so a dated reference narrows to that row in
25
+ # the index itself; the `year` ARGUMENT is what still needs filtering
26
+ # afterwards, because an undated reference reaches every year of the
27
+ # document. That split is why `bib_results_filter` stays.
28
+ def get(ref, year = nil, opts = {})
34
29
  Util.info "Fetching from Relaton repository ...", key: ref
35
- result = search(code, year, opts) || (return nil)
30
+ result = search(ref, year, opts) || (return nil)
36
31
  ret = bib_results_filter(result, year)
37
32
  if ret[:ret]
38
33
  Util.info "Found: `#{ret[:ret].docidentifier.first.content}`", key: ref
39
34
  ret[:ret]
40
35
  else
41
36
  Util.info "Not found.", key: ref
42
- fetch_ref_err(code, year, ret[:years])
37
+ fetch_ref_err(ref, year, ret[:years])
43
38
  end
44
39
  end
45
40
 
@@ -63,7 +58,10 @@ module Relaton::Calconnect
63
58
  item.fetched = Date.today.to_s
64
59
  return { ret: item } if !year
65
60
 
66
- /:(?<id_year>\d{4})$/ =~ r.hit[:id]
61
+ # The row id is a `Pubid::Calconnect::Identifier` now, so the year is
62
+ # read off the identifier rather than scraped from a rendered string.
63
+ # The old `/:(\d{4})$/` regex would raise TypeError against it.
64
+ id_year = r.hit[:id].date&.year
67
65
  return { ret: item } if year.to_i == id_year.to_i
68
66
 
69
67
  missed_years << id_year.to_i if id_year
@@ -4,6 +4,11 @@ require "json"
4
4
  require "mechanize"
5
5
  require "relaton/core"
6
6
  require "relaton/index"
7
+ # The flavor top-level, for INDEXFILE and ::Pubid::Calconnect::Identifier.
8
+ # `relaton-data-calconnect`'s crawler requires THIS file and nothing else, so
9
+ # without it `#index` NameErrors on the very first document. (The ECMA form;
10
+ # the same invariant the processor's `remove_index_file` follows.)
11
+ require_relative "../calconnect"
7
12
  require_relative "scraper"
8
13
  require_relative "util"
9
14
 
@@ -18,8 +23,18 @@ module Relaton::Calconnect
18
23
  @etagfile ||= File.join @output, "etag.txt"
19
24
  end
20
25
 
26
+ # The pubid `index-v2` this crawl builds.
27
+ #
28
+ # `pubid_class:` is required on the producer too: `FileIO#save` calls
29
+ # `to_hash` only for instances of it, so without it the crawl writes
30
+ # v1-shaped rows under a v2 name, silently, and the consumer then rejects
31
+ # the whole index. Memoized with `||=` — re-creating the Type on every call
32
+ # evicts the pooled entry a suite (or a sibling call site) set up.
21
33
  def index
22
- @index = Relaton::Index.find_or_create :CC, file: "index-v1.yaml"
34
+ @index ||= Relaton::Index.find_or_create(
35
+ :CC, file: "#{INDEXFILE}.yaml",
36
+ pubid_class: ::Pubid::Calconnect::Identifier
37
+ )
23
38
  end
24
39
 
25
40
  def log_error(msg)
@@ -66,21 +81,74 @@ module Relaton::Calconnect
66
81
  end
67
82
 
68
83
  def write_doc(slug, bib) # rubocop:disable Metrics/MethodLength
69
- file = output_file slug
84
+ # Distinct slugs can sanitize to one filename; take a path of our own
85
+ # rather than overwriting the other document (Core#unique_output_file).
86
+ file = unique_output_file slug
70
87
  if @files.include? file
88
+ # Same reserved path == same slug: a genuine duplicate. Checked FIRST,
89
+ # because a disambiguated path stays != output_file forever.
71
90
  Util.warn "#{file} exist"
72
- else
73
- @files << file
91
+ elsif file != output_file(slug)
92
+ Util.warn "#{output_file slug} exist; writing #{file} instead"
74
93
  end
75
- index.add_or_update primary_docid(bib), file
94
+ @files << file
95
+ # Write first, index second: an id pubid rejects is skipped from the
96
+ # index, and the document still has to reach disk.
76
97
  File.write file, serialize(bib), encoding: "UTF-8"
98
+ add_to_index bib, file
99
+ end
100
+
101
+ #
102
+ # Index the document, or record why it could not be indexed.
103
+ #
104
+ # An id pubid cannot rebuild is recorded in `@errors` — the inherited
105
+ # `report_errors` logs a String value as the message, and its GhIssue
106
+ # channel opens a GitHub issue at the end of the crawl — and the row is
107
+ # skipped rather than indexed unparsed: `Relaton::Index` rejects the WHOLE
108
+ # index if a single row fails to deserialize, and its sort calls
109
+ # `.root.number` on every id. The data file is already written by the
110
+ # caller, so the document is unindexed, never lost. (The ECMA/W3C shape.)
111
+ #
112
+ # @param bib [Relaton::Calconnect::ItemData]
113
+ # @param file [String] path the document was written to
114
+ #
115
+ def add_to_index(bib, file)
116
+ id = index_id bib
117
+ return index.add_or_update(id, file) if id
118
+
119
+ docid = primary_docid(bib)&.content || file
120
+ @errors[docid.to_s] = "Unparseable primary id `#{docid}` was not indexed (#{file})"
121
+ end
122
+
123
+ #
124
+ # The index key: the primary docidentifier's own
125
+ # `Pubid::Calconnect::Identifier`.
126
+ #
127
+ # Taken from the parsed model, never re-parsed from a rendered string, and
128
+ # never mutated — unlike ECMA, the CalConnect index key IS the document's
129
+ # printed id (`CC/DIR 10005:2019`), because pubid renders the publisher by
130
+ # default and the flavor models no edition or volume. There is no
131
+ # index-only component to add and none to strip.
132
+ #
133
+ # It is still **duplicated**, for a different reason than ECMA's: the index
134
+ # holds the object, and `Docidentifier#remove_date!` mutates the identifier
135
+ # in place. Sharing it would let anything that asks a crawled record for its
136
+ # most-recent reference rewrite an already-indexed key, between
137
+ # `add_or_update` and `index.save`, with nothing to show for it.
138
+ #
139
+ # @param bib [Relaton::Calconnect::ItemData]
140
+ # @return [Pubid::Calconnect::Identifier, nil] nil if pubid rejects the docid
141
+ #
142
+ def index_id(bib)
143
+ primary_docid(bib)&.pubid&.dup
77
144
  end
78
145
 
79
- # Index entries are keyed by the canonical doc identifier
80
- # (e.g. "CC/DIR 10005:2019"), not the upstream slug used for filenames.
146
+ # The docidentifier the index is keyed on — the canonical one
147
+ # (e.g. "CC/DIR 10005:2019"), never the upstream slug used for filenames.
148
+ # Every published record carries exactly one, marked primary; the fallback
149
+ # is for a record that marks none.
81
150
  def primary_docid(bib)
82
- docid = bib.docidentifier.find(&:primary) || bib.docidentifier.first
83
- docid.content
151
+ bib.docidentifier.find(&:primary) || bib.docidentifier.first
84
152
  end
85
153
 
86
154
  def to_yaml(bib) = bib.to_yaml
@@ -0,0 +1,80 @@
1
+ module Relaton
2
+ module Calconnect
3
+ # A CalConnect document identifier that carries its parsed pubid alongside
4
+ # the source string.
5
+ #
6
+ # Every published record carries exactly one docidentifier, `type:
7
+ # CalConnect`, `primary: true`, in the form `CC[/<series>] <number>[:<date>]`
8
+ # — `CC/DIR 10005:2019`, `CC 18011:2018`, `CC/WD 51017:2024-07-23`. That is
9
+ # exactly what `Pubid::Calconnect::Identifier.parse` accepts, so nothing has
10
+ # to synthesize or strip a publisher prefix.
11
+ # `DataFetcher#index_id` takes `#pubid` straight from here to build the
12
+ # `index-v2` rows.
13
+ #
14
+ # Follows the IHO/W3C shape rather than the ISO one: `content=` calls
15
+ # `super` first, so `content` keeps the source string verbatim and
16
+ # serialization is unchanged. That matters here beyond convention —
17
+ # `Relaton::Calconnect::ItemData#create_id` derives the record's `id` from
18
+ # `content.gsub(/\W+/, "")`, so a re-render would move every published id.
19
+ #
20
+ # Unlike W3C, CalConnect ids carry a date, so `remove_date!` is real and has
21
+ # to re-render. It writes back through `store_content`, never `content=`: a
22
+ # re-parse would rebuild `@pubid` from the string and discard the mutation.
23
+ #
24
+ # There is deliberately no `render` option to opt out of. `Pubid::Calconnect`
25
+ # renders the publisher by default and models no edition or volume, so its
26
+ # default output IS the stored docid form — the index key and the document's
27
+ # own printed id are the same string. This is why the flavor needs no
28
+ # `with_*` flag dance (contrast `Relaton::Ecma::Docidentifier`, which opts
29
+ # out of two, and `Relaton::ThreeGpp::Docidentifier`, which opts in to one).
30
+ class Docidentifier < Bib::Docidentifier
31
+ attr_reader :pubid
32
+
33
+ # Capture the inherited (LocalizedMarkedUpString) content setter before
34
+ # overriding #content=, so #refresh_content! can write the re-rendered
35
+ # string back WITHOUT re-parsing.
36
+ alias_method :store_content, :content=
37
+
38
+ def content=(value)
39
+ super
40
+ @pubid = value && parse(value)
41
+ end
42
+
43
+ # CalConnect's one optional component, and the only real mutator here.
44
+ # `CC/DIR 10005:2019` -> `CC/DIR 10005`.
45
+ def remove_date!
46
+ return unless @pubid
47
+
48
+ @pubid.date = nil
49
+ refresh_content!
50
+ end
51
+
52
+ # No-ops, and not for lack of an override. A CalConnect number is one
53
+ # token — `0812-1` and `0707.1` are numbers, not a number plus a part —
54
+ # so `Pubid::Calconnect::Identifier` models no part, and there is nothing
55
+ # for either of these to strip. Stated explicitly so the next reader does
56
+ # not "fix" them by splitting the number.
57
+ def remove_part!; end
58
+
59
+ def to_all_parts!; end
60
+
61
+ private
62
+
63
+ # An identifier that does not parse is a data defect, so it is reported at
64
+ # ERROR — never at WARN. It does not raise: an already-published record
65
+ # still has to deserialize and render. The crawl escalates the same
66
+ # failure into a tracked GitHub issue (see `DataFetcher#add_to_index` and
67
+ # `Core::DataFetcher#report_errors`).
68
+ def parse(value)
69
+ ::Pubid::Calconnect::Identifier.parse value.to_s
70
+ rescue StandardError => e
71
+ Util.error "Failed to parse pubid `#{value}`: #{e.message}"
72
+ nil
73
+ end
74
+
75
+ def refresh_content!
76
+ store_content(@pubid.to_s)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,84 +1,92 @@
1
- require "yaml"
2
- require "fileutils"
3
-
4
1
  module Relaton::Calconnect
5
2
  class HitCollection < Relaton::Core::HitCollection
6
- # ENDPOINT = "https://standards.calconnect.org/relaton/index.yaml".freeze
7
- # ENDPOINT = "http://127.0.0.1:4000/relaton/index.yaml".freeze
8
- # DATADIR = File.expand_path ".relaton/calconnect", Dir.home
9
- # DATAFILE = File.expand_path "bibliography.yml", DATADIR
10
- # ETAGFILE = File.expand_path "etag.txt", DATADIR
11
3
  GHURL = "https://raw.githubusercontent.com/relaton/relaton-data-calconnect/refs/heads/v2/".freeze
12
4
 
13
5
  # @param ref [Strig]
14
6
  # @param year [String]
15
- # @param opts [Hash]
16
7
  def initialize(ref, year = nil)
17
8
  super
18
- # @array = from_yaml(ref).sort_by do |hit|
19
- # hit.hit["revdate"] ? Date.parse(hit.hit["revdate"]) : Date.new
20
- # end.reverse
21
- index = Relaton::Index.find_or_create :CC, url: "#{GHURL}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml"
22
- @array = index.search(ref).map do |row|
23
- Hit.new(row, self)
24
- end
9
+ @array = search_index(ref).map { |row| Hit.new(row, self) }
25
10
  end
26
11
 
27
- # private
12
+ private
13
+
14
+ # @return [Relaton::Index::Type]
15
+ def index
16
+ Relaton::Index.find_or_create :CC, url: "#{GHURL}#{INDEXFILE}.zip",
17
+ file: "#{INDEXFILE}.yaml",
18
+ pubid_class: ::Pubid::Calconnect::Identifier
19
+ end
28
20
 
29
21
  #
30
- # Fetch data from yaml
22
+ # The index rows matching a reference, most recent first.
31
23
  #
32
- # @param docid [String]
24
+ # **The pubid is passed to `Index::Type#search`, not the string.**
25
+ # `search_candidates` narrows only when the argument is not a `String`, and
26
+ # a block alone never narrows — so the plain string this used to pass
27
+ # disabled the binary search however the index was built. `pubid_class:` on
28
+ # the index alone fixes nothing; both had to change together.
33
29
  #
34
- # @return [Array<RelatonBib::Hit>]
30
+ # This also ends the substring scan the old string search did, which was
31
+ # silently ambiguous: `CC/DIR 1000` answered with all five `CC/DIR 1000x`
32
+ # documents and `CC/A 1` with every `CC/A 1xxx`. A number now matches
33
+ # exactly, and a leading zero is significant (`CC/A 0001` is not `CC/A 1`).
35
34
  #
36
- # def from_yaml(docid, **_opts)
37
- # data["root"]["items"].select do |doc|
38
- # doc["docid"] && doc["docid"]["id"].include?(docid)
39
- # end.map { |h| Hit.new(h, self) }
40
- # end
41
-
35
+ # The rows are selected with pubid's subset match `pubid === row`, the
36
+ # default of `Index::Type#search`. A date that the reference omits matches
37
+ # any value. pubid declares `series` strict for CalConnect, so the series
38
+ # keeps `CC/CD 51016` and `CC/WD 51016` apart, and a series-less
39
+ # `CC 36010` does not match `CC/WD 36010`.
42
40
  #
43
- # Fetches YAML data
41
+ # @param ref [String]
42
+ # @return [Array<Hash>] matching index rows
44
43
  #
45
- # @return [Hash]
46
- # def data
47
- # FileUtils.mkdir_p DATADIR
48
- # ctime = File.ctime DATAFILE if File.exist? DATAFILE
49
- # fetch_data if !ctime || ctime.to_date < Date.today
50
- # @data ||= YAML.safe_load File.read(DATAFILE, encoding: "UTF-8")
51
- # end
44
+ def search_index(ref)
45
+ pubid = parse_ref ref
46
+ return [] unless pubid
47
+
48
+ index.search(pubid).sort_by { |row| [recency_key(row[:id]), row[:file]] }
49
+ end
52
50
 
53
51
  #
54
- # fetch data from server and save it to file.
52
+ # Parse a user reference into a `Pubid::Calconnect::Identifier`, or nil.
55
53
  #
56
- # def fetch_data
57
- # resp = Faraday.new(ENDPOINT, headers: { "If-None-Match" => etag }).get
58
- # # return if there aren't any changes since last fetching
59
- # return unless resp.status == 200
60
-
61
- # self.etag = resp[:etag]
62
- # @data = YAML.safe_load resp.body
63
- # File.write DATAFILE, @data.to_yaml, encoding: "UTF-8"
64
- # end
65
-
54
+ # A reference pubid rejects is a **miss, not an error**: this returns nil
55
+ # outside the transport rescue in `Bibliography.search`, so it never becomes
56
+ # a `Relaton::RequestError`.
66
57
  #
67
- # Read ETag from file
58
+ # @param ref [String]
59
+ # @return [Pubid::Calconnect::Identifier, nil]
68
60
  #
69
- # @return [String, NilClass]
70
- # def etag
71
- # @etag ||= if File.exist? ETAGFILE
72
- # File.read ETAGFILE, encoding: "UTF-8"
73
- # end
74
- # end
61
+ # An unrecognized reference **raises**; like ISO, ETSI and 3GPP we let it
62
+ # propagate. relaton-cli rescues `Pubid::Errors::Error` and renders
63
+ # `"..." is not a recognized standards identifier`
64
+ # (`gems/relaton-cli/lib/relaton/cli/command.rb:324`), and `Db#fetch`
65
+ # logs it through the `StandardError` arm at `lib/relaton/db.rb:122`.
66
+ # Rescuing here would collapse "this identifier is malformed" into "no
67
+ # such document", leaving a caller unable to tell them apart.
68
+ def parse_ref(ref)
69
+ ::Pubid::Calconnect::Identifier.parse ref.to_s.strip
70
+ end
75
71
 
76
72
  #
77
- # Save ETag to file
73
+ # Sort key placing the most recent document first.
74
+ #
75
+ # The index is sorted by number, so rows sharing a number arrive in no
76
+ # meaningful order — without this, `Bibliography.get "CC/S 0601"` would
77
+ # answer with an arbitrary one of the 2005 and 2006 documents. Segments are
78
+ # compared as integers and negated for descending order; an absent month or
79
+ # day sorts as 0, which is right because only one row in the corpus carries
80
+ # either.
78
81
  #
79
- # @param tag [String]
80
- # def etag=(e_tag)
81
- # File.write ETAGFILE, e_tag, encoding: "UTF-8"
82
- # end
82
+ # @param id [Pubid::Calconnect::Identifier]
83
+ # @return [Array<Integer>]
84
+ #
85
+ def recency_key(id)
86
+ date = id.date
87
+ return [0, 0, 0] unless date
88
+
89
+ [date.year, date.month, date.day].map { |part| -part.to_i }
90
+ end
83
91
  end
84
92
  end
@@ -1,5 +1,6 @@
1
1
  require "relaton/bib"
2
2
  require_relative "../item_data"
3
+ require_relative "../docidentifier"
3
4
  require_relative "ext"
4
5
 
5
6
  module Relaton
@@ -7,6 +8,12 @@ module Relaton
7
8
  class Item < Bib::Item
8
9
  model ItemData
9
10
 
11
+ # The flavor's own Docidentifier, so every parsed record carries a
12
+ # `#pubid` for `DataFetcher#index_id` to key the index-v2 on. Bibitem and
13
+ # Bibdata subclass Item, so they inherit it.
14
+ attribute :docidentifier, Docidentifier, collection: true,
15
+ initialize_empty: true
16
+
10
17
  attribute :ext, Ext
11
18
  end
12
19
  end
@@ -10,6 +10,7 @@ module Relaton::Calconnect
10
10
  @defaultprefix = %r{^CC(?!\w)}
11
11
  @idtype = "CC"
12
12
  @datasets = %w[calconnect-org]
13
+ @pubid_flavor = :Calconnect
13
14
  end
14
15
 
15
16
  # @param code [String]
@@ -58,9 +59,14 @@ module Relaton::Calconnect
58
59
  #
59
60
  # Remove index file
60
61
  #
62
+ # `url: true` names the cached file. No `pubid_class:`: `Type#remove_file`
63
+ # deletes the file and never reads the index.
64
+ #
61
65
  def remove_index_file
62
66
  require_relative "../calconnect"
63
- Relaton::Index.find_or_create(:CC, url: true, file: "#{INDEXFILE}.yaml").remove_file
67
+ Relaton::Index.find_or_create(
68
+ :CC, url: true, file: "#{INDEXFILE}.yaml"
69
+ ).remove_file
64
70
  end
65
71
  end
66
72
  end
@@ -1,3 +1,8 @@
1
+ # Not lazy: DataFetcher names ::Pubid::Calconnect::Identifier as the index
2
+ # `pubid_class:`, and Docidentifier parses every docid through it.
3
+ # (The ECMA/IANA/IHO/OGC form; spec/relaton/lazy_loading_spec.rb guards that
4
+ # this file is not itself loaded when a Db is built.)
5
+ require "pubid"
1
6
  require "relaton/index"
2
7
  require "relaton/core"
3
8
  require_relative "version"
@@ -12,7 +17,12 @@ require_relative "calconnect/scraper"
12
17
 
13
18
  module Relaton
14
19
  module Calconnect
15
- INDEXFILE = "index-v1".freeze
20
+ # The one index this flavor builds and reads: pubid-keyed rows
21
+ # (`_type: pubid:calconnect:standard`), via
22
+ # `pubid_class: ::Pubid::Calconnect::Identifier`.
23
+ # `relaton-data-calconnect`'s crawler derives the legacy `index-v1` from
24
+ # these rows for released consumers, so it is not produced or read here.
25
+ INDEXFILE = "index-v2".freeze
16
26
 
17
27
  # Returns hash of XML reammar
18
28
  # @return [String]