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
@@ -73,8 +73,14 @@ module Relaton
73
73
  ATTRS = %i[type fetched title docidentifier date source abstract contributor place
74
74
  ext relation extent series medium].freeze
75
75
 
76
+ # Matches content that looks like markup. Same shape as the tag detector
77
+ # of `Relaton::Bib::Sanitizer`, so both agree on what to parse.
78
+ TAG_RE = %r{<[a-zA-Z/!?]}
79
+
76
80
  CROSSREF_API_URL = "https://api.crossref.org/works?query=%{query}&filter=%{filter}".freeze
77
81
  MAX_RETRIES = 3
82
+ RATE_LIMITED = "429".freeze
83
+ MAX_RETRY_DELAY = 60
78
84
 
79
85
  #
80
86
  # Initialize instance.
@@ -209,10 +215,48 @@ module Relaton
209
215
  # @return [Bib::Title] The title.
210
216
  #
211
217
  def create_title(title, type = "main")
212
- cnt = str_cleanup CGI.unescapeHTML(title)
218
+ cnt = str_cleanup normalize_markup(title)
213
219
  Bib::Title.new type: type, content: cnt, script: "Latn"
214
220
  end
215
221
 
222
+ #
223
+ # Prepare raw Crossref markup for the relaton-bib sanitizer.
224
+ #
225
+ # Crossref returns JATS markup, and it sometimes encodes that markup as
226
+ # HTML entities, so decode them before the content reaches a model
227
+ # attribute. The markup also carries the `jats:` namespace prefix, and
228
+ # sometimes an `xlink:` prefix on an attribute, which Relaton never
229
+ # declares. Removing those prefixes is `Relaton::Bib::Sanitizer`'s job:
230
+ # every `Bib::Title` and `Bib::Abstract` runs the sanitizer on
231
+ # assignment, and it declares each undeclared prefix on a wrapper
232
+ # element so the markup parses and maps to the basicdoc set. An
233
+ # undeclared prefix that survives into the output makes
234
+ # relaton-render return nil, and isodoc then raises. See
235
+ # metanorma-pdfa#99.
236
+ #
237
+ # Upstream relaton-doi carries its own prefix removal, because the
238
+ # sanitizer fix is not in a released relaton-bib. Here both ship in one
239
+ # gem, so this method is the entity decode only.
240
+ #
241
+ # Decode only content that carries no markup of its own. Crossref
242
+ # escapes an ampersand and a literal angle bracket *inside* real
243
+ # markup, so an unconditional decode turns `<jats:p>Smith &amp;
244
+ # Jones</jats:p>` into markup that no longer parses. The sanitizer
245
+ # would then give up on it and an unescapable string would reach the
246
+ # output -- the metanorma-pdfa#99 failure again, on the far more
247
+ # common input.
248
+ #
249
+ # @param [String, nil] str The raw Crossref content.
250
+ #
251
+ # @return [String, nil] The content, with the HTML entities decoded
252
+ # when it holds no markup.
253
+ #
254
+ def normalize_markup(str)
255
+ return str if str.nil? || str.match?(TAG_RE)
256
+
257
+ CGI.unescapeHTML(str)
258
+ end
259
+
216
260
  #
217
261
  # Parse a docidentifier from the source hash.
218
262
  #
@@ -303,9 +347,10 @@ module Relaton
303
347
  def parse_abstract
304
348
  return [] unless @src["abstract"]
305
349
 
306
- content = @src["abstract"]
307
350
  abstract = Bib::Abstract.new(
308
- content: content, language: "en", script: "Latn",
351
+ content: normalize_markup(@src["abstract"]),
352
+ language: "en",
353
+ script: "Latn",
309
354
  )
310
355
  [abstract]
311
356
  end
@@ -448,8 +493,10 @@ module Relaton
448
493
  # @return [Bib::Organization] The organization.
449
494
  #
450
495
  def create_org(name, abbreviation = nil)
451
- n = [Bib::TypedLocalizedString.new(content: name)]
452
- a = abbreviation ? Bib::LocalizedString.new(content: abbreviation) : nil
496
+ n = [Bib::TypedLocalizedString.new(content: normalize_markup(name))]
497
+ a = if abbreviation
498
+ Bib::LocalizedString.new(content: normalize_markup(abbreviation))
499
+ end
453
500
  Bib::Organization.new name: n, abbreviation: a
