relaton-doi 1.14.2 → 1.14.4

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.
@@ -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