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
@@ -1,8 +1,74 @@
1
1
  module Relaton
2
2
  module Itu
3
+ # An ITU document identifier, backed by `Pubid::Itu`.
4
+ #
5
+ # `content` stays the source string for serialization. `#pubid` parses it
6
+ # into a `Pubid::Itu::Identifier`, and `#remove_date!` changes that pubid
7
+ # and renders it back into `content`. There is no string surgery.
8
+ #
9
+ # ## Why the parse is lazy
10
+ #
11
+ # Most ITU records also carry an `ISO` co-identifier ("ISO/IEC 14496-10"),
12
+ # which never parses with `Pubid::Itu`. An eager parse in `content=` would
13
+ # run a failed Parslet parse on every deserialization. So `#pubid` parses on
14
+ # first use and keeps the result until `content` changes.
15
+ #
16
+ # The parse is soft: content that does not parse gives a nil `#pubid`, with
17
+ # no log, because an ISO co-identifier is not a defect. With a nil `#pubid`
18
+ # the mutators do nothing.
3
19
  class Docidentifier < Bib::Docidentifier
20
+ # Capture the inherited content setter before the override, so that a
21
+ # mutation can write the rendered string back and keep the changed pubid.
22
+ # `content=` would clear it, and a new parse would lose nothing here, but
23
+ # it would parse again for no reason.
24
+ alias_method :store_content, :content=
25
+
26
+ def content=(value)
27
+ super
28
+ @pubid = nil
29
+ @pubid_parsed = false
30
+ end
31
+
32
+ # @return [Pubid::Itu::Identifier, nil] nil if the content does not parse
33
+ def pubid
34
+ return @pubid if @pubid_parsed
35
+
36
+ @pubid_parsed = true
37
+ @pubid = parse
38
+ end
39
+
40
+ # Removes the year and the month from the identifier and from every
41
+ # identifier in its `base` chain, because `Pubid::Identifier#exclude`
42
+ # recurses into `base`:
43
+ #
44
+ # ITU-T L.163 (11/2018) -> ITU-T L.163
45
+ # ITU-T H.264 (2005) Amd. 1 (06/2006) -> ITU-T H.264 Amd. 1
46
+ #
47
+ # The version stays: `ITU-T H.264 (V14) (08/2021)` -> `ITU-T H.264 (V14)`.
48
+ # It is not a date, and two versions are two documents.
49
+ #
50
+ # The result is the pubid canonical spelling, so a source `v10` becomes
51
+ # `(V10)`. `Pubid::Itu` has no `year=`/`month=` setters, so `#exclude` is
52
+ # the only way to clear them.
53
+ #
54
+ # `remove_part!` and `to_all_parts!` stay the inherited no-ops: the `-3`
55
+ # of `ITU-R P.838-3` is a revision, not a part.
4
56
  def remove_date!
5
- self.content = content.gsub(/\s*\((?:\d{2}\/)?\d{4}\)/, "")
57
+ return unless pubid
58
+
59
+ @pubid = @pubid.exclude(:year, :month)
60
+ store_content @pubid.to_s
61
+ end
62
+
63
+ private
64
+
65
+ def parse
66
+ return unless content
67
+
68
+ require "pubid"
69
+ ::Pubid::Itu.parse content.to_s
70
+ rescue LoadError, StandardError
71
+ nil
6
72
  end
7
73
  end
8
74
  end
@@ -14,6 +14,25 @@ module Relaton
14
14
  map_element "amendment", to: :amendment
15
15
  map_element "corrigendum", to: :corrigendum
16
16
  end
17
+
18
+ # `Bib::ItemData#ext_to_all_parts!` / `#ext_remove_date` broadcast to
19
+ # `ext.structuredidentifier` as well as to `docidentifier`. This class
20
+ # descends from `Lutaml::Model::Serializable` rather than
21
+ # `Bib::StructuredIdentifier`, so it does not inherit that class's no-op
22
+ # defaults and the broadcast raised `NoMethodError` on every ITU item.
23
+ #
24
+ # ITU structured identifiers model neither a part nor a date: `bureau`,
25
+ # `docnumber`, `annexid`, `amendment` and `corrigendum` are all identity,
26
+ # and stripping any of them would name a different document. The date is
27
+ # handled by `Relaton::Itu::Docidentifier#remove_date!`, which removes the
28
+ # year and month from the docid's pubid and renders it again. So all
29
+ # three are no-ops.
30
+
31
+ def remove_part!; end
32
+
33
+ def remove_date!; end
34
+
35
+ def to_all_parts!; end
17
36
  end