454
501
  end
455
502
 
@@ -462,11 +509,7 @@ module Relaton
462
509
  return [] unless @src["standards-body"]
463
510
 
464
511
  name, acronym = @src["standards-body"].values_at("name", "acronym")
465
- org = create_org(
466
- CGI.unescapeHTML(name),
467
- acronym && CGI.unescapeHTML(acronym),
468
- )
469
- [contributor(org, "authorizer")]
512
+ [contributor(create_org(name, acronym), "authorizer")]
470
513
  end
471
514
 
472
515
  #
@@ -728,7 +771,8 @@ module Relaton
728
771
 
729
772
  @src["container-title"].map do |ct|
730
773
  contrib = create_authors_editors false, "editor"
731
- bib = Bib::ItemBase.new(title: [Bib::Title.new(content: ct)], contributor: contrib)
774
+ title = Bib::Title.new content: normalize_markup(ct)
775
+ bib = Bib::ItemBase.new(title: [title], contributor: contrib)
732
776
  Bib::Relation.new(type: "includedIn", bibitem: bib)
733
777
  end
734
778
  end
@@ -777,7 +821,7 @@ module Relaton
777
821
  else []
778
822
  end
779
823
  con_ttl.map do |ct|
780
- title = Bib::Title.new content: ct
824
+ title = Bib::Title.new content: normalize_markup(ct)
781
825
  Bib::Series.new title: [title], abbreviation: abbrev
782
826
  end
783
827
  end
@@ -800,17 +844,36 @@ module Relaton
800
844
  # @param [String] query The query string.
801
845
  # @param [String] filter The filter string.
802
846
  #
803
- # @return [Array<Hash>, nil] Items array from response or nil for 4xx responses.
847
+ # @return [Array<Hash>, nil] Items array from response or nil for 4xx responses
848
+ # other than 429.
804
849
  #
805
- # @raise [Relaton::RequestError] If request fails after retries.
850
+ # @raise [Relaton::RequestError] If request fails after retries, or if the
851
+ # rate limit does not clear.
806
852
  #
