serrano 0.3.6 → 0.6.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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +28 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +50 -0
- data/.rubocop_todo.yml +105 -0
- data/CHANGELOG.md +33 -0
- data/{CONDUCT.md → CODE_OF_CONDUCT.md} +2 -2
- data/Gemfile +2 -0
- data/Gemfile.lock +56 -38
- data/LICENSE +1 -1
- data/README.md +35 -7
- data/Rakefile +26 -18
- data/bin/serrano +96 -98
- data/lib/serrano.rb +194 -77
- data/lib/serrano/cn.rb +7 -9
- data/lib/serrano/cnrequest.rb +42 -28
- data/lib/serrano/error.rb +2 -0
- data/lib/serrano/faraday.rb +15 -14
- data/lib/serrano/filterhandler.rb +13 -15
- data/lib/serrano/filters.rb +91 -78
- data/lib/serrano/helpers/configuration.rb +2 -2
- data/lib/serrano/request.rb +46 -39
- data/lib/serrano/request_cursor.rb +66 -59
- data/lib/serrano/styles.rb +14 -9
- data/lib/serrano/utils.rb +21 -14
- data/lib/serrano/version.rb +3 -1
- data/serrano.gemspec +33 -20
- metadata +87 -69
- data/.travis.yml +0 -8
- data/lib/serrano/constants.rb +0 -36
- data/lib/serrano/cursor_testing.rb +0 -52
data/Rakefile
CHANGED
@@ -1,41 +1,49 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
2
4
|
require 'rake/testtask'
|
5
|
+
require 'rubocop/rake_task'
|
3
6
|
|
4
7
|
Rake::TestTask.new do |t|
|
5
|
-
t.libs <<
|
6
|
-
t.test_files = FileList['test/
|
8
|
+
t.libs << 'test'
|
9
|
+
t.test_files = FileList['test/test_*.rb']
|
7
10
|
t.verbose = true
|
11
|
+
t.warning = false
|
8
12
|
end
|
9
13
|
|
10
|
-
desc
|
11
|
-
task :
|
14
|
+
desc 'Run tests'
|
15
|
+
task default: :test
|
16
|
+
|
17
|
+
RuboCop::RakeTask.new(:rubocop) do |t|
|
18
|
+
t.options = ['--display-cop-names']
|
19
|
+
end
|
12
20
|
|
13
|
-
desc
|
21
|
+
desc 'Build serrano docs'
|
14
22
|
task :docs do
|
15
|
-
|
23
|
+
system 'yardoc'
|
16
24
|
end
|
17
25
|
|
18
|
-
desc
|
26
|
+
desc 'bundle install'
|
19
27
|
task :bundle do
|
20
|
-
system
|
28
|
+
system 'bundle install'
|
21
29
|
end
|
22
30
|
|
23
|
-
desc
|
31
|
+
desc 'clean out builds'
|
24
32
|
task :clean do
|
25
|
-
system
|
33
|
+
system 'ls | grep [0-9].gem | xargs rm'
|
26
34
|
end
|
27
35
|
|
28
|
-
desc
|
36
|
+
desc 'Build serrano'
|
29
37
|
task :build do
|
30
|
-
|
38
|
+
system 'gem build serrano.gemspec'
|
31
39
|
end
|
32
40
|
|
33
|
-
desc
|
34
|
-
task :
|
35
|
-
|
41
|
+
desc 'Install serrano'
|
42
|
+
task install: %i[bundle build] do
|
43
|
+
system "gem install serrano-#{Serrano::VERSION}.gem"
|
36
44
|
end
|
37
45
|
|
38
|
-
desc
|
39
|
-
task :
|
46
|
+
desc 'Release to Rubygems'
|
47
|
+
task release: :build do
|
40
48
|
system "gem push serrano-#{Serrano::VERSION}.gem"
|
41
49
|
end
|
data/bin/serrano
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require 'thor'
|
5
|
+
require 'serrano'
|
6
|
+
require 'multi_json'
|
6
7
|
|
7
8
|
class Sr < Thor
|
8
9
|
include Thor::Actions
|
9
10
|
# class_option :json, :type => :boolean, :default => false
|
10
11
|
|
11
|
-
desc
|
12
|
+
desc 'works [DOIs]', 'Get works by DOIs'
|
12
13
|
long_desc <<-LONGDESC
|
13
14
|
`serrano works` accepts one or more DOIs to search for works
|
14
15
|
|
@@ -47,17 +48,17 @@ class Sr < Thor
|
|
47
48
|
\x5"http://orcid.org/0000-0003-4087-8021"
|
48
49
|
\x5"http://orcid.org/0000-0002-2076-5452"
|
49
50
|
LONGDESC
|
50
|
-
option :json, :
|
51
|
-
option :filter, :
|
52
|
-
option :limit, :
|
53
|
-
def works(ids=nil)
|
54
|
-
if ids.nil?
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
51
|
+
option :json, type: :boolean, default: false
|
52
|
+
option :filter, type: :hash, default: nil
|
53
|
+
option :limit, type: :numeric, default: nil
|
54
|
+
def works(ids = nil)
|
55
|
+
out = if ids.nil?
|
56
|
+
Serrano.works(filter: options[:filter], limit: options[:limit])
|
57
|
+
else
|
58
|
+
Serrano.works(ids: ids.split(','), filter: options[:filter])
|
59
|
+
end
|
59
60
|
if !options[:json]
|
60
|
-
out = out.collect { |x| x['message'].select { |k,
|
61
|
+
out = out.collect { |x| x['message'].select { |k, _v| k[/DOI|type|title/] } }
|
61
62
|
out.each do |x|
|
62
63
|
puts 'DOI: ' + x['DOI'].to_s
|
63
64
|
puts 'type: ' + x['type']
|
@@ -69,7 +70,7 @@ class Sr < Thor
|
|
69
70
|
end
|
70
71
|
end
|
71
72
|
|
72
|
-
desc
|
73
|
+
desc 'members [member IDs]', 'Get members by id'
|
73
74
|
long_desc <<-LONGDESC
|
74
75
|
`serrano members` accepts one or more Crossref member IDs
|
75
76
|
|
@@ -96,26 +97,26 @@ class Sr < Thor
|
|
96
97
|
|
97
98
|
24 7 45 31 22 67 72 17 53 75 0 68 12 16 50 79 40 18 40 195
|
98
99
|
LONGDESC
|
99
|
-
option :json, :
|
100
|
-
option :query, :
|
101
|
-
option :limit, :
|
102
|
-
option :works, :
|
103
|
-
def members(ids=nil)
|
100
|
+
option :json, type: :boolean, default: false
|
101
|
+
option :query, type: :string, default: nil
|
102
|
+
option :limit, type: :numeric, default: nil
|
103
|
+
option :works, type: :boolean, default: false
|
104
|
+
def members(ids = nil)
|
104
105
|
if ids.nil?
|
105
106
|
out = Serrano.members(query: options[:query], limit: options[:limit],
|
106
|
-
|
107
|
+
works: options[:works])
|
107
108
|
else
|
108
|
-
out = Serrano.members(ids: ids.split(
|
109
|
-
|
109
|
+
out = Serrano.members(ids: ids.split(','), query: options[:query], limit: options[:limit],
|
110
|
+
works: options[:works])
|
110
111
|
end
|
111
112
|
|
112
113
|
if !options[:json]
|
113
114
|
if !options[:works]
|
114
|
-
if out.class == Hash
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
115
|
+
out = if out.class == Hash
|
116
|
+
out['message']['items']
|
117
|
+
else
|
118
|
+
out.collect { |x| x['message'] }
|
119
|
+
end
|
119
120
|
|
120
121
|
# out = out.collect { |x| x['message'].select { |k,v| k[/id|primary-name/] } }
|
121
122
|
out.each do |x|
|
@@ -131,8 +132,7 @@ class Sr < Thor
|
|
131
132
|
end
|
132
133
|
end
|
133
134
|
|
134
|
-
|
135
|
-
desc "prefixes [DOI prefixes]", "Search for prefixes by DOI prefix"
|
135
|
+
desc 'prefixes [DOI prefixes]', 'Search for prefixes by DOI prefix'
|
136
136
|
long_desc <<-LONGDESC
|
137
137
|
`serrano prefixes` accepts one or more Crossref member IDs
|
138
138
|
|
@@ -162,24 +162,24 @@ class Sr < Thor
|
|
162
162
|
\x5"Co-Action Publishing"
|
163
163
|
\x5"Oxford University Press (OUP)"
|
164
164
|
LONGDESC
|
165
|
-
option :json, :
|
166
|
-
option :limit, :
|
167
|
-
option :works, :
|
168
|
-
def prefixes(ids=nil)
|
169
|
-
if ids.nil?
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
165
|
+
option :json, type: :boolean, default: false
|
166
|
+
option :limit, type: :numeric, default: nil
|
167
|
+
option :works, type: :boolean, default: false
|
168
|
+
def prefixes(ids = nil)
|
169
|
+
out = if ids.nil?
|
170
|
+
Serrano.prefixes(limit: options[:limit], works: options[:works])
|
171
|
+
else
|
172
|
+
Serrano.prefixes(ids: ids.split(','), limit: options[:limit],
|
173
|
+
works: options[:works])
|
174
|
+
end
|
175
175
|
|
176
176
|
if !options[:json]
|
177
177
|
if !options[:works]
|
178
|
-
if out.class == Hash
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
178
|
+
out = if out.class == Hash
|
179
|
+
out['message']['items']
|
180
|
+
else
|
181
|
+
out.collect { |x| x['message'] }
|
182
|
+
end
|
183
183
|
|
184
184
|
out.each do |x|
|
185
185
|
puts 'member: ' + x['member']
|
@@ -195,7 +195,7 @@ class Sr < Thor
|
|
195
195
|
end
|
196
196
|
end
|
197
197
|
|
198
|
-
desc
|
198
|
+
desc 'funders [funder IDs]', 'Search for funders by DOI prefix'
|
199
199
|
long_desc <<-LONGDESC
|
200
200
|
`serrano funders` accepts one or more Crossref funder IDs
|
201
201
|
|
@@ -226,26 +226,26 @@ class Sr < Thor
|
|
226
226
|
|
227
227
|
$ serrano funders 10.13039/100000001 --works=true --limit=2
|
228
228
|
LONGDESC
|
229
|
-
option :json, :
|
230
|
-
option :query, :
|
231
|
-
option :limit, :
|
232
|
-
option :works, :
|
233
|
-
def funders(ids=nil)
|
229
|
+
option :json, type: :boolean, default: false
|
230
|
+
option :query, type: :string, default: nil
|
231
|
+
option :limit, type: :numeric, default: nil
|
232
|
+
option :works, type: :boolean, default: false
|
233
|
+
def funders(ids = nil)
|
234
234
|
if ids.nil?
|
235
235
|
out = Serrano.funders(query: options[:query], limit: options[:limit],
|
236
|
-
|
236
|
+
works: options[:works])
|
237
237
|
else
|
238
|
-
out = Serrano.funders(ids: ids.split(
|
239
|
-
|
238
|
+
out = Serrano.funders(ids: ids.split(','), query: options[:query], limit: options[:limit],
|
239
|
+
works: options[:works])
|
240
240
|
end
|
241
241
|
|
242
242
|
if !options[:json]
|
243
243
|
if !options[:works]
|
244
|
-
if out.class == Hash
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
244
|
+
out = if out.class == Hash
|
245
|
+
out['message']['items']
|
246
|
+
else
|
247
|
+
out.collect { |x| x['message'] }
|
248
|
+
end
|
249
249
|
|
250
250
|
out.each do |x|
|
251
251
|
puts 'id: ' + x['id']
|
@@ -261,8 +261,7 @@ class Sr < Thor
|
|
261
261
|
end
|
262
262
|
end
|
263
263
|
|
264
|
-
|
265
|
-
desc "journals [journal ISSNs]", "Search for journals by ISSNs"
|
264
|
+
desc 'journals [journal ISSNs]', 'Search for journals by ISSNs'
|
266
265
|
long_desc <<-LONGDESC
|
267
266
|
`serrano journals` accepts one or more journal ISSNs
|
268
267
|
|
@@ -293,26 +292,26 @@ class Sr < Thor
|
|
293
292
|
|
294
293
|
$ serrano journals 2167-8359 --query=ecology --works=true --limit=2
|
295
294
|
LONGDESC
|
296
|
-
option :json, :
|
297
|
-
option :query, :
|
298
|
-
option :limit, :
|
299
|
-
option :works, :
|
300
|
-
def journals(ids=nil)
|
295
|
+
option :json, type: :boolean, default: false
|
296
|
+
option :query, type: :string, default: nil
|
297
|
+
option :limit, type: :numeric, default: nil
|
298
|
+
option :works, type: :boolean, default: false
|
299
|
+
def journals(ids = nil)
|
301
300
|
if ids.nil?
|
302
301
|
out = Serrano.journals(query: options[:query], limit: options[:limit],
|
303
|
-
|
302
|
+
works: options[:works])
|
304
303
|
else
|
305
|
-
out = Serrano.journals(ids: ids.split(
|
306
|
-
|
304
|
+
out = Serrano.journals(ids: ids.split(','), query: options[:query], limit: options[:limit],
|
305
|
+
works: options[:works])
|
307
306
|
end
|
308
307
|
|
309
308
|
if !options[:json]
|
310
309
|
if !options[:works]
|
311
|
-
if out.class == Hash
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
310
|
+
out = if out.class == Hash
|
311
|
+
out['message']['items']
|
312
|
+
else
|
313
|
+
out.collect { |x| x['message'] }
|
314
|
+
end
|
316
315
|
|
317
316
|
out.each do |x|
|
318
317
|
puts 'ISSN: ' + x['ISSN'][0]
|
@@ -328,7 +327,7 @@ class Sr < Thor
|
|
328
327
|
end
|
329
328
|
end
|
330
329
|
|
331
|
-
desc
|
330
|
+
desc 'types [type name]', 'Search for types by name'
|
332
331
|
long_desc <<-LONGDESC
|
333
332
|
`serrano types` accepts one or more type names
|
334
333
|
|
@@ -359,20 +358,20 @@ class Sr < Thor
|
|
359
358
|
|
360
359
|
$ serrano types dissertation --works --limit=2
|
361
360
|
LONGDESC
|
362
|
-
option :json, :
|
363
|
-
option :works, :
|
364
|
-
option :limit, :
|
361
|
+
option :json, type: :boolean, default: false
|
362
|
+
option :works, type: :boolean, default: false
|
363
|
+
option :limit, type: :numeric, default: nil
|
365
364
|
def types(*ids)
|
366
365
|
out = Serrano.types(ids: ids, limit: options[:limit],
|
367
|
-
|
366
|
+
works: options[:works])
|
368
367
|
|
369
368
|
if !options[:json]
|
370
369
|
if !options[:works]
|
371
|
-
if out.class == Hash
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
370
|
+
out = if out.class == Hash
|
371
|
+
out['message']['items']
|
372
|
+
else
|
373
|
+
out.collect { |x| x['message'] }
|
374
|
+
end
|
376
375
|
|
377
376
|
out.each do |x|
|
378
377
|
puts 'id: ' + x['id']
|
@@ -387,7 +386,7 @@ class Sr < Thor
|
|
387
386
|
end
|
388
387
|
end
|
389
388
|
|
390
|
-
desc
|
389
|
+
desc 'licenses', 'Search for licenses by name'
|
391
390
|
long_desc <<-LONGDESC
|
392
391
|
`serrano licenses` accepts one or more type names
|
393
392
|
|
@@ -416,8 +415,8 @@ class Sr < Thor
|
|
416
415
|
"National Science Foundation"
|
417
416
|
\x5"U.S. Department of Energy"
|
418
417
|
LONGDESC
|
419
|
-
option :json, :
|
420
|
-
option :query, :
|
418
|
+
option :json, type: :boolean, default: false
|
419
|
+
option :query, type: :string, default: nil
|
421
420
|
def licenses
|
422
421
|
out = Serrano.licenses(query: options[:query])
|
423
422
|
if !options[:json]
|
@@ -432,7 +431,7 @@ class Sr < Thor
|
|
432
431
|
end
|
433
432
|
end
|
434
433
|
|
435
|
-
desc
|
434
|
+
desc 'contneg', 'Content negotiation'
|
436
435
|
long_desc <<-LONGDESC
|
437
436
|
`serrano contneg` accepts a DOI
|
438
437
|
|
@@ -444,15 +443,15 @@ class Sr < Thor
|
|
444
443
|
|
445
444
|
Murtaugh PA (2014). In defense of P values . Ecology 95: 611–617.
|
446
445
|
LONGDESC
|
447
|
-
option :format, :
|
448
|
-
option :style, :
|
449
|
-
option :locale, :
|
446
|
+
option :format, type: :string, default: 'text'
|
447
|
+
option :style, type: :string, default: 'apa'
|
448
|
+
option :locale, type: :string, default: 'en-US'
|
450
449
|
def contneg(ids)
|
451
450
|
puts Serrano.content_negotiation(ids: ids, format: options[:format], style:
|
452
451
|
options[:style], locale: options[:locale])
|
453
452
|
end
|
454
453
|
|
455
|
-
desc
|
454
|
+
desc 'version', 'Get serrano version'
|
456
455
|
def version
|
457
456
|
puts Serrano::VERSION
|
458
457
|
end
|
@@ -460,11 +459,11 @@ class Sr < Thor
|
|
460
459
|
private
|
461
460
|
|
462
461
|
def print_works(data)
|
463
|
-
if data.is_a? Array
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
462
|
+
data = if data.is_a? Array
|
463
|
+
data[0]['message']['items']
|
464
|
+
else
|
465
|
+
data.collect { |x| x['message'].select { |k, _v| k[/DOI|type|title/] } }
|
466
|
+
end
|
468
467
|
data.each do |x|
|
469
468
|
puts 'DOI: ' + x['DOI']
|
470
469
|
puts 'type: ' + x['type']
|
@@ -472,7 +471,6 @@ class Sr < Thor
|
|
472
471
|
puts
|
473
472
|
end
|
474
473
|
end
|
475
|
-
|
476
474
|
end
|
477
475
|
|
478
476
|
Sr.start(ARGV)
|
data/lib/serrano.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
require 'serrano/version'
|
5
|
+
require 'serrano/request'
|
6
|
+
require 'serrano/request_cursor'
|
7
|
+
require 'serrano/filterhandler'
|
8
|
+
require 'serrano/cnrequest'
|
9
|
+
require 'serrano/filters'
|
10
|
+
require 'serrano/styles'
|
8
11
|
|
9
12
|
require 'rexml/document'
|
10
13
|
require 'rexml/xpath'
|
@@ -18,13 +21,21 @@ require 'rexml/xpath'
|
|
18
21
|
# Max: 100.
|
19
22
|
# @param sort [String] Field to sort on, one of score, relevance,
|
20
23
|
# updated (date of most recent change to metadata - currently the same as deposited),
|
21
|
-
# deposited (time of most recent deposit), indexed (time of most recent index),
|
22
|
-
# published (publication date)
|
24
|
+
# deposited (time of most recent deposit), indexed (time of most recent index),
|
25
|
+
# published (publication date), published-print (print publication date),
|
26
|
+
# published-online (online publication date), issued (issued date (earliest known publication date)),
|
27
|
+
# is-referenced-by-count (number of times this DOI is referenced by other Crossref DOIs), or
|
28
|
+
# references-count (number of references included in the references section of the document
|
29
|
+
# identified by this DOI). Note: If the API call includes a query, then the sort
|
23
30
|
# order will be by the relevance score. If no query is included, then the sort order
|
24
31
|
# will be by DOI update date.
|
25
32
|
# @param order [String] Sort order, one of 'asc' or 'desc'
|
26
33
|
# @param facet [Boolean/String] Include facet results OR a query (e.g., `license:*`) to facet by
|
27
34
|
# license. Default: false
|
35
|
+
# @param select [String/Array(String)] Crossref metadata records can be
|
36
|
+
# quite large. Sometimes you just want a few elements from the schema. You can "select"
|
37
|
+
# a subset of elements to return. This can make your API calls much more efficient. Not
|
38
|
+
# clear yet which fields are allowed here.
|
28
39
|
# @param verbose [Boolean] Print request headers to stdout. Default: false
|
29
40
|
|
30
41
|
# @!macro cursor_params
|
@@ -52,10 +63,18 @@ require 'rexml/xpath'
|
|
52
63
|
# - oauth [Hash] A hash with OAuth details
|
53
64
|
|
54
65
|
# @!macro field_queries
|
55
|
-
# @param [Hash<Object>] args Field queries, as named parameters.
|
56
|
-
#
|
66
|
+
# @param [Hash<Object>] args Field queries, as named parameters. See
|
67
|
+
# https://github.com/CrossRef/rest-api-doc/blob/master/rest_api.md#field-queries
|
57
68
|
# Field query parameters mut be named, and must start with `query_`. Any dashes or
|
58
|
-
# periods should be replaced with underscores.
|
69
|
+
# periods should be replaced with underscores. The options include:
|
70
|
+
# - query_container_title: Query container-title aka. publication name
|
71
|
+
# - query_author: Query author given and family names
|
72
|
+
# - query_editor: Query editor given and family names
|
73
|
+
# - query_chair: Query chair given and family names
|
74
|
+
# - query_translator: Query translator given and family names
|
75
|
+
# - query_contributor: Query author, editor, chair and translator given and family names
|
76
|
+
# - query_bibliographic: Query bibliographic information, useful for citation look up. Includes titles, authors, ISSNs and publication years
|
77
|
+
# - query_affiliation: Query contributor affiliations
|
59
78
|
|
60
79
|
##
|
61
80
|
# Serrano - The top level module for using methods
|
@@ -91,7 +110,29 @@ require 'rexml/xpath'
|
|
91
110
|
# searching full text, or even abstracts of articles, but only what is
|
92
111
|
# available in the data that is returned to you. That is, they search
|
93
112
|
# article titles, authors, etc. For some discussion on this, see
|
94
|
-
# https://
|
113
|
+
# https://gitlab.com/crossref/issues/issues/101
|
114
|
+
#
|
115
|
+
#
|
116
|
+
# The Polite Pool
|
117
|
+
# As of September 18th 2017 any API queries that use HTTPS and have
|
118
|
+
# appropriate contact information will be directed to a special pool
|
119
|
+
# of API machines that are reserved for polite users. If you connect
|
120
|
+
# to the Crossreef API using HTTPS and provide contact
|
121
|
+
# information, then they will send you to a separate pool of machines,
|
122
|
+
# with better control the performance of these machines because they can
|
123
|
+
# block abusive users.
|
124
|
+
#
|
125
|
+
# We have been using `https` in `serrano` for a while now, so that's good
|
126
|
+
# to go. To get into the Polite Pool, also set your `mailto` email address
|
127
|
+
# with `Serrano.configuration` (example below), or set as an environment
|
128
|
+
# variable with the name `CROSSREF_EMAIL` and your mailto will be set
|
129
|
+
# for each request automatically.
|
130
|
+
#
|
131
|
+
# require 'serrano'
|
132
|
+
# Serrano.configuration do |config|
|
133
|
+
# config.mailto = "foo@bar.com"
|
134
|
+
# end
|
135
|
+
#
|
95
136
|
#
|
96
137
|
# Rate limiting
|
97
138
|
# Crossref introduced rate limiting recently. The rate limits apparently vary,
|
@@ -99,13 +140,20 @@ require 'rexml/xpath'
|
|
99
140
|
# limit is 50 requests per second. Look for the headers `X-Rate-Limit-Limit`
|
100
141
|
# and `X-Rate-Limit-Interval` in requests to see what the current rate
|
101
142
|
# limits are.
|
143
|
+
#
|
144
|
+
#
|
145
|
+
# URL Encoding
|
146
|
+
# We do URL encoding of DOIs for you for all methods except `Serrano.citation_count`
|
147
|
+
# which doesn't work if you encode DOIs beforehand. We use `ERB::Util.url_encode`
|
148
|
+
# to encode.
|
102
149
|
|
103
150
|
module Serrano
|
104
151
|
extend Configuration
|
105
152
|
|
106
153
|
define_setting :access_token
|
107
154
|
define_setting :access_secret
|
108
|
-
define_setting :
|
155
|
+
define_setting :mailto, ENV['CROSSREF_EMAIL']
|
156
|
+
define_setting :base_url, 'https://api.crossref.org/'
|
109
157
|
|
110
158
|
##
|
111
159
|
# Search the works route
|
@@ -154,8 +202,12 @@ module Serrano
|
|
154
202
|
# # sample
|
155
203
|
# Serrano.works(sample: 2)
|
156
204
|
#
|
205
|
+
# # select - pass an array or a comma separated string
|
206
|
+
# Serrano.works(query: "ecology", select: "DOI,title", limit: 30)
|
207
|
+
# Serrano.works(query: "ecology", select: ["DOI","title"], limit: 30)
|
208
|
+
#
|
157
209
|
# # cursor for deep paging
|
158
|
-
# Serrano.works(query: "widget", cursor: "*", limit: 100)
|
210
|
+
# Serrano.works(query: "widget", cursor: "*", limit: 100, cursor_max: 1000)
|
159
211
|
# # another query, more results this time
|
160
212
|
# res = Serrano.works(query: "science", cursor: "*", limit: 250, cursor_max: 1000);
|
161
213
|
# res.collect { |x| x['message']['items'].length }.reduce(0, :+)
|
@@ -173,13 +225,18 @@ module Serrano
|
|
173
225
|
# ## query.container-title
|
174
226
|
# res = Serrano.works(query: "ecology", query_container_title: 'Ecology')
|
175
227
|
# res['message']['items'].collect { |x| x['container-title'] }
|
228
|
+
#
|
229
|
+
# # select certain fields
|
230
|
+
# Serrano.works(select: ['DOI', 'title'], limit: 3)
|
176
231
|
def self.works(ids: nil, query: nil, filter: nil, offset: nil,
|
177
|
-
|
178
|
-
|
232
|
+
limit: nil, sample: nil, sort: nil, order: nil, facet: nil,
|
233
|
+
select: nil, options: nil, verbose: false, cursor: nil,
|
234
|
+
cursor_max: 5000, **args)
|
179
235
|
|
236
|
+
assert_valid_filters(filter) if filter
|
180
237
|
RequestCursor.new('works', ids, query, filter, offset,
|
181
|
-
|
182
|
-
|
238
|
+
limit, sample, sort, order, facet, select, nil, nil, options,
|
239
|
+
verbose, cursor, cursor_max, args).perform
|
183
240
|
end
|
184
241
|
|
185
242
|
##
|
@@ -225,14 +282,18 @@ module Serrano
|
|
225
282
|
# ## query.title
|
226
283
|
# res = Serrano.members(ids: 221, works: true, query_container_title: 'Advances')
|
227
284
|
# res[0]['message']['items'].collect { |x| x['container-title'] }
|
285
|
+
#
|
286
|
+
# # select certain fields
|
287
|
+
# Serrano.members(ids: 340, works: true, select: ['DOI', 'title'], limit: 3)
|
228
288
|
def self.members(ids: nil, query: nil, filter: nil, offset: nil,
|
229
|
-
|
230
|
-
|
231
|
-
|
289
|
+
limit: nil, sample: nil, sort: nil, order: nil, facet: nil,
|
290
|
+
select: nil, works: false, options: nil, verbose: false,
|
291
|
+
cursor: nil, cursor_max: 5000, **args)
|
232
292
|
|
293
|
+
assert_valid_filters(filter) if filter
|
233
294
|
RequestCursor.new('members', ids, query, filter, offset,
|
234
|
-
|
235
|
-
|
295
|
+
limit, sample, sort, order, facet, select, works, nil,
|
296
|
+
options, verbose, cursor, cursor_max, args).perform
|
236
297
|
end
|
237
298
|
|
238
299
|
##
|
@@ -266,17 +327,21 @@ module Serrano
|
|
266
327
|
# items.collect{ |z| z['DOI'] }[0,50]
|
267
328
|
#
|
268
329
|
# # field queries
|
269
|
-
# ## query.
|
270
|
-
# res = Serrano.prefixes(ids: "10.1016", works: true,
|
330
|
+
# ## query.bibliographic
|
331
|
+
# res = Serrano.prefixes(ids: "10.1016", works: true, query_bibliographic: 'cell biology')
|
271
332
|
# res[0]['message']['items'].collect { |x| x['title'] }
|
333
|
+
#
|
334
|
+
# # select certain fields
|
335
|
+
# Serrano.prefixes(ids: "10.1016", works: true, select: ['DOI', 'title'], limit: 3)
|
272
336
|
def self.prefixes(ids:, filter: nil, offset: nil,
|
273
|
-
|
274
|
-
|
275
|
-
|
337
|
+
limit: nil, sample: nil, sort: nil, order: nil, facet: nil,
|
338
|
+
select: nil, works: false, options: nil, verbose: false,
|
339
|
+
cursor: nil, cursor_max: 5000, **args)
|
276
340
|
|
341
|
+
assert_valid_filters(filter) if filter
|
277
342
|
RequestCursor.new('prefixes', ids, nil, filter, offset,
|
278
|
-
|
279
|
-
|
343
|
+
limit, sample, sort, order, facet, select, works, nil,
|
344
|
+
options, verbose, cursor, cursor_max, args).perform
|
280
345
|
end
|
281
346
|
|
282
347
|
##
|
@@ -318,14 +383,18 @@ module Serrano
|
|
318
383
|
# ## query.title
|
319
384
|
# res = Serrano.funders(ids: "10.13039/100000001", works: true, query_author: 'Simon')
|
320
385
|
# res[0]['message']['items'].collect { |x| x['author'][0]['family'] }
|
386
|
+
#
|
387
|
+
# # select certain fields
|
388
|
+
# Serrano.funders(ids: "10.13039/100000001", works: true, select: ['DOI', 'title'], limit: 3)
|
321
389
|
def self.funders(ids: nil, query: nil, filter: nil, offset: nil,
|
322
|
-
|
323
|
-
|
324
|
-
|
390
|
+
limit: nil, sample: nil, sort: nil, order: nil, facet: nil,
|
391
|
+
select: nil, works: false, options: nil, verbose: false,
|
392
|
+
cursor: nil, cursor_max: 5000, **args)
|
325
393
|
|
394
|
+
assert_valid_filters(filter) if filter
|
326
395
|
RequestCursor.new('funders', ids, query, filter, offset,
|
327
|
-
|
328
|
-
|
396
|
+
limit, sample, sort, order, facet, select, works, nil, options,
|
397
|
+
verbose, cursor, cursor_max, args).perform
|
329
398
|
end
|
330
399
|
|
331
400
|
##
|
@@ -367,14 +436,18 @@ module Serrano
|
|
367
436
|
# ## query.title
|
368
437
|
# res = Serrano.journals(ids: "2167-8359", works: true, query_container_title: 'Advances')
|
369
438
|
# res[0]['message']['items'].collect { |x| x['container-title'] }
|
439
|
+
#
|
440
|
+
# # select certain fields
|
441
|
+
# Serrano.journals(ids: "2167-8359", works: true, select: ['DOI', 'title'], limit: 3)
|
370
442
|
def self.journals(ids: nil, query: nil, filter: nil, offset: nil,
|
371
|
-
|
372
|
-
|
373
|
-
|
443
|
+
limit: nil, sample: nil, sort: nil, order: nil, facet: nil,
|
444
|
+
select: nil, works: false, options: nil, verbose: false,
|
445
|
+
cursor: nil, cursor_max: 5000, **args)
|
374
446
|
|
447
|
+
assert_valid_filters(filter) if filter
|
375
448
|
RequestCursor.new('journals', ids, query, filter, offset,
|
376
|
-
|
377
|
-
|
449
|
+
limit, sample, sort, order, facet, select, works, nil, options,
|
450
|
+
verbose, cursor, cursor_max, args).perform
|
378
451
|
end
|
379
452
|
|
380
453
|
##
|
@@ -385,6 +458,10 @@ module Serrano
|
|
385
458
|
# @!macro field_queries
|
386
459
|
# @param ids [Array] DOIs (digital object identifier) or other identifiers
|
387
460
|
# @param works [Boolean] If true, works returned as well. Default: false
|
461
|
+
# @param select [String/Array(String)] Crossref metadata records can be
|
462
|
+
# quite large. Sometimes you just want a few elements from the schema. You can "select"
|
463
|
+
# a subset of elements to return. This can make your API calls much more efficient. Not
|
464
|
+
# clear yet which fields are allowed here.
|
388
465
|
# @return [Array] An array of hashes
|
389
466
|
#
|
390
467
|
# @example
|
@@ -404,20 +481,42 @@ module Serrano
|
|
404
481
|
# ## query.title
|
405
482
|
# res = Serrano.types(ids: "journal", works: true, query_container_title: 'Advances')
|
406
483
|
# res[0]['message']['items'].collect { |x| x['container-title'] }
|
407
|
-
|
408
|
-
|
484
|
+
#
|
485
|
+
# # select certain fields
|
486
|
+
# Serrano.types(ids: "journal", works: true, select: ['DOI', 'title'], limit: 3)
|
487
|
+
def self.types(ids: nil, offset: nil, limit: nil, select: nil, works: false,
|
488
|
+
options: nil, verbose: false, cursor: nil, cursor_max: 5000, **args)
|
409
489
|
|
410
490
|
RequestCursor.new('types', ids, nil, nil, offset,
|
411
|
-
|
412
|
-
|
491
|
+
limit, nil, nil, nil, nil, select, works, nil, options,
|
492
|
+
verbose, cursor, cursor_max, args).perform
|
413
493
|
end
|
414
494
|
|
415
495
|
##
|
416
496
|
# Search the licenses route
|
417
497
|
#
|
418
|
-
# @!macro serrano_params
|
419
498
|
# @!macro serrano_options
|
420
499
|
# @param query [String] A query string
|
500
|
+
# @param offset [Fixnum] Number of record to start at, any non-negative integer up to 10,000
|
501
|
+
# @param limit [Fixnum] Number of results to return. Not relavant when searching with specific dois.
|
502
|
+
# Default: 20. Max: 1000
|
503
|
+
# @param sample [Fixnum] Number of random results to return. when you use the sample parameter,
|
504
|
+
# the limit and offset parameters are ignored. This parameter only used when works requested.
|
505
|
+
# Max: 100.
|
506
|
+
# @param sort [String] Field to sort on, one of score, relevance,
|
507
|
+
# updated (date of most recent change to metadata - currently the same as deposited),
|
508
|
+
# deposited (time of most recent deposit), indexed (time of most recent index),
|
509
|
+
# published (publication date), published-print (print publication date),
|
510
|
+
# published-online (online publication date), issued (issued date (earliest known publication date)),
|
511
|
+
# is-referenced-by-count (number of times this DOI is referenced by other Crossref DOIs), or
|
512
|
+
# references-count (number of references included in the references section of the document
|
513
|
+
# identified by this DOI). Note: If the API call includes a query, then the sort
|
514
|
+
# order will be by the relevance score. If no query is included, then the sort order
|
515
|
+
# will be by DOI update date.
|
516
|
+
# @param order [String] Sort order, one of 'asc' or 'desc'
|
517
|
+
# @param facet [Boolean/String] Include facet results OR a query (e.g., `license:*`) to facet by
|
518
|
+
# license. Default: false
|
519
|
+
# @param verbose [Boolean] Print request headers to stdout. Default: false
|
421
520
|
# @return [Array] An array of hashes
|
422
521
|
#
|
423
522
|
# @example
|
@@ -426,11 +525,11 @@ module Serrano
|
|
426
525
|
# Serrano.licenses()
|
427
526
|
# Serrano.licenses(limit: 3)
|
428
527
|
def self.licenses(query: nil, offset: nil,
|
429
|
-
|
430
|
-
|
528
|
+
limit: nil, sample: nil, sort: nil, order: nil,
|
529
|
+
facet: nil, options: nil, verbose: false)
|
431
530
|
|
432
531
|
Request.new('licenses', nil, query, nil, offset,
|
433
|
-
|
532
|
+
limit, sample, sort, order, facet, nil, nil, nil, options, verbose).perform
|
434
533
|
end
|
435
534
|
|
436
535
|
##
|
@@ -445,9 +544,8 @@ module Serrano
|
|
445
544
|
# Serrano.registration_agency(ids: '10.1371/journal.pone.0033693')
|
446
545
|
# Serrano.registration_agency(ids: ['10.1007/12080.1874-1746','10.1007/10452.1573-5125', '10.1111/(issn)1442-9993'])
|
447
546
|
def self.registration_agency(ids:, options: nil, verbose: false)
|
448
|
-
|
449
547
|
Request.new('works', ids, nil, nil, nil,
|
450
|
-
|
548
|
+
nil, nil, nil, nil, nil, nil, false, true, options, verbose).perform
|
451
549
|
end
|
452
550
|
|
453
551
|
##
|
@@ -468,9 +566,8 @@ module Serrano
|
|
468
566
|
# Serrano.random_dois(sample: 10)
|
469
567
|
# Serrano.random_dois(sample: 100)
|
470
568
|
def self.random_dois(sample: 10, options: nil, verbose: false)
|
471
|
-
|
472
569
|
tmp = Request.new('works', nil, nil, nil, nil,
|
473
|
-
|
570
|
+
nil, sample, nil, nil, nil, nil, false, nil, options, verbose).perform
|
474
571
|
tmp['message']['items'].collect { |x| x['DOI'] }
|
475
572
|
end
|
476
573
|
|
@@ -497,7 +594,7 @@ module Serrano
|
|
497
594
|
# Serrano.content_negotiation(ids: "10.1126/science.169.3946.635", format: "crossref-xml")
|
498
595
|
# Serrano.content_negotiation(ids: "10.1126/science.169.3946.635", format: "text")
|
499
596
|
#
|
500
|
-
# # return
|
597
|
+
# # return a bibentry type
|
501
598
|
# Serrano.content_negotiation(ids: "10.1126/science.169.3946.635", format: "bibentry")
|
502
599
|
# Serrano.content_negotiation(ids: "10.6084/m9.figshare.97218", format: "bibentry")
|
503
600
|
#
|
@@ -510,30 +607,32 @@ module Serrano
|
|
510
607
|
# Serrano.content_negotiation(ids: "10.1126/science.169.3946.635", format: "text", style: "oikos")
|
511
608
|
#
|
512
609
|
# # example with many DOIs
|
513
|
-
# dois =
|
514
|
-
# Serrano.content_negotiation(dois, format: "text", style: "apa")
|
610
|
+
# dois = Serrano.random_dois(sample: 2)
|
611
|
+
# Serrano.content_negotiation(ids: dois, format: "text", style: "apa")
|
515
612
|
#
|
516
613
|
# # Using DataCite DOIs
|
517
|
-
#
|
518
|
-
#
|
519
|
-
#
|
520
|
-
#
|
614
|
+
# doi = "10.1126/science.169.3946.635"
|
615
|
+
# Serrano.content_negotiation(ids: doi, format: "text")
|
616
|
+
# Serrano.content_negotiation(ids: doi, format: "crossref-xml")
|
617
|
+
# Serrano.content_negotiation(ids: doi, format: "crossref-tdm")
|
521
618
|
#
|
522
619
|
# ## But most do work
|
523
|
-
# Serrano.content_negotiation(ids:
|
524
|
-
# Serrano.content_negotiation(ids:
|
525
|
-
# Serrano.content_negotiation(ids:
|
526
|
-
# Serrano.content_negotiation(ids:
|
527
|
-
# Serrano.content_negotiation(ids:
|
528
|
-
# Serrano.content_negotiation(ids:
|
529
|
-
# Serrano.content_negotiation(ids:
|
530
|
-
# Serrano.content_negotiation(ids:
|
620
|
+
# Serrano.content_negotiation(ids: doi, format: "datacite-xml")
|
621
|
+
# Serrano.content_negotiation(ids: doi, format: "rdf-xml")
|
622
|
+
# Serrano.content_negotiation(ids: doi, format: "turtle")
|
623
|
+
# Serrano.content_negotiation(ids: doi, format: "citeproc-json")
|
624
|
+
# Serrano.content_negotiation(ids: doi, format: "ris")
|
625
|
+
# Serrano.content_negotiation(ids: doi, format: "bibtex")
|
626
|
+
# Serrano.content_negotiation(ids: doi, format: "bibentry")
|
627
|
+
# Serrano.content_negotiation(ids: doi, format: "bibtex")
|
531
628
|
#
|
532
629
|
# # many DOIs
|
533
|
-
# dois = ['10.5167/UZH-30455','10.5167/UZH-49216','10.5167/UZH-503',
|
630
|
+
# dois = ['10.5167/UZH-30455','10.5167/UZH-49216','10.5167/UZH-503',
|
631
|
+
# '10.5167/UZH-38402','10.5167/UZH-41217']
|
534
632
|
# x = Serrano.content_negotiation(ids: dois)
|
535
633
|
# puts x
|
536
|
-
def self.content_negotiation(ids:, format:
|
634
|
+
def self.content_negotiation(ids:, format: 'bibtex', style: 'apa', locale: 'en-US')
|
635
|
+
ids = Array(ids).map { |x| ERB::Util.url_encode(x) }
|
537
636
|
CNRequest.new(ids, format, style, locale).perform
|
538
637
|
end
|
539
638
|
|
@@ -552,17 +651,17 @@ module Serrano
|
|
552
651
|
# Serrano.citation_count(doi: "10.1016/j.fbr.2012.01.001")
|
553
652
|
# # DOI not found
|
554
653
|
# Serrano.citation_count(doi: "10.1016/j.fbr.2012")
|
555
|
-
def self.citation_count(doi:, url:
|
556
|
-
|
654
|
+
def self.citation_count(doi:, url: 'https://www.crossref.org/openurl/',
|
655
|
+
key: 'cboettig@ropensci.org', options: nil)
|
557
656
|
|
558
|
-
args = { id:
|
559
|
-
opts = args.delete_if { |
|
560
|
-
conn = Faraday.new(:
|
657
|
+
args = { id: 'doi:' + doi, pid: key, noredirect: true }
|
658
|
+
opts = args.delete_if { |_k, v| v.nil? }
|
659
|
+
conn = Faraday.new(url: url, request: options)
|
561
660
|
res = conn.get '', opts
|
562
661
|
x = res.body
|
563
662
|
oc = REXML::Document.new("<doc>#{x}</doc>")
|
564
663
|
value = REXML::XPath.first(oc, '//query').attributes['fl_count'].to_i
|
565
|
-
|
664
|
+
value
|
566
665
|
end
|
567
666
|
|
568
667
|
# Get csl styles
|
@@ -572,7 +671,25 @@ module Serrano
|
|
572
671
|
# @example
|
573
672
|
# Serrano.csl_styles
|
574
673
|
def self.csl_styles
|
575
|
-
|
674
|
+
fetch_styles
|
576
675
|
end
|
577
676
|
|
677
|
+
def self.assert_valid_filters(filters)
|
678
|
+
unless filters.is_a? Hash
|
679
|
+
raise ArgumentError, <<~ERR
|
680
|
+
Filters must be provided as a hash, like:
|
681
|
+
|
682
|
+
Serrano.works(query: "something", filters: { has_abstract: true })
|
683
|
+
ERR
|
684
|
+
end
|
685
|
+
|
686
|
+
filters.each do |name, _|
|
687
|
+
filter_strings = Filters.names.map(&:to_s)
|
688
|
+
next if filter_strings.include?(name.to_s)
|
689
|
+
|
690
|
+
raise ArgumentError, <<~ERR
|
691
|
+
The filter "#{name}" is not a valid filter. Please run `Serrano.filters.filters` to see all valid filters.
|
692
|
+
ERR
|
693
|
+
end
|
694
|
+
end
|
578
695
|
end
|