18
37
  end
19
38
  end
@@ -8,7 +8,7 @@ module Relaton
8
8
  @prefix = "ITU"
9
9
  @defaultprefix = %r{^ITU\s}
10
10
  @idtype = "ITU"
11
- @datasets = %w[itu-r]
11
+ @datasets = %w[itu-r itu-t]
12
12
  end
13
13
 
14
14
  # @param code [String]
@@ -21,9 +21,13 @@ module Relaton
21
21
  end
22
22
 
23
23
  #
24
- # Fetch all the documents from https://extranet.itu.int/brdocsearch/
24
+ # Harvest a dataset. Only "itu-t" harvests (the searchRecs corpus); "itu-r"
25
+ # raises — its bulk-enumeration endpoint is gone (issue #75) — and it stays
26
+ # in @datasets on purpose, so a stray `relaton fetch-data itu-r` surfaces
27
+ # that error instead of the registry's warn-and-exit-0 for an unknown
28
+ # dataset.
25
29
  #
26
- # @param [String] source source name (itu-r)
30
+ # @param [String] source source name (itu-t; itu-r raises)
27
31
  # @param [Hash] opts
28
32
  # @option opts [String] :output directory to output documents, default is data
29
33
  # @option opts [String] :format output format, default is yaml
@@ -59,7 +63,9 @@ module Relaton
59
63
  #
60
64
  def remove_index_file
61
65
  require_relative "../itu"
62
- Relaton::Index.find_or_create(:itu, url: true, file: "#{INDEXFILE}.yaml").remove_file
66
+ Relaton::Index.find_or_create(
67
+ :itu, url: true, file: "#{INDEXFILE}.yaml"
68
+ ).remove_file
63
69
  end
64
70
  end
65
71
  end
@@ -11,7 +11,15 @@ module Relaton
11
11
 
12
12
  rule(:prefix) { str("ITU").as(:prefix) }
13
13
  rule(:sector) { separator >> match("[A-Z]").as(:sector) }
14
- rule(:type) { separator >> str("REC").as(:type) }
14
+ # "Report" is not decoration: ITU-R Recommendations and Reports number
15
+ # independently, so "ITU-R BT.2020-1" alone names two different documents
16
+ # (Rec. BT.2020-1, 06/2014 vs Report BT.2020-1, 2000). ITU writes the
17
+ # discriminator first ("Report ITU-R BT.2020-1"), which is the form
18
+ # `Pubid::Itu` parses as `pubid:itu:report`; the sector-first spelling is
19
+ # accepted too because bibliographies use both.
20
+ rule(:report) { (str("Report") | str("REP")).as(:type) >> space }
21
+ rule(:report?) { report.maybe }
22
+ rule(:type) { separator >> (str("REC") | str("Report") | str("REP")).as(:type) }
15
23
  rule(:type?) { type.maybe }
16
24
  rule(:code) { separator >> (match["A-Z0-9"].repeat(1) >> match["[:alnum:]/.-"].repeat).as(:code) }
17
25
  rule(:year) { (match["12"] >> num.repeat(3, 3)).as(:year) }
@@ -41,7 +49,7 @@ module Relaton
41
49
 
42
50
  rule(:itu_pubid_sector) { prefix >> sector >> type? >> code >> sup? >> annex? >> ver? >> date? >> amd? >> any.repeat }
43
51
  rule(:itu_pubid_no_sector) { prefix >> type? >> code >> sup? >> annex? >> ver? >> date? >> amd? >> any.repeat }
44
- rule(:itu_pubid) { itu_pubid_sector | itu_pubid_no_sector }
52
+ rule(:itu_pubid) { report? >> (itu_pubid_sector | itu_pubid_no_sector) }
45
53
  root(:itu_pubid)
46
54
  end
47
55
 
@@ -74,7 +82,11 @@ module Relaton
74
82
  rescue Parslet::ParseFailed => e
75
83
  Util.error "`#{id}` is invalid ITU publication identifier\n" \
76
84
  "#{e.parse_failure_cause.ascii_tree}"