807
- def fetch_crossref(query:, filter:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
853
+ def fetch_crossref(query:, filter:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
808
854
  url = format(CROSSREF_API_URL, query: query, filter: filter)
809
855
  retries = 0
856
+ throttled = 0
810
857
  begin
811
858
  resp = Crossref.agent.get url
812
859
  JSON.parse(resp.body).dig("message", "items")
813
860
  rescue Mechanize::ResponseCodeError => e
861
+ # 429 must be handled before the 4xx nil return: a throttle is not
862
+ # "no such record". Returning nil here made a rate-limited lookup look
863
+ # like a missing document, so callers such as #parent_item silently
864
+ # produced an incomplete item instead of failing.
865
+ if e.response_code == RATE_LIMITED
866
+ throttled += 1
867
+ if throttled <= MAX_RETRIES
868
+ # Cap the backoff itself, not just the header value: escalating by
869
+ # attempt would otherwise let a single `Retry-After: 60` stall one
870
+ # lookup for 60+120+180s.
871
+ sleep [retry_delay(e) * throttled, MAX_RETRY_DELAY].min
872
+ retry
873
+ end
874
+ raise Relaton::RequestError,
875
+ "Crossref rate limit not cleared after #{MAX_RETRIES} retries: #{url}"
876
+ end
814
877
  return nil if e.response_code.start_with?("4")
815
878
 
816
879
  raise Relaton::RequestError, "Crossref request failed: #{e.response_code} #{e.page.body}"
@@ -822,6 +885,37 @@ module Relaton
822
885
  raise Relaton::RequestError, "Crossref JSON parsing error: #{e.message}"
823
886
  end
824
887
  end
888
+
889
+ #
890
+ # Base seconds to wait before retrying a throttled Crossref request.
891
+ # Prefers Retry-After, falls back to Crossref's X-Rate-Limit-Interval
892
+ # (formatted "1s", hence the digit scan). An empty-bodied 429 carries
893
+ # neither header, so this never assumes one is present.
894
+ #
895
+ # @param [Mechanize::ResponseCodeError] err The rate-limit error.
896
+ #
897
+ # @return [Integer] Delay in seconds, at least 1 and at most MAX_RETRY_DELAY.
898
+ #
899
+ def retry_delay(err)
900
+ headers = err.page&.response || {}
901
+ secs = retry_after_seconds(headers["retry-after"]) ||
902
+ headers["x-rate-limit-interval"].to_s[/\d+/].to_i
903
+ secs.positive? ? [secs, MAX_RETRY_DELAY].min : 1
904
+ end
905
+
906
+ #
907
+ # Parse a Retry-After header value, which RFC 9110 allows to be either
908
+ # delta-seconds or an HTTP-date. Only the numeric form is usable as a
909
+ # delay: scanning digits out of a date would pick up the day-of-month,
910
+ # silently turning a long server-mandated backoff into a few seconds.
911
+ #
912
+ # @param [String, nil] value The raw header value.
913
+ #
914
+ # @return [Integer, nil] Seconds, or nil if absent or in the date form.
915
+ #
916
+ def retry_after_seconds(value)
917
+ value.to_s.strip.match?(/\A\d+\z/) ? value.to_i : nil
918
+ end
825
919
  end
826
920
  end
827
921
  end
@@ -0,0 +1,8 @@
1
+ module Relaton
2
+ module Easc
3
+ class Bibdata < Item
4
+ model ItemData
5
+ include Bib::BibdataShared
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Relaton
2
+ module Easc
3
+ class Bibitem < Item
4
+ model ItemData
5
+ include Bib::BibitemShared
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,95 @@
1
+ require "net/http"
2
+
3
+ module Relaton
4
+ module Easc
5
+ # Index-backed retrieval for the EASC flavor (no scraping). Mirrors
6
+ # Relaton::Oiml::Bibliography: it parses the reference with
7
+ # Pubid::Easc, narrows the pre-built `relaton-data-easc` index by
8
+ # number, fetches the matching per-document YAML over Net::HTTP, and
9
+ # deserializes it into a Relaton::Easc::Item.
10
+ module Bibliography
11
+ ENDPOINT = "https://raw.githubusercontent.com/relaton/relaton-data-easc/main/".freeze
12
+
13
+ class << self
14
+ #
15
+ # Search for an EASC publication by its identifier.
16
+ #
17
+ # @param text [String, Pubid::Easc::Identifier] the EASC reference to
18
+ # look up (e.g. "ПМГ 03-2025", "РМГ 151-2025", or the Latin
19
+ # transliteration "PMG 03-2025")
20
+ # @param year [String, nil] the edition year (optional; usually embedded
21
+ # in the reference)
22
+ # @param _opts [Hash] options (unused)
23
+ #
24
+ # @return [Relaton::Easc::Item, nil] the publication or nil if not found
25
+ #
26
+ def search(text, year = nil, _opts = {})
27
+ pubid = text.is_a?(String) ? ::Pubid::Easc.parse(text) : text
28
+ Util.info "Fetching from Relaton repository ...", key: pubid.to_s
29
+ # Pass the pubid so Relaton::Index narrows candidates by number via
30
+ # binary search before applying the block. Every row's `:id` is a
31
+ # Pubid::Easc::Identifier (deserialized via the `pubid_class` passed to
32
+ # #index), so the block compares pubids and picks the latest edition.
33
+ row = index.search(pubid) { |r| pubid_match?(r[:id], pubid, year) }
34
+ .max_by { |r| r[:id].year.to_i }
35
+ unless row
36
+ Util.info "Not found.", key: pubid.to_s
37
+ return
38
+ end
39
+
40
+ uri = URI("#{ENDPOINT}#{row[:file]}")
41
+ resp = Net::HTTP.get_response uri
42
+ unless resp.code == "200"
43
+ raise Relaton::RequestError, "Could not access #{uri}: HTTP #{resp.code}"
44
+ end
45
+
46
+ item = Relaton::Easc::Item.from_yaml resp.body
47
+ Util.info "Found: `#{item.docidentifier.first&.content}`", key: pubid.to_s
48
+ item.tap { |i| i.fetched = Date.today.to_s }
49
+ rescue SocketError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
50
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
51
+ Net::ProtocolError, Net::ReadTimeout, OpenSSL::SSL::SSLError,
52
+ Errno::ETIMEDOUT => e
53
+ raise Relaton::RequestError, "Could not access #{uri}: #{e.message}"
54
+ end
55
+
56
+ #
57
+ # Fetch an EASC publication. EASC citations are always dated
58
+ # (e.g. "ПМГ 03-2025"), so there is no undated/most-recent handling to
59
+ # mirror from the ISO/OIML fetchers — `get` just returns what `search`
60
+ # resolves.
61
+ #
62
+ # @see #search
63
+ #
64
+ def get(ref, year = nil, opts = {})
65
+ search(ref, year, opts)
66
+ end
67
+
68
+ private
69
+
70
+ def index
71
+ Relaton::Index.find_or_create(
72
+ :easc,
73
+ url: "#{ENDPOINT}#{INDEXFILE}.zip",
74
+ file: "#{INDEXFILE}.yaml",
75
+ pubid_class: ::Pubid::Easc::Identifier,
76
+ )
77
+ end
78
+
79
+ # Both `row_id` and `query` are Pubid::Easc::Identifier instances. A row
80
+ # matches when its series, defense variant, and number are equal (so a
81
+ # ПМГ never matches a РМГ, and the «В» defense variant never matches the
82
+ # plain series). Year is nil-tolerant: an unqualified query finds the
83
+ # latest edition (selected by `max_by` in #search); the `year` argument
84
+ # lets a caller pin an edition the reference string omitted.
85
+ def pubid_match?(row_id, query, year)
86
+ wanted_year = (query.year || year)&.to_s
87
+ row_id.series == query.series &&
88
+ row_id.variant.to_s == query.variant.to_s &&
89
+ row_id.number.to_s == query.number.to_s &&
90
+ (wanted_year.nil? || row_id.year.to_s == wanted_year)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,100 @@
1
+ module Relaton
2
+ module Easc
3
+ # Structured EASC document identifier. The docid string is the canonical
4
+ # citation form (e.g. "ПМГ 03-2025", "РМГ 151-2025"), parsed into a
5
+ # `Pubid::Easc::Identifier` kept in `@pubid` while the lutaml `content`
6
+ # attribute stays a plain string for serialization.
7
+ #
8
+ # Parsing is **soft**: `content=` lazily requires pubid and rescues
9
+ # `LoadError`/`StandardError`, so a missing gem or non-EASC content leaves
10
+ # `@pubid` nil rather than raising.
11
+ #
12
+ # This was an empty subclass, which meant it inherited
13
+ # `Bib::Docidentifier`'s abstract `remove_part!` / `remove_date!` /
14
+ # `to_all_parts!`. `Bib::ItemData` broadcasts all three to every
15
+ # docidentifier, so `#to_all_parts` and `#to_most_recent_reference` raised
16
+ # `NotImplementedError` on **every** EASC item — and it descends from
17
+ # `ScriptError`, so a caller's `rescue => e` did not catch it.
18
+ class Docidentifier < Bib::Docidentifier
19
+ attr_reader :pubid
20
+
21
+ # Capture the inherited content setter before overriding #content=, so
22
+ # #refresh_content! can write the re-rendered string back WITHOUT
23
+ # re-parsing (a re-parse would rebuild @pubid and discard in-place
24
+ # 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::Easc.parse(value)
41
+ rescue LoadError, StandardError
42
+ nil
43
+ end
44
+ end
45
+
46
+ # `Pubid::Easc::Identifier` models publisher/series/variant/number/year,
47
+ # so the mapping is EASC-specific:
48
+ #
49
+ # - **`remove_date!` → clears `year`.** EASC carries no separate date
50
+ # component; the year is the edition discriminator, so clearing it
51
+ # yields the undated ("most recent") reference: `ПМГ 03-2025` →
52
+ # `ПМГ 03`.
53
+ # - **`remove_part!` → clears the (unused) `part`/`subpart`.** EASC models
54
+ # no part, so this is a no-op for the rendered string; implemented so it
55
+ # never raises, and it starts working if pubid-easc ever adds one.
56
+ # - **`to_all_parts!` → both, then wraps `@pubid` in pubid's `AllParts`.**
57
+ # `content` stays the plain stripped id — EASC's own renderer has no
58
+ # "(all parts)" marker, and `content` is cached from the pre-wrap
59
+ # pubid on purpose. `#pubid` itself, read directly, now answers
60
+ # `all_parts? == true` and renders WITH pubid's generic marker
61
+ # (`#to_s`), since it's the wrapper.
62
+ #
63
+ # All three no-op safely when `@pubid` is nil.
64
+
65
+ def remove_part!
66
+ clear_attr!(:part)
67
+ end
68
+
69
+ def remove_date!
70
+ clear_attr!(:year)
71
+ end
72
+
73
+ def to_all_parts!
74
+ return if !@pubid || @pubid.all_parts?
75
+
76
+ # `#exclude` (no args) rebuilds a full independent copy — `remove_part!`
77
+ # / `remove_date!` mutate `@pubid` in place, so a bare reference here
78
+ # would lose the original year to that mutation too.
79
+ original = @pubid.exclude
80
+ remove_part!
81
+ remove_date!
82
+ @pubid = original.to_all_parts
83
+ end
84
+
85
+ private
86
+
87
+ def clear_attr!(attr)
88
+ return unless @pubid && @pubid.respond_to?("#{attr}=")
89
+
90
+ @pubid.public_send("#{attr}=", nil)
91
+ @pubid.subpart = nil if attr == :part && @pubid.respond_to?(:subpart=)
92
+ refresh_content!
93
+ end
94
+
95
+ def refresh_content!
96
+ store_content(@pubid.to_s) if @pubid
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,14 @@
1
+ module Relaton
2
+ module Easc
3
+ # EASC document types. Two series, both issued by the Eurasian
4
+ # Economic Standards Council:
5
+ # * PMG — ПМГ (Правила по межгосударственной стандартизации)
6
+ # * RMG — РМГ (Рекомендации по межгосударственной стандартизации)
7
+ class Doctype < Bib::Doctype
8
+ TYPES = %w[
9
+ pmg
10
+ rmg
11
+ ].freeze
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ module Relaton
2
+ module Easc
3
+ # EASC bibliographic item extension. Adds EASC-specific structured
4
+ # metadata on top of the shared Relaton::Bib::Ext so the fields
5
+ # round-trip through both XML and YAML without per-repo merge hacks.
6
+ #
7
+ # Fields:
8
+ # * urn — urn:easc:<series>:...
9
+ # * webpage — mgscatalog.by detail URL
10
+ # * session — МГС council session that adopted the doc
11
+ # (e.g. "67МГС", "19МГС")
12
+ # * developer — Разработчик (e.g. "Российская Федерация")
13
+ # * joining_states — country codes of states that joined
14
+ # (collection: e.g. ["АРМ", "БЕИ", "КАЗ"])
15
+ # * assigned_to — Закреплен за (e.g. "МТК 536 Методология...")
16
+ class Ext < Bib::Ext
17
+ attribute :doctype, Doctype
18
+ attribute :urn, :string
19
+ attribute :webpage, :string
20
+ attribute :session, :string
21
+ attribute :developer, :string
22
+ attribute :joining_states, :string, collection: true, initialize_empty: true
23
+ attribute :assigned_to, :string
24
+
25
+ xml do
26
+ map_element "urn", to: :urn
27
+ map_element "webpage", to: :webpage
28
+ map_element "session", to: :session
29
+ map_element "developer", to: :developer
30
+ map_element "joining_states", to: :joining_states
31
+ map_element "assigned_to", to: :assigned_to
32
+ end
33
+
34
+ key_value do
35
+ map_element "urn", to: :urn
36
+ map_element "webpage", to: :webpage
37
+ map_element "session", to: :session
38
+ map_element "developer", to: :developer
39
+ map_element "joining_states", to: :joining_states
40
+ map_element "assigned_to", to: :assigned_to
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ module Relaton
2
+ module Easc
3
+ # EASC bibliographic item. Re-declares +ext+ to use the typed Ext
4
+ # subclass so EASC-specific fields round-trip natively.
5
+ class Item < Bib::Item
6
+ model ItemData
7
+
8
+ attribute :ext, Ext
9
+ attribute :docidentifier, Docidentifier, collection: true,
10
+ initialize_empty: true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module Relaton
2
+ module Easc
3
+ class ItemBase < Lutaml::Model::Serializable
4
+ include Bib::NamespaceHelper
5
+
6
+ attr_accessor :type
7
+
8
+ model ItemData
9
+
10
+ instance_exec(&Bib::ItemShared::ATTRIBUTES)
11
+
12
+ xml do
13
+ map_attribute "type", to: :type
14
+ instance_exec(&Bib::ItemShared::XML_BODY)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ module Relaton
2
+ module Easc
3
+ class ItemData < Bib::ItemData
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,46 @@
1
+ require "relaton/core/processor"
2
+
3
+ module Relaton
4
+ module Easc
5
+ # Relaton processor for the EASC flavor. Registered by
6
+ # Relaton::Db::Registry when the unified gem loads (see
7
+ # lib/relaton/db/registry.rb). Supports `relaton fetch easc ...`
8
+ # once the relaton-data-easc dataset is installed.
9
+ class Processor < Core::Processor
10
+ attr_reader :idtype
11
+
12
+ def initialize
13
+ @short = :relaton_easc
14
+ @prefix = "EASC"
15
+ # Both Cyrillic and Latin series prefixes route here.
16
+ @defaultprefix = %r{^(?:ПМГ|РМГ|PMG|RMG)\b}
17
+ @idtype = "EASC"
18
+ end
19
+
20
+ def get(code, date, opts)
21
+ require_relative "../easc"
22
+ Bibliography.get(code, date, opts)
23
+ end
24
+
25
+ def from_xml(xml)
26
+ require_relative "../easc"
27
+ Item.from_xml xml
28
+ end
29
+
30
+ def from_yaml(yaml)
31
+ require_relative "../easc"
32
+ Item.from_yaml(yaml)
33
+ end
34
+
35
+ def grammar_hash
36
+ require_relative "../easc"
37
+ @grammar_hash ||= ::Relaton::Easc.grammar_hash
38
+ end
39
+
40
+ def remove_index_file
41
+ require_relative "../easc"
42
+ Relaton::Index.find_or_create(:easc, url: true, file: "#{INDEXFILE}.yaml").remove_file
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,8 @@
1
+ module Relaton
2
+ module Easc
3
+ module Util
4
+ extend Relaton::Bib::Util
5
+ PROGNAME = "relaton-easc".freeze
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "relaton/bib"
4
+ require "relaton/index"
5
+ require "relaton/version"
6
+ require "pubid"
7
+
8
+ # Relaton::Easc is the EASC (Евразийский экономический совет /
9
+ # Eurasian Economic Standards Council) flavor of the unified relaton
10
+ # gem. Adds typed Ext fields (urn, webpage, session, developer,
11
+ # joining_states) so they round-trip natively through YAML and XML
12
+ # without per-repo merge hacks.
13
+ module Relaton
14
+ module Easc
15
+ INDEXFILE = "index-v2".freeze
16
+
17
+ class Error < StandardError; end
18
+
19
+ autoload :Doctype, "relaton/easc/doctype"
20
+ autoload :Ext, "relaton/easc/ext"
21
+ autoload :ItemData, "relaton/easc/item_data"
22
+ autoload :ItemBase, "relaton/easc/item_base"
23
+ autoload :Item, "relaton/easc/item"
24
+ autoload :Bibitem, "relaton/easc/bibitem"
25
+ autoload :Bibdata, "relaton/easc/bibdata"
26
+ autoload :Docidentifier, "relaton/easc/docidentifier"
27
+ autoload :Util, "relaton/easc/util"
28
+ autoload :Bibliography, "relaton/easc/bibliography"
29
+ autoload :Processor, "relaton/easc/processor"
30
+
31
+ def self.grammar_hash
32
+ Digest::MD5.hexdigest Relaton::VERSION
33
+ end
34
+ end
35
+ end