aspire 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +59 -0
  3. data/.rbenv-gemsets +1 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Dockerfile +20 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +851 -0
  10. data/Rakefile +10 -0
  11. data/aspire.gemspec +40 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/entrypoint.sh +11 -0
  15. data/exe/build-cache +13 -0
  16. data/lib/aspire.rb +11 -0
  17. data/lib/aspire/api.rb +2 -0
  18. data/lib/aspire/api/base.rb +198 -0
  19. data/lib/aspire/api/json.rb +195 -0
  20. data/lib/aspire/api/linked_data.rb +214 -0
  21. data/lib/aspire/caching.rb +4 -0
  22. data/lib/aspire/caching/builder.rb +356 -0
  23. data/lib/aspire/caching/cache.rb +365 -0
  24. data/lib/aspire/caching/cache_entry.rb +296 -0
  25. data/lib/aspire/caching/cache_logger.rb +63 -0
  26. data/lib/aspire/caching/util.rb +210 -0
  27. data/lib/aspire/cli/cache_builder.rb +123 -0
  28. data/lib/aspire/cli/command.rb +20 -0
  29. data/lib/aspire/enumerator/base.rb +29 -0
  30. data/lib/aspire/enumerator/json_enumerator.rb +130 -0
  31. data/lib/aspire/enumerator/linked_data_uri_enumerator.rb +32 -0
  32. data/lib/aspire/enumerator/report_enumerator.rb +64 -0
  33. data/lib/aspire/exceptions.rb +36 -0
  34. data/lib/aspire/object.rb +7 -0
  35. data/lib/aspire/object/base.rb +155 -0
  36. data/lib/aspire/object/digitisation.rb +43 -0
  37. data/lib/aspire/object/factory.rb +87 -0
  38. data/lib/aspire/object/list.rb +590 -0
  39. data/lib/aspire/object/module.rb +36 -0
  40. data/lib/aspire/object/resource.rb +371 -0
  41. data/lib/aspire/object/time_period.rb +47 -0
  42. data/lib/aspire/object/user.rb +46 -0
  43. data/lib/aspire/properties.rb +20 -0
  44. data/lib/aspire/user_lookup.rb +103 -0
  45. data/lib/aspire/util.rb +185 -0
  46. data/lib/aspire/version.rb +3 -0
  47. data/lib/retry.rb +197 -0
  48. metadata +274 -0
