relaton-doi 1.20.2 → 2.0.0.pre.alpha.1

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.
@@ -1,806 +0,0 @@
1
- module RelatonDoi
2
- class Parser
3
- COUNTRIES = %w[USA].freeze
4
-
5
- TYPES = {
6
- "book-chapter" => "inbook",
7
- "book-part" => "inbook",
8
- "book-section" => "inbook",
9
- "book-series" => "book",
10
- "book-set" => "book",
11
- "book-track" => "inbook",
12
- "component" => "misc",
13
- "database" => "dataset",
14
- "dissertation" => "thesis",
15
- "edited-book" => "book",
16
- "grant" => "misc",
17
- "journal-article" => "article",
18
- "journal-issue" => "article",
19
- "journal-volume" => "journal",
20
- "monograph" => "book",
21
- "other" => "misc",
22
- "peer-review" => "article",
23
- "posted-content" => "dataset",
24
- "proceedings-article" => "inproceedings",
25
- "proceedings-series" => "proceedings",
26
- "reference-book" => "book",
27
- "reference-entry" => "inbook",
28
- "report-component" => "techreport",
29
- "report-series" => "techreport",
30
- "report" => "techreport",
31
- }.freeze
32
-
33
- REALATION_TYPES = {
34
- "is-cited-by" => "isCitedIn",
35
- "belongs-to" => "related",
36
- "is-child-of" => "includedIn",
37
- "is-expression-of" => "expressionOf",
38
- "has-expression" => "hasExpression",
39
- "is-manifestation-of" => "manifestationOf",
40
- "is-manuscript-of" => "draftOf",
41
- "has-manuscript" => "hasDraft",
42
- "is-preprint-of" => "draftOf",
43
- "has-preprint" => "hasDraft",
44
- "is-replaced-by" => "obsoletedBy",
45
- "replaces" => "obsoletes",
46
- "is-translation-of" => "translatedFrom",
47
- "has-translation" => "hasTranslation",
48
- "is-version-of" => "editionOf",
49
- "has-version" => "hasEdition",
50
- "is-based-on" => "updates",
51
- "is-basis-for" => "updatedBy",
52
- "is-comment-on" => "commentaryOf",
53
- "has-comment" => "hasCommentary",
54
- "is-continued-by" => "hasSuccessor",
55
- "continues" => "successorOf",
56
- "is-derived-from" => "derives",
57
- "has-derivation" => "derivedFrom",
58
- "is-documented-by" => "describedBy",
59
- "documents" => "describes",
60
- "is-part-of" => "partOf",
61
- "has-part" => "hasPart",
62
- "is-review-of" => "reviewOf",
63
- "has-review" => "hasReview",
64
- "references" => "cites",
65
- "is-referenced-by" => "isCitedIn",
66
- "requires" => "hasComplement",
67
- "is-required-by" => "complementOf",
68
- "is-supplement-to" => "complementOf",
69
- "is-supplemented-by" => "hasComplement",
70
- }.freeze
71
-
72
- ATTRS = %i[type fetched title docid date link abstract contributor place
73
- doctype relation extent series medium].freeze
74
-
75
- CROSSREF_API_URL = "https://api.crossref.org/works?query=%{query}&filter=%{filter}".freeze
76
- MAX_RETRIES = 3
77
-
78
- #
79
- # Initialize instance.
80
- #
81
- # @param [Hash] src The source hash.
82
- #
83
- def initialize(src)
84
- @src = src
85
- @item = {}
86
- end
87
-
88
- #
89
- # Initialize instance and parse the source hash.
90
- #
91
- # @param [Hash] src The source hash.
92
- #
93
- # @return [RelatonBib::BibliographicItem, RelatonIetf::IetfBibliographicItem,
94
- # RelatonBipm::BipmBibliographicItem, RelatonIeee::IeeeBibliographicItem,
95
- # RelatonNist::NistBibliographicItem] The bibitem.
96
- #
97
- def self.parse(src)
98
- new(src).parse
99
- end
100
-
101
- #
102
- # Parse the source hash.
103
- #
104
- # @return [RelatonBib::BibliographicItem, RelatonIetf::IetfBibliographicItem,
105
- # RelatonBipm::BipmBibliographicItem, RelatonIeee::IeeeBibliographicItem,
106
- # RelatonNist::NistBibliographicItem] The bibitem.
107
- #
108
- def parse
109
- ATTRS.each { |m| @item[m] = send "parse_#{m}" }
110
- create_bibitem @src["DOI"], @item
111
- end
112
-
113
- #
114
- # Create a bibitem from the bibitem hash.
115
- #
116
- # @param [String] doi The DOI.
117
- # @param [Hash] bibitem The bibitem hash.
118
- #
119
- # @return [RelatonBib::BibliographicItem, RelatonIetf::IetfBibliographicItem,
120
- # RelatonBipm::BipmBibliographicItem, RelatonIeee::IeeeBibliographicItem,
121
- # RelatonNist::NistBibliographicItem] The bibitem.
122
- #
123
- def create_bibitem(doi, bibitem) # rubocop:disable Metrics/CyclomaticComplexity
124
- case doi
125
- when /\/nist/ then RelatonNist::NistBibliographicItem.new(**bibitem)
126
- when /\/rfc\d+/ then RelatonIetf::IetfBibliographicItem.new(**bibitem)
127
- when /\/0026-1394\// then RelatonBipm::BipmBibliographicItem.new(**bibitem)
128
- when /\/ieee/ then RelatonIeee::IeeeBibliographicItem.new(**bibitem)
129
- else RelatonBib::BibliographicItem.new(**bibitem)
130
- end
131
- end
132
-
133
- #
134
- # Parse the type.
135
- #
136
- # @return [String] The type.
137
- #
138
- def parse_type
139
- TYPES[@src["type"]] || @src["type"]
140
- end
141
-
142
- #
143
- # Parse the document type
144
- #
145
- # @return [String] The document type.
146
- #
147
- def parse_doctype
148
- RelatonBib::DocumentType.new type: @src["type"]
149
- end
150
-
151
- #
152
- # Parse the fetched date.
153
- #
154
- # @return [String] The fetched date.
155
- #
156
- def parse_fetched
157
- Date.today.to_s
158
- end
159
-
160
- #
161
- # Parse titles from the source hash.
162
- #
163
- # @return [Array<Hash>] The titles.
164
- #
165
- def parse_title # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
166
- if @src["title"].is_a?(Array) && @src["title"].any?
167
- main_sub_titles
168
- elsif @src["project"].is_a?(Array) && @src["project"].any?
169
- project_titles
170
- elsif @src["container-title"].is_a?(Array) && @src["container-title"].size > 1
171
- @src["container-title"][0..-2].map { |t| create_title t }
172
- else []
173
- end
174
- end
175
-
176
- #
177
- # Parse main and subtitle from the source hash.
178
- #
179
- # @return [Array<Hash>] The titles.
180
- #
181
- def main_sub_titles
182
- title = @src["title"].map { |t| create_title t }
183
- RelatonBib.array(@src["subtitle"]).each { |t| title << create_title(t, "subtitle") }
184
- RelatonBib.array(@src["short-title"]).each { |t| title << create_title(t, "short") }
185
- title
186
- end
187
-
188
- #
189
- # Fetch titles from the projects.
190
- #
191
- # @return [Array<Hash>] The titles.
192
- #
193
- def project_titles
194
- RelatonBib.array(@src["project"]).reduce([]) do |memo, proj|
195
- memo + RelatonBib.array(proj["project-title"]).map { |t| create_title t["title"] }
196
- end
197
- end
198
-
199
- #
200
- # Create a title from the title and type.
201
- #
202
- # @param [String] title The title content.
203
- # @param [String] type The title type. Defaults to "main".
204
- #
205
- # @return [RelatonBib::TypedTitleString] The title.
206
- #
207
- def create_title(title, type = "main")
208
- cnt = str_cleanup title
209
- RelatonBib::TypedTitleString.new type: type, content: cnt, script: "Latn"
210
- end
211
-
212
- #
213
- # Parse a docid from the source hash.
214
- #
215
- # @return [Array<RelatonBib::DocumentIdentifier>] The docid.
216
- #
217
- def parse_docid
218
- %w[DOI ISBN ISSN].each_with_object([]) do |type, obj|
219
- prm = type == "DOI"
220
- RelatonBib.array(@src[type]).each do |id|
221
- t = issn_type(type, id)
222
- obj << RelatonBib::DocumentIdentifier.new(type: t, id: id, primary: prm)
223
- end
224
- end
225
- end
226
-
227
- #
228
- # Create an ISSN type if it's an ISSN ID.
229
- #
230
- # @param [String] type identifier type
231
- # @param [String] id identifier
232
- #
233
- # @return [String] identifier type
234
- #
235
- def issn_type(type, id)
236
- return type unless type == "ISSN"
237
-
238
- t = @src["issn-type"]&.find { |it| it["value"] == id }&.dig("type")
239
- t ? "issn.#{t}" : type.downcase
240
- end
241
-
242
- #
243
- # Parce dates from the source hash.
244
- #
245
- # @return [Array<RelatonBib::BibliographicDate>] The dates.
246
- #
247
- def parse_date # rubocop:disable Metrics/CyclomaticComplexity
248
- dates = %w[issued published approved].each_with_object([]) do |type, obj|
249
- next unless @src.dig(type, "date-parts")&.first&.compact&.any?
250
-
251
- obj << RelatonBib::BibliographicDate.new(type: type, on: date_type(type))
252
- end
253
- if dates.none?
254
- dates << RelatonBib::BibliographicDate.new(type: "created", on: date_type("created"))
255
- end
256
- dates
257
- end
258
-
259
- #
260
- # Join date parts into a string.
261
- #
262
- # @param [String] type The date type.
263
- #
264
- # @return [String] The date string.
265
- #
266
- def date_type(type)
267
- @src[type]["date-parts"][0].map { |d| d.to_s.rjust(2, "0") }.join "-"
268
- end
269
-
270
- #
271
- # Parse links from the source hash.
272
- #
273
- # @return [Array<RelatonBib::TypedUri>] The links.
274
- #
275
- def parse_link # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
276
- disprefered_links = %w[similarity-checking text-mining]
277
- links = []
278
- if @src["URL"]
279
- links << RelatonBib::TypedUri.new(type: "DOI", content: @src["URL"])
280
- end
281
- [@src["link"], @src.dig("resource", "primary")].flatten.compact.each do |l|
282
- next if disprefered_links.include? l["intended-application"]
283
-
284
- type = case l["URL"]
285
- when /\.pdf$/ then "pdf"
286
- # when /\/rfc\d+$|iopscience\.iop\.org|ieeexplore\.ieee\.org/
287
- else "src"
288
- end
289
- links << RelatonBib::TypedUri.new(type: type, content: l["URL"]) # if type
290
- end
291
- links
292
- end
293
-
294
- #
295
- # Parse abstract from the source hash.
296
- #
297
- # @return [Array<RelatonBib::FormattedString>] The abstract.
298
- #
299
- def parse_abstract
300
- return [] unless @src["abstract"]
301
-
302
- content = @src["abstract"]
303
- abstract = RelatonBib::FormattedString.new(
304
- content: content, language: "en", script: "Latn", format: "text/html",
305
- )
306
- [abstract]
307
- end
308
-
309
- #
310
- # Parse contributors from the source hash.
311
- #
312
- # @return [Array<RelatonBib::ContributionInfo>] The contributors.
313
- #
314
- def parse_contributor
315
- contribs = author_investigators
316
- contribs += authors_editors_translators
317
- contribs += contribs_from_parent(contribs)
318
- contribs << contributor(org_publisher, "publisher")
319
- contribs += org_aurhorizer
320
- contribs + org_enabler
321
- end
322
-
323
- #
324
- # Create authors investigators from the source hash.
325
- #
326
- # @return [Array<RelatonBib::ContributionInfo>] The authors investigators.
327
- #
328
- def author_investigators
329
- RelatonBib.array(@src["project"]).reduce([]) do |memo, proj|
330
- memo + create_investigators(proj, "lead-investigator") +
331
- create_investigators(proj, "investigator")
332
- end
333
- end
334
-
335
- #
336
- # Create investigators from the project.
337
- #
338
- # @param [Hash] project The project hash.
339
- # @param [String] type The investigator type. "lead-investigator" or "investigator".
340
- #
341
- # @return [Array<RelatonBib::ContributionInfo>] The investigators.
342
- #
343
- def create_investigators(project, type)
344
- description = type.gsub("-", " ")
345
- RelatonBib.array(project[type]).map do |inv|
346
- contributor(create_person(inv), "author", description)
347
- end
348
- end
349
-
350
- #
351
- # Create authors editors translators from the source hash.
352
- #
353
- # @return [Array<RelatonBib::ContributionInfo>] The authors editors translators.
354
- #
355
- def authors_editors_translators
356
- %w[author editor translator].each_with_object([]) do |type, a|
357
- @src[type]&.each do |c|
358
- contrib = if c["family"]
359
- create_person(c)
360
- else
361
- RelatonBib::Organization.new(name: str_cleanup(c["name"]))
362
- end
363
- a << contributor(contrib, type)
364
- end
365
- end
366
- end
367
-
368
- #
369
- # Fetch authors and editors from parent if they are not present in the book part.
370
- #
371
- # @param [Array<RelatonBib::ContributionInfo>] contribs present contributors
372
- #
373
- # @return [Array<RelatonBib::ContributionInfo>] contributors with authors and editors from parent
374
- #
375
- def contribs_from_parent(contribs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
376
- return [] unless %w[inbook inproceedings dataset].include?(parse_type) && @src["container-title"]
377
-
378
- has_authors = contribs.any? { |c| c.role&.any? { |r| r.type == "author" } }
379
- has_editors = contribs.any? { |c| c.role&.any? { |r| r.type == "editor" } }
380
- return [] if has_authors && has_editors
381
-
382
- create_authors_editors(has_authors, "author")
383
- end
384
-
385
- #
386
- # Fetch parent item from Crossref.
387
- #
388
- # @return [Hash, nil] parent item
389
- #
390
- def parent_item
391
- @parent_item ||= begin
392
- query = CGI.escape [@src["container-title"][0], fetch_year].compact.join("+")
393
- filter = "type:#{%w[book book-set edited-book monograph reference-book].join ',type:'}"
394
- items = fetch_crossref(query: query, filter: filter)
395
- items&.detect { |i| i["title"].include? @src["container-title"][0] }
396
- end
397
- end
398
-
399
- #
400
- # Create authors and editors from parent item.
401
- #
402
- # @param [Boolean] has true if authors or editors are present in the book part
403
- # @param [String] type "author" or "editor"
404
- #
405
- # @return [Array<RelatonBib::ContributionInfo>] authors or editors
406
- #
407
- def create_authors_editors(has, type)
408
- return [] if has || !parent_item
409
-
410
- RelatonBib.array(parent_item[type]).map { |a| contributor(create_person(a), type) }
411
- end
412
-
413
- #
414
- # Cerate an organization publisher from the source hash.
415
- #
416
- # @return [RelatonBib::Organization] The organization.
417
- #
418
- def org_publisher
419
- pbr = @src["institution"]&.detect do |i|
420
- @src["publisher"].include?(i["name"]) ||
421
- i["name"].include?(@src["publisher"])
422
- end
423
- a = pbr["acronym"]&.first if pbr
424
- RelatonBib::Organization.new name: str_cleanup(@src["publisher"]), abbreviation: a
425
- end
426
-
427
- #
428
- # Clean up trailing punctuation and whitespace from a string.
429
- #
430
- # @param [String] str The string to clean up.
431
- #
432
- # @return [String] The cleaned up string.
433
- #
434
- def str_cleanup(str)
435
- str.strip.sub(/[,\/\s]+$/, "").sub(/\s:$/, "")
436
- end
437
-
438
- #
439
- # Parse authorizer contributor from the source hash.
440
- #
441
- # @return [Array<RelatonBib::ContributionInfo>] The authorizer contributor.
442
- #
443
- def org_aurhorizer
444
- return [] unless @src["standards-body"]
445
-
446
- name, acronym = @src["standards-body"].values_at("name", "acronym")
447
- org = RelatonBib::Organization.new name: name, abbreviation: acronym
448
- [contributor(org, "authorizer")]
449
- end
450
-
451
- #
452
- # Parse enabler contributor from the source hash.
453
- #
454
- # @return [Array<RelatonBib::ContributionInfo>] The enabler contributor.
455
- #
456
- def org_enabler
457
- RelatonBib.array(@src["project"]).each_with_object([]) do |proj, memo|
458
- proj["funding"].each do |f|
459
- memo << create_enabler(f.dig("funder", "name"))
460
- end
461
- end + RelatonBib.array(@src["funder"]).map { |f| create_enabler f["name"] }
462
- end
463
-
464
- #
465
- # Create enabler contributor with type "enabler".
466
- #
467
- # @param [String] name <description>
468
- #
469
- # @return [RelatonBib::ContributionInfo] The enabler contributor.
470
- #
471
- def create_enabler(name)
472
- org = RelatonBib::Organization.new name: name
473
- contributor(org, "enabler")
474
- end
475
-
476
- #
477
- # Create contributor from an entity and a role type.
478
- #
479
- # @param [RelatonBib::Person, RelatonBib::Organization] entity The entity.
480
- # @param [String] type The role type.
481
- #
482
- # @return [RelatonBib::ContributionInfo] The contributor.
483
- #
484
- def contributor(entity, type, descriprion = nil)
485
- role = { type: type }
486
- role[:description] = [descriprion] if descriprion
487
- RelatonBib::ContributionInfo.new(entity: entity, role: [role])
488
- end
489
-
490
- #
491
- # Create a person from a person hash.
492
- #
493
- # @param [Hash] person The person hash.
494
- #
495
- # @return [RelatonBib::Person] The person.
496
- #
497
- def create_person(person)
498
- RelatonBib::Person.new(
499
- name: create_person_name(person),
500
- affiliation: create_affiliation(person),
501
- identifier: person_id(person),
502
- )
503
- end
504
-
505
- #
506
- # Create person affiliations from a person hash.
507
- #
508
- # @param [Hash] person The person hash.
509
- #
510
- # @return [Array<RelatonBib::Affiliation>] The affiliations.
511
- #
512
- def create_affiliation(person)
513
- (person["affiliation"] || []).map do |a|
514
- org = RelatonBib::Organization.new(name: a["name"])
515
- RelatonBib::Affiliation.new organization: org
516
- end
517
- end
518
-
519
- #
520
- # Create a person full name from a person hash.
521
- #
522
- # @param [Hash] person The person hash.
523
- #
524
- # @return [RelatonBib::FullName] The full name.
525
- #
526
- def create_person_name(person)
527
- surname = titlecase(person["family"])
528
- sn = RelatonBib::LocalizedString.new(surname, "en", "Latn")
529
- RelatonBib::FullName.new(
530
- surname: sn, forename: forename(person), addition: nameaddition(person),
531
- completename: completename(person), prefix: nameprefix(person)
532
- )
533
- end
534
-
535
- #
536
- # Capitalize the first letter of each word in a string except for words that
537
- # are 2 letters or less.
538
- #
539
- # @param [<Type>] str <description>
540
- #
541
- # @return [<Type>] <description>
542
- #
543
- def titlecase(str)
544
- str.split.map do |s|
545
- if s.size > 2 && s.upcase == s && !/\.&/.match?(s)
546
- s.capitalize
547
- else
548
- s
549
- end
550
- end.join " "
551
- end
552
-
553
- #
554
- # Create a person name prefix from a person hash.
555
- #
556
- # @param [Hash] person The person hash.
557
- #
558
- # @return [Array<RelatonBib::LocalizedString>] The name prefix.
559
- #
560
- def nameprefix(person)
561
- return [] unless person["prefix"]
562
-
563
- [RelatonBib::LocalizedString.new(person["prefix"], "en", "Latn")]
564
- end
565
-
566
- #
567
- # Create a complete name from a person hash.
568
- #
569
- # @param [Hash] person The person hash.
570
- #
571
- # @return [RelatonBib::LocalizedString] The complete name.
572
- #
573
- def completename(person)
574
- return unless person["name"]
575
-
576
- RelatonBib::LocalizedString.new(person["name"], "en", "Latn")
577
- end
578
-
579
- #
580
- # Create a forename from a person hash.
581
- #
582
- # @param [Hash] person The person hash.
583
- #
584
- # @return [Array<RelatonBib::LocalizedString>] The forename.
585
- #
586
- def forename(person)
587
- return [] unless person["given"]
588
-
589
- fname = titlecase(person["given"])
590
- [RelatonBib::Forename.new(content: fname, language: "en", script: "Latn")]
591
- end
592
-
593
- #
594
- # Create an addition from a person hash.
595
- #
596
- # @param [Hash] person The person hash.
597
- #
598
- # @return [Array<RelatonBib::LocalizedString>] The addition.
599
- #
600
- def nameaddition(person)
601
- return [] unless person["suffix"]
602
-
603
- [RelatonBib::LocalizedString.new(person["suffix"], "en", "Latn")]
604
- end
605
-
606
- #
607
- # Create a person identifier from a person hash.
608
- #
609
- # @param [Hash] person The person hash.
610
- #
611
- # @return [Array<RelatonBib::PersonIdentifier>] The person identifier.
612
- #
613
- def person_id(person)
614
- return [] unless person["ORCID"]
615
-
616
- [RelatonBib::PersonIdentifier.new("orcid", person["ORCID"])]
617
- end
618
-
619
- #
620
- # Parse a place from the source hash.
621
- #
622
- # @return [Array<RelatonBib::Place>] The place.
623
- #
624
- def parse_place # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
625
- pub_location = @src["publisher-location"] || fetch_location
626
- return [] unless pub_location
627
-
628
- pls1, pls2 = pub_location.split(", ")
629
- pls1 = str_cleanup pls1
630
- pls2 &&= str_cleanup pls2
631
- if COUNTRIES.include? pls2
632
- country = RelatonBib::Place::RegionType.new(name: pls2)
633
- [RelatonBib::Place.new(city: pls1, country: [country])]
634
- elsif pls2 && pls2 == pls2&.upcase
635
- region = RelatonBib::Place::RegionType.new(name: pls2)
636
- [RelatonBib::Place.new(city: pls1, region: [region])]
637
- elsif pls1 == pls2 || pls2.nil? || pls2.empty?
638
- [RelatonBib::Place.new(city: pls1)]
639
- else
640
- [RelatonBib::Place.new(city: pls1), RelatonBib::Place.new(city: pls2)]
641
- end
642
- end
643
-
644
- #
645
- # Fetch location from container.
646
- #
647
- # @return [String, nil] The location.
648
- #
649
- def fetch_location
650
- title = @item[:title].first&.title&.content
651
- qparts = [title, fetch_year, @src["publisher"]]
652
- query = CGI.escape qparts.compact.join("+").gsub(" ", "+")
653
- filter = "type:#{%w[book-chapter book-part book-section book-track].join(',type:')}"
654
- items = fetch_crossref(query: query, filter: filter)
655
- items&.detect do |i|
656
- i["publisher-location"] && i["container-title"].include?(title)
657
- end&.dig("publisher-location")
658
- end
659
-
660
- #
661
- # Parse relations from the source hash.
662
- #
663
- # @return [Array<RelatonBib::DocumentRelation>] The relations.
664
- #
665
- def parse_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
666
- rels = included_in_relation
667
- @src["relation"].each_with_object(rels) do |(k, v), a|
668
- type, desc = relation_type k
669
- RelatonBib.array(v).each do |r|
670
- rel_item = Crossref.get_by_id r["id"]
671
- title = rel_item["title"].map { |t| create_title t }
672
- docid = RelatonBib::DocumentIdentifier.new(id: r["id"], type: "DOI")
673
- bib = create_bibitem r["id"], title: title, docid: [docid]
674
- a << RelatonBib::DocumentRelation.new(type: type, description: desc, bibitem: bib)
675
- end
676
- end
677
- end
678
-
679
- #
680
- # Transform crossref relation type to relaton relation type.
681
- #
682
- # @param [String] crtype The crossref relation type.
683
- #
684
- # @return [Array<String>] The relaton relation type and description.
685
- #
686
- def relation_type(crtype)
687
- type = REALATION_TYPES[crtype] || begin
688
- desc = RelatonBib::FormattedString.new(content: crtype)
689
- "related"
690
- end
691
- [type, desc]
692
- end
693
-
694
- #
695
- # Create included in relation.
696
- #
697
- # @return [Array<RelatonBib::DocumentRelation>] The relations.
698
- #
699
- def included_in_relation
700
- types = %w[
701
- book book-chapter book-part book-section book-track dataset journal-issue
702
- journal-value proceedings-article reference-entry report-component
703
- ]
704
- return [] unless @src["container-title"] && types.include?(@src["type"])
705
-
706
- @src["container-title"].map do |ct|
707
- contrib = create_authors_editors false, "editor"
708
- bib = RelatonBib::BibliographicItem.new(title: [content: ct], contributor: contrib)
709
- RelatonBib::DocumentRelation.new(type: "includedIn", bibitem: bib)
710
- end
711
- end
712
-
713
- #
714
- # Fetch year from the source hash.
715
- #
716
- # @return [String] The year.
717
- #
718
- def fetch_year
719
- d = @src["published"] || @src["approved"] || @src["created"]
720
- d["date-parts"][0][0]
721
- end
722
-
723
- #
724
- # Parse an extent from the source hash.
725
- #
726
- # @return [Array<RelatonBib::Locality>] The extent.
727
- #
728
- def parse_extent # rubocop:disable Metrics/AbcSize
729
- extent = []
730
- extent << RelatonBib::Locality.new("volume", @src["volume"]) if @src["volume"]
731
- extent << RelatonBib::Locality.new("issue", @src["issue"]) if @src["issue"]
732
- if @src["page"]
733
- from, to = @src["page"].split("-")
734
- extent << RelatonBib::Locality.new("page", from, to)
735
- end
736
- extent.any? ? [RelatonBib::Extent.new(extent)] : []
737
- end
738
-
739
- #
740
- # Parse a series from the source hash.
741
- #
742
- # @return [Arrey<RelatonBib::Series>] The series.
743
- #
744
- def parse_series # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
745
- types = %w[inbook incollection inproceedings]
746
- return [] if !@src["container-title"] || types.include?(@item[:type]) || @src["type"] == "report-component"
747
-
748
- con_ttl = if main_sub_titles.any? || project_titles.any?
749
- @src["container-title"]
750
- elsif @src["container-title"].size > 1
751
- sct = @src["short-container-title"]&.last
752
- abbrev = RelatonBib::LocalizedString.new sct if sct
753
- @src["container-title"][-1..-1]
754
- else []
755
- end
756
- con_ttl.map do |ct|
757
- title = RelatonBib::TypedTitleString.new content: ct
758
- RelatonBib::Series.new title: title, abbreviation: abbrev
759
- end
760
- end
761
-
762
- #
763
- # Parse a medium from the source hash.
764
- #
765
- # @return [RelatonBib::Mediub, nil] The medium.
766
- #
767
- def parse_medium
768
- genre = @src["degree"]&.first
769
- return unless genre
770
-
771
- RelatonBib::Medium.new genre: genre
772
- end
773
-
774
- #
775
- # Fetch data from Crossref API with retry logic.
776
- #
777
- # @param [String] query The query string.
778
- # @param [String] filter The filter string.
779
- #
780
- # @return [Array<Hash>, nil] Items array from response or nil for 4xx responses.
781
- #
782
- # @raise [RelatonBib::RequestError] If request fails after retries.
783
- #
784
- def fetch_crossref(query:, filter:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
785
- url = format(CROSSREF_API_URL, query: query, filter: filter)
786
- retries = 0
787
- begin
788
- resp = Faraday.get url
789
- case resp.status
790
- when 200..299
791
- JSON.parse(resp.body).dig("message", "items")
792
- when 400..499
793
- nil
794
- else
795
- raise RelatonBib::RequestError, "Crossref request failed: #{resp.status} #{resp.body}"
796
- end
797
- rescue Faraday::Error => e
798
- retries += 1
799
- retry if retries <= MAX_RETRIES
800
- raise RelatonBib::RequestError, "Crossref network error after #{MAX_RETRIES} retries: #{e.message}"
801
- rescue JSON::ParserError => e
802
- raise RelatonBib::RequestError, "Crossref JSON parsing error: #{e.message}"
803
- end
804
- end
805
- end
806
- end