77
- raise e
85
+ # This grammar is local, so re-raise it as pubid's error: relaton-cli
86
+ # rescues `Pubid::Errors::Error`. `::` is needed, because inside this
87
+ # class a bare `Pubid` is `Relaton::Itu::Pubid`.
88
+ raise ::Pubid::Errors::ParseError.new(e.message, e.parse_failure_cause,
89
+ input: id, flavor: "itu")
78
90
  end
79
91
 
80
92
  def to_h(with_type: true) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -96,10 +108,20 @@ module Relaton
96
108
  to_s ref: true
97
109
  end
98
110
 
111
+ # @return [Boolean] whether this reference names an ITU-R Report rather
112
+ # than a Recommendation — the one type that must survive into #to_ref,
113
+ # because it is what tells the two apart (see the `report` parser rule).
114
+ def report?
115
+ %w[Report REP].include? type
116
+ end
117
+
99
118
  def to_s(ref: false) # rubocop:disable Metrics/AbcSize
100
- s = prefix.dup
119
+ # "Report" leads, the way ITU cites it and the way Pubid::Itu parses it;
120
+ # `REC` stays dropped from a reference because it is redundant there.
121
+ s = report? ? +"Report " : +""
122
+ s << prefix
101
123
  s << "-#{sector}" if sector
102
- s << " #{type}" if type && !ref
124
+ s << " #{type}" if type && !ref && !report?
103
125
  s << " #{code}"
104
126
  s << " Suppl. #{suppl}" if suppl
105
127
  s << " Annex #{annex}" if annex
