relaton-doi 1.14.2 → 1.14.4

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