@@ -0,0 +1,36 @@
1
+ require 'aspire/object/base'
2
+ require 'aspire/properties'
3
+
4
+ module Aspire
5
+ module Object
6
+ # Represents a module in the Aspire API
7
+ class Module < Base
8
+ include Aspire::Properties
9
+
10
+ # @!attribute [rw] code
11
+ # @return [String] the module code
12
+ attr_accessor :code
13
+
14
+ # @!attribute [rw] name
15
+ # @return [String] the module name
16
+ attr_accessor :name
17
+
18
+ # Initialises a new Module instance
19
+ def initialize(uri, factory, json: nil, ld: nil)
20
+ super(uri, factory)
21
+ self.code =
22
+ get_property('code', json) ||
23
+ get_property(AIISO_CODE, ld)
24
+ self.name =
25
+ get_property('name', json) ||
26
+ get_property(AIISO_NAME, ld)
27
+ end
28
+
29
+ # Returns a string representation of the Module instance (the module name)
30
+ # @return [String] the string representation of the Module instance
31
+ def to_s
32
+ name.to_s || super
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,371 @@
1
+ require_relative 'base'
2
+
3
+ module Aspire
4
+ module Object
5
+ # Delegates citation_<property> method calls to the parent resource
6
+ module ResourcePropertyMixin
7
+ CITATION_PROPERTIES = %w[
8
+ authors book_jacket_url date doi edition edition_data eissn has_part
9
+ is_part_of isbn10 isbn13 isbns issn issue issued latest_edition
10
+ local_control_number online_resource page page_end page_start
11
+ place_of_publication publisher title type url volume
12
+ ].freeze
13
+
14
+ # Handles citation_<property>() accessor calls by proxying to the parent
15
+ # resource if no instance value is set.
16
+ # The <property> accessor for this instance is called first, and if this
17
+ # returns nil and there is a parent resource (is_part_of), the property
18
+ # accessor of the parent is called.
19
+ # This continues up through the ancestor resources until a value is found.
20
+ # @param method_name [Symbol] the method name
21
+ # Positional and keyword arguments are passed to the property accessor
22
+ def method_missing(method_name, *args, &block)
23
+ property_name = get_property_name_from_method(method_name)
24
+ super if property_name.nil?
25
+ # Try the resource's property first
26
+ value = public_send(property_name, *args, &block)
27
+ return value unless value.nil?
28
+ # Delegate to the parent resource's property if it exists
29
+ # Call the parent's citation_<property> rather than <property> to
30
+ # delegate up the ancestor chain.
31
+ if is_part_of
32
+ value = is_part_of.public_send(method_name, *args, &block)
33
+ return value unless value.nil?
34
+ end
35
+ # Otherwise return nil
36
+ nil
37
+ end
38
+
39
+ # Returns the property name from a citation_<property> missing method call
40
+ # @param method_name [Symbol] the method name
41
+ # @param _include_all [Boolean] if true, include private/protected methods
42
+ # @return [String, nil] the property name, or nil if not a valid property
43
+ def get_property_name_from_method(method_name, _include_all = false)
44
+ # Ignore method names not beginning with citation_
45
+ return nil unless method_name.to_s.start_with?('citation_')
46
+ # Remove the 'citation_' prefix to get the property name
47
+ property = method_name[9..-1]
48
+ return nil if property.nil? || property.empty?
49
+ # Accept only whitelisted properties
50
+ return nil unless CITATION_PROPERTIES.include?(property)
51
+ # Accept only properties with accessor methods
52
+ return nil unless respond_to?(property)
53
+ # Return the property name
54
+ property
55
+ end
56
+
57
+ # Returns true if this method is supported, false if not
58
+ # @param method_name [Symbol] the method name
59
+ # @param include_all [Boolean] if true, include private/protected methods
60
+ # @return [Boolean] true if the method is supported, false otherwise
61
+ def respond_to_missing?(method_name, include_all = false)
62
+ property_name = get_property_name_from_method(method_name, include_all)
63
+ # property_name is not nil if the method is supported
64
+ !property_name.nil?
65
+ end
66
+ end
67
+
68
+ # Shortcut methods
69
+ module ResourceShortcutsMixin
70
+ # Returns the title of the journal article associated with this resource
71
+ # @return [String, nil] the journal article title or nil if not applicable
72
+ def article_title
73
+ part_title_by_type('Article')
74
+ end
75
+
76
+ # Returns the title of the book associated with this resource
77
+ # @return [String, nil] the book title or nil if not applicable
78
+ def book_title
79
+ part_of_title_by_type('Book')
80
+ end
81
+
82
+ # Returns the title of the book chapter associated with this resource
83
+ # @return [String, nil] the book chapter title or nil if not applicable
84
+ def chapter_title
85
+ part_title_by_type('Chapter')
86
+ end
87
+
88
+ # Returns the resource title as expected by the Alma reading list loader
89
+ # (Article = article title, book = book title, other = resource title)
90
+ # @return [String] the citation title
91
+ def citation_title
92
+ article_title || book_title || title
93
+ end
94
+
95
+ # Returns the title of the journal associated with this resource
96
+ # @return [String, nil] the journal title or nil if not applicable
97
+ def journal_title
98
+ part_of_title_by_type('Journal')
99
+ end
100
+
101
+ # Returns the title of the parent resource (book, journal etc.)
102
+ # @return [String] the title of the parent resource
103
+ def part_of_title
104
+ is_part_of ? is_part_of.title : nil
105
+ end
106
+
107
+ # Returns the title of the parent resource (book, journal etc.)
108
+ # @return [String] the title of the parent resource
109
+ def part_of_title_by_type(res_type)
110
+ return title if type == res_type
111
+ return is_part_of.title if is_part_of && is_part_of.type == res_type
112
+ nil
113
+ end
114
+
115
+ # Returns the title of the part (book chapter, journal article etc.)
116
+ # @return [String] the title of the part
117
+ def part_title
118
+ has_part ? has_part.title : nil
119
+ end
120
+
121
+ # Returns the title of the part
122
+ # @param res_type [String] the type of the resource
123
+ # @return [String] the title of the part
124
+ def part_title_by_type(res_type)
125
+ return title if type == res_type
126
+ return has_part.title if has_part && has_part.type == res_type
127
+ nil
128
+ end
129
+ end
130
+
131
+ # Represents a resource in the Aspire API
132
+ class Resource < Base
133
+ include ResourcePropertyMixin
134
+ include ResourceShortcutsMixin
135
+
136
+ PAGE_RANGE = /(?<start>[\da-zA-Z]*)\s*-\s*(?<end>[\da-zA-Z]*)/
137
+
138
+ # @!attribute [rw] authors
139
+ # @return [Array<String>] the list of authors of the resource
140
+ attr_accessor :authors
141
+
142
+ # @!attribute [rw] book_jacket_url
143
+ # @return [String] the book jacket image URL
144
+ attr_accessor :book_jacket_url
145
+
146
+ # @!attribute [rw] date
147
+ # @return [String] the date of publication
148
+ attr_accessor :date
149
+
150
+ # @!attribute [rw] doi
151
+ # @return [String] the DOI for the resource
152
+ attr_accessor :doi
153
+
154
+ # @!attribute [rw] edition
155
+ # @return [String] the edition
156
+ attr_accessor :edition
157
+
158
+ # @!attribute [rw] edition_data
159
+ # @return [Boolean] true if edition data is available
160
+ attr_accessor :edition_data
161
+
162
+ # @!attribute [rw] eissn
163
+ # @return [String] the electronic ISSN for the resource
164
+ attr_accessor :eissn
165
+
166
+ # @!attribute [rw] has_part
167
+ # @return [Array<Aspire::Object::Resource>] child resources
168
+ attr_accessor :has_part
169
+
170
+ # @!attribute [rw] is_part_of
171
+ # @return [Array<Aspire::Object::Resource>] parent resources
172
+ attr_accessor :is_part_of
173
+
174
+ # @!attribute [rw] isbn10
175
+ # @return [String] the 10-digit ISBN for the resource
176
+ attr_accessor :isbn10
177
+
178
+ # @!attribute [rw] isbn13
179
+ # @return [String] the 13-digit ISBN for the resource
180
+ attr_accessor :isbn13
181
+
182
+ # @!attribute [rw] isbns
183
+ # @return [Array<String>] the list of ISBNs for the resource
184
+ attr_accessor :isbns
185
+
186
+ # @!attribute [rw] issn
187
+ # @return [Array<String>] the ISSN for the resource
188
+ attr_accessor :issn
189
+
190
+ # @!attribute [rw] issue
191
+ # @return [String] the issue
192
+ attr_accessor :issue
193
+
194
+ # @!attribute [rw] issued
195
+ # @return [String] the issue date
196
+ attr_accessor :issued
197
+
198
+ # @!attribute [rw] latest_edition
199
+ # @return [Boolean] true if this is the latest edition
200
+ attr_accessor :latest_edition
201
+
202
+ # @!attribute [rw] local_control_number
203
+ # @return [String] the local control number in the library catalogue
204
+ attr_accessor :local_control_number
205
+
206
+ # @!attribute [rw] online_resource
207
+ # @return [Boolean] true if this is an online resource
208
+ attr_accessor :online_resource
209
+
210
+ # @!attribute [rw] page
211
+ # @return [String] the page range
212
+ attr_accessor :page
213
+
214
+ # @!attribute [rw] page_end
215
+ # @return [String] the end page
216
+ attr_accessor :page_end
217
+
218
+ # @!attribute [rw] page_start
219
+ # @return [String] the start page
220
+ attr_accessor :page_start
221
+
222
+ # @!attribute [rw] place_of_publication
223
+ # @return [String] the place of publication
224
+ attr_accessor :place_of_publication
225
+
226
+ # @!attribute [rw] publisher
227
+ # @return [String] the publisher
228
+ attr_accessor :publisher
229
+
230
+ # @!attribute [rw] title
231
+ # @return [String] the title of the resource
232
+ attr_accessor :title
233
+
234
+ # @!attribute [rw] type
235
+ # @return [String] the type of the resource
236
+ attr_accessor :type
237
+
238
+ # @!attribute [rw] url
239
+ # @return [String] the URL of the resource
240
+ attr_accessor :url
241
+
242
+ # @!attribute [rw] volume
243
+ # @return [String] the volume
244
+ attr_accessor :volume
245
+
246
+ # Initialises a new Resource instance
247
+ # @param json [Hash] the resource data from the Aspire JSON API
248
+ # @param ld [Hash] the resource data from the Aspire linked data API
249
+ # @return [void]
250
+ def initialize(uri = nil, factory = nil, json: nil, ld: nil)
251
+ uri ||= json ? json['uri'] : nil
252
+ super(uri, factory)
253
+ return unless json
254
+ init_general(json)
255
+ init_components(json)
256
+ init_edition(json)
257
+ init_identifiers(json)
258
+ init_part(json)
259
+ init_publication(json)
260
+ end
261
+
262
+ # Sets the page range and updates the page_start and page_end properties
263
+ # @param value [String] the page range "start-end"
264
+ # @return [String] the page range "start-end"
265
+ def page=(value)
266
+ @page = value.to_s
267
+ match = PAGE_RANGE.match(@page)
268
+ if match.nil?
269
+ # Value is not a range, treat as a single page
270
+ @page_end = @page
271
+ @page_start = @page
272
+ else
273
+ # Value is a range
274
+ @page_end = match[:end]
275
+ @page_start = match[:start]
276
+ end
277
+ @page
278
+ end
279
+
280
+ # Returns the page range spanned by the page_start and page_end properties
281
+ # @return [String] the page range "start-end" or page number
282
+ def page_range
283
+ return @page_start if @page_end.nil? || @page_start == @page_end
284
+ return @page_end if @page_start.nil?
285
+ "#{@page_start}-#{@page_end}"
286
+ end
287
+
288
+ # Returns a string representation of the resource (the title)
289
+ # @return [String] the string representation of the resource
290
+ def to_s
291
+ title
292
+ end
293
+
294
+ protected
295
+
296
+ # Sets the component-related properties
297
+ # @param json [Hash] the resource data from the Aspire JSON API
298
+ # @return [void]
299
+ def init_components(json)
300
+ has_part = json['hasPart']
301
+ is_part_of = json['isPartOf']
302
+ self.has_part = has_part ? factory.get(uri, json: has_part) : nil
303
+ self.is_part_of = is_part_of ? factory.get(uri, json: is_part_of) : nil
304
+ end
305
+
306
+ # Sets the edition-related properties
307
+ # @param json [Hash] the resource data from the Aspire JSON API
308
+ # @return [void]
309
+ def init_edition(json)
310
+ self.edition = get_property('edition', json)
311
+ self.edition_data = get_property('editionData', json)
312
+ self.latest_edition = get_property('latestEdition', json)
313
+ end
314
+
315
+ # Sets general resource properties
316
+ # @param json [Hash] the resource data from the Aspire JSON API
317
+ # @return [void]
318
+ def init_general(json)
319
+ self.authors = get_property('authors', json, single: false)
320
+ self.book_jacket_url = get_property('bookjacketURL', json)
321
+ self.issued = json ? json['issued'] : nil # TODO
322
+ self.online_resource = get_boolean('onlineResource', json)
323
+ self.title = get_property('title', json)
324
+ self.type = get_property('type', json)
325
+ self.url = get_property('url', json, is_url: true)
326
+ end
327
+
328
+ # Sets the identifier properties (DOI, ISBN/ISSN, local control number)
329
+ # @param json [Hash] the resource data from the Aspire JSON API
330
+ # @return [void]
331
+ def init_identifiers(json)
332
+ self.doi = get_property('doi', json)
333
+ self.eissn = get_property('eissn', json)
334
+ self.isbn10 = get_property('isbn10', json)
335
+ self.isbn13 = get_property('isbn13', json)
336
+ self.isbns = get_property('isbns', json, single: false)
337
+ self.issn = get_property('issn', json)
338
+ self.local_control_number = get_property('lcn', json)
339
+ end
340
+
341
+ # Sets the pagination-related properties
342
+ # @param json [Hash] the resource data from the Aspire JSON API
343
+ # @return [void]
344
+ def init_pagination(json)
345
+ self.page_end = get_property('pageEnd', json)
346
+ self.page_start = get_property('pageStart', json)
347
+ # Override page_end and page_start if the page range is specified
348
+ range = get_property('page', json)
349
+ self.page = range unless range.nil? || range.empty?
350
+ end
351
+
352
+ # Sets the part-related properties
353
+ # @param json [Hash] the resource data from the Aspire JSON API
354
+ # @return [void]
355
+ def init_part(json)
356
+ init_pagination(json)
357
+ self.issue = get_property('issue', json)
358
+ self.volume = get_property('volume', json)
359
+ end
360
+
361
+ # Sets the publication-related properties
362
+ # @param json [Hash] the resource data from the Aspire JSON API
363
+ # @return [void]
364
+ def init_publication(json)
365
+ self.date = get_property('date', json)
366
+ self.place_of_publication = get_property('placeOfPublication', json)
367
+ self.publisher = get_property('publisher', json)
368
+ end
369
+ end
370
+ end
371
+ end
@@ -0,0 +1,47 @@
1
+ require 'aspire/object/base'
2
+
3
+ module Aspire
4
+ module Object
5
+ # Represents the time period covered by a reading list in the Aspire API
6
+ class TimePeriod < Base
7
+ # @!attribute [rw] active
8
+ # @return [Boolean] true if the time period is currently active
9
+ attr_accessor :active
10
+
11
+ # @!attribute [rw] end_date
12
+ # @return [Date] the end of the time period
13
+ attr_accessor :end_date
14
+
15
+ # @!attribute [rw] start_date
16
+ # @return [Date] the start of the time period
17
+ attr_accessor :start_date
18
+
19
+ # @!attribute [rw] title
20
+ # @return [String] the title of the time period
21
+ attr_accessor :title
22
+
23
+ # Initialises a new TimePeriod instance
24
+ def initialize(uri, factory, json: nil, ld: nil)
25
+ super(uri, factory)
26
+ json ||= {}
27
+ self.active = get_property('active', json)
28
+ self.end_date = get_date('endDate', json)
29
+ self.start_date = get_date('startDate', json)
30
+ self.title = get_property('title', json)
31
+ end
32
+
33
+ # Returns a string representation of the TimePeriod instance (the title)
34
+ # @return [String] the string representation of the TimePeriod instance
35
+ def to_s
36
+ title.to_s
37
+ end
38
+
39
+ # Returns the academic year containing this time period
40
+ # @return [Integer, nil] the academic year, or nil if unspecified
41
+ def year
42
+ result = title.split('-')[0]
43
+ result ? result.to_i : nil
44
+ end
45
+ end
46
+ end
47
+ end