@@ -0,0 +1,334 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "family_cache"
5
+
6
+ module Relaton
7
+ module Itu
8
+ # Shared extraction of ITU-T Recommendation metadata from the
9
+ # `mws/api/recommendations/*` detail endpoints. Reused by BOTH the live
10
+ # lookup path (`RecommendationParser`, via `Scraper`) and the offline
11
+ # harvester (`DataParserT`), so producer output and runtime output stay
12
+ # identical field-for-field.
13
+ #
14
+ # The includer must provide four hooks:
15
+ # #agent — a Mechanize agent (browser UA; www.itu.int is behind an F5 WAF)
16
+ # #idrec — the recommendation record id
17
+ # #imp — whether this is an Implementers' Guide (live-only; harvester false)
18
+ # #cache — a cross-row cache, defaulting to NullCache (see below)
19
+ #
20
+ # `#doc` (the getRecHdrDetail JSON) is memoised here from those hooks.
21
+ #
22
+ # **The cache and the runtime boundary.** `searchRecs` returns one row per
23
+ # *edition*, but two of the four endpoints below answer for the whole
24
+ # recommendation, so the offline harvester asks the same question once per
25
+ # edition — 21 times over for H.264. `#cache` lets it ask once instead. The
26
+ # live lookup path handles one recommendation per `Bibliography.get` and must
27
+ # not share state between calls, so the default is {NullCache}, whose
28
+ # `#fetch` is a bare yield: same behaviour, same requests, nothing retained.
29
+ # `Scraper` is deliberately left calling the three-argument constructor, so
30
+ # no path from `Bibliography.get` can reach a real cache.
31
+ module RecommendationFields
32
+ include Relaton::Core::ArrayWrapper
33
+
34
+ RECHDR = "https://www.itu.int/mws/api/recommendations/getRecHdrDetail?idrec=%{idrec}&lang=en"
35
+ RECEDITIONS = "https://www.itu.int/mws/api/recommendations/getRecEditions?idrec=%{idrec}&lang=en"
36
+ RECSUPPLEMENTS = "https://www.itu.int/mws/api/recommendations/getRecSupplements?idrec=%{idrec}&lang=en"
37
+ IMPLGUIDES = "https://www.itu.int/mws/api/recommendations/getImplGuides?idrec=%{idrec}&lang=en"
38
+
39
+ # The recommendation header detail (getRecHdrDetail / getImplGuides).
40
+ # @return [Hash]
41
+ def doc
42
+ @doc ||= begin
43
+ url = (imp ? IMPLGUIDES : RECHDR) % { idrec: idrec }
44
+ resp = get_data url
45
+ imp ? resp.first : resp
46
+ end
47
+ end
48
+
49
+ # Cross-row cache for the family-invariant endpoints. Overridden by
50
+ # RecommendationParser when the harvester passes one in.
51
+ # @return [#fetch, #warm]
52
+ def cache
53
+ NullCache.instance
54
+ end
55
+
56
+ # Canonical id for the recommendation this edition belongs to: the
57
+ # smallest idrec ITU's own getRecEditions response lists.
58
+ #
59
+ # Derived from API output, never from parsing `rec_name`. That is the
60
+ # whole point — two recommendations whose names happen to normalize alike
61
+ # would silently share each other's metadata, and nothing downstream would
62
+ # notice (the data repo's `guard_enrichment` counts `contributor:` lines,
63
+ # which such a record still has).
64
+ #
65
+ # Free on the live path: `Scraper#parse_page` already calls
66
+ # `#fetch_relations`, which already calls `#editions`.
67
+ #
68
+ # @return [Integer, String]
69
+ def family_id
70
+ @family_id ||= (editions.filter_map { |e| e["idrec"] }.min || idrec)
71
+ end
72
+
73
+ # @return [String, nil]
74
+ def fetch_edition
75
+ self_edition&.dig("Version")
76
+ end
77
+
78
+ # @return [Array<Relaton::Bib::Title>]
79
+ def fetch_titles
80
+ title = imp ? doc["imp_title_e"] : doc["rec_title"]
81
+ return [] if title.nil? || title.empty?
82
+
83
+ Relaton::Bib::Title.from_string title, "en", "Latn"
84
+ end
85
+
86
+ # @return [Relaton::Bib::Status, nil]
87
+ def fetch_status
88
+ inforce = imp ? imp_status : doc["status"]
89
+ return if inforce.nil? || inforce.empty?
90
+
91
+ status = inforce == "In force" ? "Published" : "Withdrawal"
92
+ Relaton::Bib::Status.new(stage: Relaton::Bib::Status::Stage.new(content: status))
93
+ end
94
+
95
+ # @return [Array<Relaton::Bib::Date>]
96
+ def fetch_dates
97
+ array(doc_date).map { |on| Relaton::Bib::Date.new(type: "published", at: on) }
98
+ end
99
+
100
+ # @return [Array<Relaton::Bib::Abstract>]
101
+ def fetch_abstract
102
+ array(doc["summary"]).map do |content|
103
+ Relaton::Bib::Abstract.new(content: content, language: "en", script: "Latn")
104
+ end
105
+ end
106
+
107
+ # @return [Array<Relaton::Bib::Uri>]
108
+ def fetch_source
109
+ link = imp ? doc["imp_dms_link"] : doc["handle_id"]
110
+ links = [Relaton::Bib::Uri.new(type: "src", content: link)]
111
+ links << Relaton::Bib::Uri.new(type: "pdf", content: doc["handle_id_pdf_link"]) if doc["handle_id_pdf_link"]
112
+ imp_word_link { |wlink| links << Relaton::Bib::Uri.new(type: "word", content: wlink) }
113
+ links
114
+ end
115
+
116
+ def doc_date
117
+ return @doc_date if defined? @doc_date
118
+
119
+ date = imp ? doc["imp_approval_date"] : doc["approval_date"]
120
+ @doc_date = Date.parse(date).to_s rescue date # rubocop:disable Style/RescueModifier
121
+ end
122
+
123
+ # @return [Array<Relaton::Bib::Relation>]
124
+ def fetch_relations
125
+ relations = []
126
+ editions.each do |ed|
127
+ next if ed["idrec"] == idrec
128
+
129
+ relations << create_relation("hasEdition", ed["title"], ed["rec_name"])
130
+ end
131
+
132
+ supplements.each { |supp| relations << create_relation("complementOf", supp["title_text"], supp["rec_name"]) }
133
+ relations
134
+ end
135
+
136
+ # The equivalent ISO/IEC(/IEEE) identifier, from the recommendation
137
+ # header's `iso_number` (e.g. "ISO/IEC 17788:2014 (Common)", or a deliverable
138
+ # form like "ISO/IEC TR 29110"). Used as an additional docidentifier. Only
139
+ # recommendations carry it. The optional TR/TS/PAS/IWA/Guide token keeps
140
+ # deliverable forms that a bare `\d` anchor would drop.
141
+ # @return [Relaton::Itu::Docidentifier, nil]
142
+ def iso_docid
143
+ num = doc["iso_number"]
144
+ id = num && num[%r{ISO(?:/IEC)?(?:/IEEE)?(?:\s+(?:TR|TS|PAS|IWA|Guide))?\s+\d[\d-]*}]
145
+ Docidentifier.new(type: "ISO", content: id, primary: true) if id
146
+ end
147
+
148
+ # The editorial-group (study group / TSAG) contributor, from the
149
+ # recommendation HTML page's workgroup name.
150
+ # @param bureau [String, nil] the sector letter, e.g. "T" (from ITU-T)
151
+ # @return [Relaton::Bib::Contributor, nil]
152
+ def editorial_group(bureau)
153
+ return unless bureau
154
+
155
+ org = Relaton::Bib::Organization.new(
156
+ name: [Relaton::Bib::TypedLocalizedString.new(content: "International Telecommunication Union")],
157
+ abbreviation: Relaton::Bib::LocalizedString.new(content: "ITU-#{bureau.upcase}"),
158
+ subdivision: group_subdivision(fetch_workgroup),
159
+ )
160
+ role = Relaton::Bib::Contributor::Role.new(
161
+ type: "author",
162
+ description: [Relaton::Bib::LocalizedMarkedUpString.new(content: "committee")],
163
+ )
164
+ Relaton::Bib::Contributor.new(organization: org, role: [role])
165
+ end
166
+
167
+ # The publisher contributor (ITU) for the given abbreviation.
168
+ # @param abbrev [String, nil] e.g. "ITU"
169
+ # @return [Relaton::Bib::Contributor, nil]
170
+ def publisher(abbrev)
171
+ return unless abbrev == "ITU"
172
+
173
+ org = Relaton::Bib::Organization.new(
174
+ name: [Relaton::Bib::TypedLocalizedString.new(content: "International Telecommunication Union")],
175
+ abbreviation: Relaton::Bib::LocalizedString.new(content: abbrev),
176
+ uri: [Relaton::Bib::Uri.new(content: "www.itu.int")],
177
+ )
178
+ role = Relaton::Bib::Contributor::Role.new(type: "publisher")
179
+ Relaton::Bib::Contributor.new(organization: org, role: [role])
180
+ end
181
+
182
+ # @param wg_name [String, nil]
183
+ # @return [Array<Relaton::Bib::Subdivision>]
184
+ def group_subdivision(wg_name)
185
+ return [] unless wg_name
186
+
187
+ subtype = case wg_name
188
+ when /Advisory Group/ then "tsag"
189
+ when /Study Group/ then "study-group"
190
+ else "work-group"
191
+ end
192
+ [Relaton::Bib::Subdivision.new(
193
+ type: "technical-committee",
194
+ subtype: subtype,
195
+ name: [Relaton::Bib::TypedLocalizedString.new(content: wg_name)],
196
+ )]
197
+ end
198
+
199
+ # Fetch the study group name from the recommendation HTML page.
200
+ #
201
+ # The single most expensive request per record: `rec.aspx` is ~90-240 KB
202
+ # of HTML that then has to be parsed into a DOM. It is also the one field
203
+ # here that is a property of the *recommendation* rather than the edition
204
+ # — ITU renders the current owning study group even on an old edition's
205
+ # page, which is why `?rec=H.264` and `?rec=14659` both yield "ITU-T Study
206
+ # Group 21" in `spec/itu/vcr_cassettes/itu_t_h_264.yml`. So it is cached
207
+ # per family.
208
+ #
209
+ # The rescue sits outside the cache on purpose: a transient failure must
210
+ # not be stored as nil for the whole family. FamilyCache does not mark an
211
+ # entry computed when the block raises, so the next sibling retries.
212
+ # @return [String, nil]
213
+ def fetch_workgroup
214
+ # Short-circuited rather than keyed-then-yielded, because #family_id
215
+ # reads #editions: with the null cache that would add a getRecEditions
216
+ # request to a runtime lookup that did not need one. The live path must
217
+ # keep exactly the request count it had.
218
+ return workgroup_from_page unless cache.caching?
219
+
220
+ cache.fetch([:workgroup, family_id]) { workgroup_from_page }
221
+ rescue StandardError
222
+ nil
223
+ end
224
+
225
+ private
226
+
227
+ def workgroup_from_page
228
+ url = "https://www.itu.int/ITU-T/recommendations/rec.aspx?rec=#{idrec}&lang=en"
229
+ page = agent.get(url)
230
+ wg = page.at('//span[contains(@id, "uc_rec_main_info1_rpt_main_ctl00_Label8")]/a')
231
+ wg&.text
232
+ end
233
+
234
+ def fetch_editions
235
+ get_data(RECEDITIONS % { idrec: idrec }) || []
236
+ end
237
+
238
+ # The other editions this response answers for.
239
+ #
240
+ # Empty unless the response actually lists the idrec we asked for: if it
241
+ # does not, the endpoint's contract has changed under us and sharing the
242
+ # payload would hand a whole family another recommendation's data. Losing
243
+ # the dedup is the cheap failure; sharing wrongly is the expensive one.
244
+ def siblings_of(rows)
245
+ unless rows.any? { |r| r["idrec"] == idrec }
246
+ Util.warn "ITU-T: getRecEditions(#{idrec}) does not list #{idrec}; not sharing it across the family"
247
+ return []
248
+ end
249
+
250
+ rows.filter_map { |r| [:editions, r["idrec"]] unless r["idrec"] == idrec }
251
+ end
252
+
253
+ def get_data(url)
254
+ JSON.parse request_document(url).body
255
+ end
256
+
257
+ def request_document(url)
258
+ agent.get url
259
+ rescue Mechanize::ResponseCodeError, SocketError, Timeout::Error, Errno::ECONNRESET,
260
+ EOFError, Net::ProtocolError, OpenSSL::SSL::SSLError => e
261
+ raise Relaton::RequestError, "Could not access #{url}: #{e.message}"
262
+ end
263
+
264
+ # Every edition of this recommendation, newest first.
265
+ #
266
+ # One request answers the whole family, and the response says so itself:
267
+ # it lists every sibling `idrec`, and the payload for each of them is the
268
+ # same one — verified byte-for-byte for 16818 and 14659 in
269
+ # `spec/itu/vcr_cassettes/itu_t_h_264.yml`. So the result is published
270
+ # under every sibling key, and the family's other 20 editions cost nothing.
271
+ def editions
272
+ @editions ||= begin
273
+ rows = cache.fetch([:editions, idrec]) { fetch_editions }
274
+ # Warmed after #fetch released the entry lock, so there is no
275
+ # lock-order inversion between the table lock and an entry lock.
276
+ cache.warm(siblings_of(rows), rows)
277
+ rows
278
+ end
279
+ end
280
+
281
+ def self_edition
282
+ @self_edition ||= editions.find { |ed| ed["idrec"] == idrec }
283
+ end
284
+
285
+ def imp_status
286
+ self_edition&.dig("status")
287
+ end
288
+
289
+ def imp_word_link
290
+ return unless doc["imp_dms_link"]
291
+
292
+ @doc_page ||= request_document(doc["imp_dms_link"])
293
+ wrd_elm = @doc_page.at("//font[contains(.,'Word')]/../..")
294
+ yield wrd_elm[:href] if block_given? && wrd_elm
295
+ end
296
+
297
+ def create_relation(type, title_text, id)
298
+ titles = []
299
+ if title_text && !title_text.empty?
300
+ titles << Relaton::Bib::Title.new(content: title_text, language: "en", script: "Latn")
301
+ end
302
+
303
+ fref = titles.empty? ? id : nil
304
+ did = Relaton::Bib::Docidentifier.new(type: "ITU", content: id, primary: true)
305
+ bibitem = Relaton::Bib::ItemData.new(title: titles, formattedref: (fref ? Relaton::Bib::Formattedref.new(content: fref) : nil), docidentifier: [did])
306
+ Relaton::Bib::Relation.new(type: type, bibitem: bibitem)
307
+ end
308
+
309
+ # Supplements to this recommendation.
310
+ #
311
+ # **Deliberately not shared across the family**, unlike #editions and
312
+ # #fetch_workgroup. It very probably is family-invariant — supplements
313
+ # belong to a recommendation, not to an edition — but that is an
314
+ # assumption, and the only recording in the suite is a single
315
+ # `getRecSupplements?idrec=14659`. Sharing it wrongly would give a
316
+ # record another recommendation's `relation:` list, silently: the data
317
+ # repo's `guard_enrichment` counts `contributor:` lines, which a wrongly
318
+ # enriched record still has. What would settle it: add a
319
+ # `getRecSupplements?idrec=16818` interaction to
320
+ # `spec/itu/vcr_cassettes/itu_t_h_264.yml` and assert the two payloads
321
+ # are equal; then key this on #family_id like the other two.
322
+ def supplements
323
+ @supplements ||= begin
324
+ if imp
325
+ []
326
+ else
327
+ url = RECSUPPLEMENTS % { idrec: idrec }
328
+ get_data(url) || []
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end