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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 814a301b9b2e5cf3ebc1abb88696445a45cbe802
4
+ data.tar.gz: 6bf15e340d9f193d63919bce90fcad7683fae408
5
+ SHA512:
6
+ metadata.gz: 0db90365108443a9b0dde28a0a548572bca5924fc45ac6a09e91ecab81240c9b87e47aafc9b2d801c98b2c238a46427ca9269f8f1564a2685825bc3505849ddd
7
+ data.tar.gz: f0b6c6683233251de2cf045b7fce0afd245226e507c59ca02a9b7448eab0a7874b2364ba54a48326e169de42da4b21cc80cfd28d4cb025b8d46527c86d39bcd4
@@ -0,0 +1,59 @@
1
+ # IntelliJ files
2
+ .idea/
3
+ *.iml
4
+ *.iws
5
+
6
+ # Created by https://www.gitignore.io/api/ruby
7
+
8
+ ### Ruby ###
9
+ *.gem
10
+ *.rbc
11
+ /.byebug_history
12
+ /.config
13
+ /coverage/
14
+ /InstalledFiles
15
+ /pkg/
16
+ /spec/reports/
17
+ /spec/examples.txt
18
+ /test/tmp/
19
+ /test/version_tmp/
20
+ /tmp/
21
+
22
+ # Used by dotenv library to load environment variables.
23
+ .env
24
+
25
+ ## Specific to RubyMotion:
26
+ .dat*
27
+ .repl_history
28
+ build/
29
+ *.bridgesupport
30
+ build-iPhoneOS/
31
+ build-iPhoneSimulator/
32
+
33
+ ## Specific to RubyMotion (use of CocoaPods):
34
+ #
35
+ # We recommend against adding the Pods directory to your .gitignore. However
36
+ # you should judge for yourself, the pros and cons are mentioned at:
37
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
38
+ #
39
+ # vendor/Pods/
40
+
41
+ ## Documentation cache and generated files:
42
+ /.yardoc/
43
+ /_yardoc/
44
+ /doc/
45
+ /rdoc/
46
+
47
+ ## Environment normalization:
48
+ /.bundle/
49
+ /vendor/bundle
50
+ /lib/bundler/man/
51
+
52
+ # for a library or gem, you might want to ignore these files since the code is
53
+ # intended to run in multiple environments; otherwise, check them in:
54
+ Gemfile.lock
55
+ .ruby-version
56
+ .ruby-gemset
57
+
58
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
59
+ .rvmrc
@@ -0,0 +1 @@
1
+ aspire
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.14.6
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at Sh3d0fd00m. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
@@ -0,0 +1,20 @@
1
+ FROM ruby:2.4.1
2
+
3
+ #RUN groupadd -r pure && useradd -r -g pure pure
4
+
5
+ # RENAME TO LEGANTO TO ASPIRE
6
+ ENV DATA_ROOT /leganto-data
7
+ ENV APP_ROOT /leganto-extractor
8
+ RUN mkdir -p $DATA_ROOT
9
+ RUN mkdir -p $APP_ROOT
10
+ WORKDIR $APP_ROOT
11
+
12
+ ENV LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8
13
+
14
+ COPY pkg/aspire-0.1.0.gem aspire-0.1.0.gem
15
+
16
+ RUN gem install aspire-0.1.0.gem
17
+
18
+ COPY entrypoint.sh $APP_ROOT/entrypoint.sh
19
+
20
+ ENTRYPOINT ["./entrypoint.sh"]
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in aspire.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 lbaajh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,851 @@
1
+ # Aspire
2
+
3
+ This gem provides tools for working with Talis Aspire APIs to manage reading
4
+ lists. It implements a data model for the common API objects (list, section,
5
+ item, resource etc.) and provides a mechanism for caching API data for offline
6
+ access.
7
+
8
+ ## Contents
9
+
10
+ * [Installation](#installation)
11
+ * [Usage](#usage)
12
+ * [Overview](#usage-overview)
13
+ * [APIs](#usage-apis)
14
+ * [Linked Data API](#usage-apis-linked-data)
15
+ * [Authenticated (JSON) API](#usage-apis-json)
16
+ * [Caching](#caching)
17
+ * [Cache](#caching-cache)
18
+ * [Cache Builder](#caching-cache-builder)
19
+ * [Report Enumerator](#caching-cache-builder-enum)
20
+ * [Cache Builder](#caching-cache-builder-cache-builder)
21
+ * [Caveats](#caching-cache-builder-caveats)
22
+ * [Data Model](#model)
23
+ * [Overview](#model-overview)
24
+ * [User Profiles](#model-user-profiles)
25
+ * [Factory](#model-factory)
26
+ * [List](#model-list)
27
+ * [Iterating over lists and sections](#model-list-iter)
28
+ * [List Properties](#model-list-properties)
29
+ * [ListSection Properties](#model-list-section-properties)
30
+ * [ListItem Properties](#model-list-item-properties)
31
+ * [Resource](#model-resource)
32
+ * [Basic Properties](#model-resource-basic)
33
+ * [Linked Resource Properties](#model-resource-linked)
34
+ * [Digitisation](#model-digitisation)
35
+ * [Module](#model-module)
36
+ * [TimePeriod](#model-timeperiod)
37
+ * [User](#model-user)
38
+ * [Implementation Notes](#implementation)
39
+ * [Preserving List Structure](#implementation-structure)
40
+ * [Development](#development)
41
+ * [Contributing](#contributing)
42
+ * [License](#license)
43
+
44
+ ## <a name="installation"></a>Installation
45
+
46
+ Add this line to your application's Gemfile:
47
+
48
+ ```ruby
49
+ gem 'aspire'
50
+ ```
51
+
52
+ And then execute:
53
+
54
+ $ bundle
55
+
56
+ Or install it yourself as:
57
+
58
+ $ gem install aspire
59
+
60
+ ## <a name="usage"></a>Usage
61
+
62
+ ### <a name="usage-overview"></a>Overview
63
+
64
+ This gem provides tools for working with the Talis Aspire
65
+ [Linked Data API](https://support.talis.com/hc/en-us/articles/205860451) and the
66
+ newer [Authenticated (JSON) APIs](http://docs.talisrl.apiary.io/).
67
+
68
+ To use the Authenticated (JSON) APIs, you will need to request an API key and
69
+ secret from Talis
70
+ ([more](https://support.talis.com/hc/en-us/articles/208221125)).
71
+
72
+ ### <a name="usage-apis"></a>APIs
73
+ Credentials, tenancy URLs and other client-specific details are encapsulated
74
+ by API objects which are passed to other classes.
75
+
76
+ #### <a name="usage-apis-linked-data"></a>Linked Data API
77
+
78
+ To create a Linked Data API instance:
79
+ ```ruby
80
+ require 'aspire'
81
+
82
+ # Create and configure a logger if required, or pass nil to disable logging
83
+ require 'logger'
84
+ logger = Logger.new(STDOUT)
85
+
86
+ # Set the timeout in seconds for API calls, or 0 to disable (wait indefinitely).
87
+ # Large lists (several hundred items) may take up to 30 seconds so adjust this
88
+ # according to your requirements.
89
+ timeout = 10
90
+
91
+ # Tenancy configuration
92
+ # - these settings specify the base components of resource URIs; all are
93
+ # optional and default to values derived from the tenancy code
94
+
95
+ # linked_data_root is the root URI of URIs returned in linked data responses
96
+ # (defaults to https://<tenancy-code>.myreadinglists.org)
97
+ linked_data_root = 'https://myinstitution.myreadinglists.org'
98
+
99
+ # tenancy_host_aliases is a list of host name aliases which may appear in
100
+ # resource URIs
101
+ tenancy_host_aliases = ['resourcelists.myinstitution.ac.uk']
102
+
103
+ # tenancy_root is the canonical root URI of the tenancy
104
+ # (defaults to https://<tenancy_code>.rl.talis.com)
105
+ tenancy_root = 'https://myinstitution.rl.talis.com'
106
+
107
+ # Create the Linked Data API instance
108
+ # - replace 'tenancy_code' with the appropriate value for your Aspire tenancy,
109
+ # e.g. 'myinstitution'
110
+ ld_api = Aspire::API::LinkedData('tenancy_code',
111
+ linked_data_root: linked_data_root,
112
+ tenancy_host_aliases: tenancy_host_aliases,
113
+ tenancy_root: tenancy_root,
114
+ logger: logger, timeout: timeout)
115
+ ```
116
+
117
+ #### <a name="usage-apis-json"></a>Authenticated (JSON) API
118
+
119
+ To create an Authenticated (JSON) API instance:
120
+
121
+ ```ruby
122
+ require 'aspire'
123
+
124
+ # Set the logger and timeout as above
125
+ logger = ...
126
+ timeout = ...
127
+
128
+ # Create the JSON API instance
129
+ # - replace 'api_client_id', 'api_secret' and 'tenancy_code' with appropriate
130
+ # values for your Aspire tenancy
131
+ json_api = Aspire::API::JSON.new('api_client_id', 'api_secret', 'tenancy_code',
132
+ logger: logger, timeout: timeout)
133
+
134
+ # Call a JSON API endpoint
135
+ json_api.call()
136
+
137
+ ```
138
+
139
+ ### <a name="caching"></a>Caching
140
+
141
+ #### <a name="caching-cache"></a>Cache
142
+
143
+ The `Aspire::Caching::Cache` class mediates access to the Aspire APIs,
144
+ storing API responses on disk and returning the cached copies on subsequent
145
+ accesses. This is intended to serve as both a means of backing up Aspire data
146
+ locally and as a means of speeding up slow API calls.
147
+
148
+ ```ruby
149
+ require 'aspire'
150
+
151
+ # Create Linked Data and JSON API instances
152
+ ld_api = Aspire::API::LinkedData.new(...)
153
+ json_api = Aspire::API::JSON.new(...)
154
+
155
+ # Create the cache at the specified path
156
+ #
157
+ # The following optional keyword arguments are accepted:
158
+ # - clear: if true, remove and recreate the cache path
159
+ # (default: false)
160
+ # - logger: a Logger instance for logging, or nil to disable
161
+ # (default: nil)
162
+ # - mode: the octal file permissions for the cache directory
163
+ # (default: 0o0750)
164
+ #
165
+ cache = Aspire::Caching::Cache.new(ld_api, json_api, '/path/to/cache/root',
166
+ clear: false,
167
+ logger: Logger.new(STDOUT),
168
+ mode: 0o0700)
169
+ ```
170
+
171
+ The cache is mainly intended for internal use by the data model but can be
172
+ used by clients of this gem if required.
173
+
174
+ ```ruby
175
+ # An Aspire linked data URI resource
176
+ uri = 'https://myinstitution.rl.talis.com/lists/FFCB71DE-EE3A-1D42-BAAE-9CA9CFF0EE72'
177
+
178
+ # Read an Aspire linked data URI, returning the parsed JSON data as a Hash
179
+ # - use_api: if true, when a URI is not already in the cache, read data from
180
+ # the Aspire API and write it to the cache
181
+ # (default: true)
182
+ # - use_cache: if true, read data from the cache before trying the Aspire API
183
+ # (default: true)
184
+ # - json: if true, read data from the JSON API, otherwise use the Linked
185
+ # Data API
186
+ # (default: false)
187
+ data = cache.read(uri, json: false, use_api: true, use_cache: true) \
188
+ do |data, entry, from_cache, json|
189
+ # The block is called only if data is read from the cache or API
190
+ # The parameters are:
191
+ # - data = the parsed JSON data for the resource
192
+ # - entry = an ```Aspire::Caching::CacheEntry``` instance
193
+ # representing the cached entry (for internal use)
194
+ # - from_cache = true if the data was retrieved from the cache,
195
+ # false if from the API
196
+ # - json = true if the data is from the JSON API,
197
+ # false if from the Linked Data API
198
+ end
199
+
200
+ # Remove a resource from the cache, returning the cached JSON data as a Hash
201
+ # - remove_children: if true, remove
202
+ data = cache.remove(uri, force: true, remove_children: true) \
203
+ do |data, entry|
204
+ # The block is called only if cached data exists
205
+ # The parameters are:
206
+ # - data = the parsed JSON data for the cached resource
207
+ # - entry = an ```Aspire::Caching::CacheEntry``` instance representing
208
+ # the cached entry (for internal use)
209
+ end
210
+
211
+ # Delete the cache contents but not the root path
212
+ cache.clear
213
+
214
+ # Delete the cache root path and contents
215
+ cache.delete
216
+
217
+ # Return true if the cache root path is empty, false otherwise
218
+ cache.empty?
219
+ ```
220
+
221
+ #### <a name="caching-cache-builder"></a>Cache Builder
222
+
223
+ The `Aspire::Caching::Builder` class constructs an offline cache of Linked Data
224
+ and JSON API data.
225
+
226
+ Given an enumerator of list IDs, the cache builder downloads the JSON and Linked
227
+ Data API data for each list and recursively follows all URIs in the linked data
228
+ until every entity has been retrieved.
229
+
230
+ The list enumerator is expected to be an instance of
231
+ `Aspire::Enumerator::ReportEnumerator`, which parses a file as a CSV and yields
232
+ the parsed row. When parsing an Aspire "All Lists" report, the parsed CSV row is
233
+ yielded as a Hash containing the list URI at key "List Link", so the cache
234
+ builder expects this from the enumerator. If you're using a custom enumerator
235
+ rather than the `ReportEnumerator`, you should yield the list URI as follows:
236
+
237
+ ```ruby
238
+ yielder << { 'List Link' => '<list-URI>' }
239
+ ```
240
+
241
+ ##### <a name="caching-cache-builder-enum"></a>Report Enumerator
242
+
243
+ A list enumerator is created from an Aspire "All Lists" report CSV as follows:
244
+
245
+ ```ruby
246
+
247
+ # Optionally define one or more filters to control which lists are selected.
248
+ # Each filter is a Proc instance which accepts the Hash or Array from the
249
+ # CSV parser and returns true to include the list or false to ignore it.
250
+ # All filters must return true for the list to be included.
251
+ filters = [
252
+ proc { |row| row['Status'] == 'Published' },
253
+ proc { |row| row['Privacy'] == 'Public' },
254
+ proc { |row| row['Time Period'] == '2017-18' }
255
+ ]
256
+
257
+ # The filename of a downloaded Aspire "All Lists" report in CSV format
258
+ filename = '/path/to/all_lists.csv'
259
+
260
+ # Create the report enumerator
261
+ lists = Aspire::Enumerator::ReportEnumerator(filename, filters)
262
+ ```
263
+
264
+ ##### <a name="caching-cache-builder-cache-builder"></a>Cache Builder
265
+
266
+ ```ruby
267
+ # Create a Cache
268
+ cache = Aspire::Caching::Cache.new(...)
269
+
270
+ # Create a list enumerator
271
+ lists = Aspire::Enumerator::ReportEnumerator(...)
272
+
273
+ # Create a cache Builder
274
+ builder = Aspire::Caching::Builder.new(cache)
275
+
276
+ # Build the cache
277
+ # - clear: if true, clear the cache before building
278
+ builder.build(lists, clear: true)
279
+ ```
280
+
281
+ ##### <a name="caching-cache-builder-caveats"></a>Caveats
282
+
283
+ 1. The current implementation is slow to run (partly due to the slow speed of
284
+ the JSON API for large lists) and memory-intensive (due to the recursive
285
+ processing of referenced resources, which can result in deeply-nested method
286
+ call stacks). A parallelised approach would help to improve this.
287
+
288
+ 2. The current implementation doesn't reliably handle resuming an interrupted
289
+ build and may skip data. Because of this, it's **strongly recommended** to
290
+ always build a new cache with the `clear: true` flag.
291
+
292
+ 3. Due to the previous two points (slow run speed and inability to resume an
293
+ interrupted build), and the possibility that network and other problems may
294
+ break a long-running build, it's recommended to build a number of small caches
295
+ rather than a single cache of everything. Filters passed to the
296
+ `Aspire::Enumerator::ReportEnumerator` can limit the size of the cache.
297
+ For example, you may want to build one cache per time period.
298
+
299
+ 4. The cache builder can only download publicly-visible lists (private lists
300
+ require authentication by the owner), so you should always include a filter for
301
+ this:
302
+
303
+ ```ruby
304
+ filters = [
305
+ proc { |row| row['Privacy'] == 'Public' },
306
+ # other filters
307
+ ]
308
+ ```
309
+ ### <a name="model"></a>Data Model
310
+
311
+ #### <a name="model-overview"></a>Overview
312
+
313
+ The data model provides a set of classes representing common resources in the
314
+ Talis Aspire APIs, such as lists, list sections, list items and bibliographic
315
+ resources.
316
+
317
+ Model instances are retrieved through a factory which uses a combination of the
318
+ Linked Data API, Authenticated (JSON) API and the Aspire "All User Profiles"
319
+ report to construct the models.
320
+
321
+ #### <a name="model-user-profiles"></a>User Profiles
322
+
323
+ User profiles referenced by the Aspire Linked Data API URIs are not directly
324
+ available through the Linked Data or JSON APIs, so the `Aspire::Object::Factory`
325
+ class accepts a Hash of user data of the form:
326
+
327
+ ```ruby
328
+ users = {
329
+ 'https://myinstitution.rl.talis.com/users/ABCD1234-FE98-DC76-BA54-54321FEBACD0' => {
330
+ email: 'anne.onymouse@myinstution.ac.uk',
331
+ firstName: 'Anne',
332
+ role: ['List publisher', 'List creator'],
333
+ surname: 'Onymous',
334
+ uri: 'https://myinstitution.rl.talis.com/users/ABCD1234-FE98-DC76-BA54-54321FEBACD0'
335
+ }
336
+ }
337
+ ```
338
+
339
+ The data hash follows the JSON format documented by the
340
+ [Aspire JSON API](http://docs.talisrl.apiary.io/#reference/catalog/catalog-record-based-on-isbn/get-user-profile)
341
+
342
+ The `Aspire::UserLookup` class provides a simple means of loading an Aspire
343
+ "All User Profiles" report CSV.
344
+
345
+ ```ruby
346
+ require 'aspire'
347
+
348
+ users = Aspire::UserLookup.new(filename: '/path/to/all_user_profiles.csv')
349
+ user = users['https://myinstitution.rl.talis.com/users/ABCD1234-FE98-DC76-BA54-54321FEBACD0']
350
+ ```
351
+
352
+ #### <a name="model-factory"></a>Factory
353
+
354
+ The `Aspire::Object::Factory` class returns data model instances. Data is
355
+ read from an Aspire API data cache (see *Cache* above) except for user data,
356
+ which is supplied by a Hash mapping user URIs to a Hash of user data (see
357
+ *User Profiles* above).
358
+
359
+ ```ruby
360
+ require 'aspire'
361
+
362
+ # Create a cache
363
+ cache = Aspire::Caching::Cache.new(...)
364
+
365
+ # Create a user hash from an Aspire "All User Profiles" report CSV
366
+ users = Aspire::UserLookup.new(filename: '/path/to/all_user_profiles.csv')
367
+
368
+ # Create a factory
369
+ factory = Aspire::Object::Factory.new(cache, users)
370
+
371
+ # Get a model instance by its URI
372
+ uri = 'https://myinstitution.rl.talis.com/lists/FFCB71DE-EE3A-1D42-BAAE-9CA9CFF0EE72'
373
+ list = factory.get(uri)
374
+ ```
375
+
376
+ #### <a name="model-list"></a>List
377
+
378
+ `Aspire::Object::List` represents a resource list, composed of an
379
+ ordered sequence of `Aspire::Object::ListSection` and `Aspire::Object::ListItem`
380
+ instances. The ordering of list entries and the nested list structure are both
381
+ preserved from Aspire (see *Implementation Notes* for details).
382
+
383
+ `Aspire::Object::ListSection` represents a resource list section, composed of
384
+ an ordered sequence of `Aspire::Object::ListSection` and
385
+ `Aspire::Object::ListItem` instances (nested subsections and list items).
386
+
387
+ `Aspire::Object::ListItem` represents a single list item.
388
+
389
+ ##### <a name="model-list-iter"></a>Iterating over lists and sections
390
+
391
+ `List` and `ListSection` act as ordered containers for child `ListSection` and
392
+ `ListItem` instances. Both classes support various iterators over their child
393
+ and parent objects:
394
+
395
+ ```ruby
396
+ require 'aspire'
397
+
398
+ # Create a factory
399
+ factory = Aspire::Object::Factory.new(...)
400
+
401
+ # Get a list
402
+ list = factory.get(...)
403
+
404
+ # Iterate over the top-level list contents in list order
405
+ list.each { |item| # item is a ListSection or ListItem instance }
406
+
407
+ # Iterate over all ListItem instances in list order
408
+ # Nested list sections are iterated in depth-first order
409
+ list.each_item { |item| # item is a ListItem instance }
410
+
411
+ # Iterate over all ListSection instances in list order
412
+ # Nested list sections are iterated in depth-first order
413
+ list.each_section { |section| # section is a ListSection instance }
414
+
415
+ # Get a list of all ListItem instances in list order
416
+ # Nested list sections are iterated in depth-first order
417
+ items = list.items
418
+
419
+ # Get a list of all ListSection instances in list order
420
+ # Nested list sections are iterated in depth-first order
421
+ sections = list.sections
422
+
423
+ # Get the number of top-level list items (sections and items)
424
+ length = list.length(:entry)
425
+
426
+ # Get the number of ListItem instances
427
+ # Both forms are equivalent
428
+ length = list.length
429
+ length = list.length(:item)
430
+
431
+ # Get the number of top-level ListSection instances
432
+ length = list.length(:section)
433
+
434
+ # Get the parent list of a List, ListSection or ListItem
435
+ parent_list = list.parent_list
436
+
437
+ # Get the immediate parent section of a ListSection or ListItem
438
+ parent_section = list.parent_section
439
+
440
+ # Get a list of parent sections of a ListSection or ListItem in nearest ancestor
441
+ # first order
442
+ parent_sections = list.parent_sections
443
+
444
+ # Get a list of parent items (ListSection and List) of a ListSection or ListItem
445
+ # in nearest ancestor first order
446
+ parents = list.parents
447
+
448
+ # Get a list of parent items matching the supplied classes
449
+ parents = list.parents(List, ListSection)
450
+
451
+ # Get a list of parent items where the supplied block returns true
452
+ parents = list.parents { |item| item.is_a?(ListItem) }
453
+ ```
454
+
455
+ ##### <a name="model-list-properties"></a>List properties
456
+
457
+ ```ruby
458
+
459
+ # Creation timestamp of the list as a DateTime
460
+ list.created
461
+
462
+ # Reading list creators as an array of Aspire::Object::User
463
+ list.creator
464
+
465
+ # Description of the list
466
+ list.description
467
+
468
+ # List items as a hash of Aspire::Object::ListItem indexed by item URI
469
+ list.items
470
+
471
+ # Timestamp of the most recent list publication as a DateTime
472
+ list.last_published
473
+
474
+ # Timestamp of the most recent list update as a DateTime
475
+ list.last_updated
476
+
477
+ # Modules referencing this list as an array of Aspire::Object::Module
478
+ list.modules
479
+
480
+ # Reading list name
481
+ list.name
482
+ list.to_s
483
+
484
+ # List owner as an Aspire::Object::User
485
+ list.owner
486
+
487
+ # List publisher as an Aspire::Object::User
488
+ list.publisher
489
+
490
+ # Period covered by the list as an Aspire::Object::TimePeriod
491
+ list.time_period
492
+ ```
493
+
494
+ ##### <a name="model-list-section-properties"></a>ListSection properties
495
+
496
+ ```ruby
497
+ # Section description
498
+ section.description
499
+
500
+ # Section name
501
+ section.name
502
+ section.to_s
503
+ ```
504
+
505
+ ##### <a name="model-list-item-properties"></a>ListItem properties
506
+
507
+ ```ruby
508
+ # Digitisation details for the item as an Aspire::Object::Digitisation
509
+ item.digitisation
510
+
511
+ # Importance of the item
512
+ item.importance
513
+
514
+ # Private library note for the item
515
+ item.library_note
516
+
517
+ # Identifier of the resource in the local library management system
518
+ item.local_control_number
519
+
520
+ # General public note for the item
521
+ item.note
522
+
523
+ # Student note if available, otherwise the general public note
524
+ item.public_note
525
+
526
+ # Resource for the item as an Aspire::Object::Resource
527
+ item.resource
528
+
529
+ # Public student note for the item
530
+ item.student_note
531
+
532
+ # Title of the item (i.e. the title of the associated resource)
533
+ item.title
534
+ # The resource title is always returned if it is available.
535
+ # If there is no associated resource, an alternative can be specified;
536
+ # the default is to return nil.
537
+ item.title(:library_note) # returns library_note
538
+ item.title(:note) # returns public_note || library_note
539
+ item.title(:public_note) # returns public_note
540
+ item.title(:uri) # returns the item URI
541
+ ```
542
+
543
+ #### <a name="model-resource"></a>Resource
544
+
545
+ `Aspire::Object::Resource` represents the bibliographic item (book/chapter,
546
+ journal/article, online resource etc.) referenced by a list item.
547
+
548
+ Resources may be linked to other resources. For example, a book chapter resource
549
+ may be linked to its parent book, or a journal article to its parent journal.
550
+
551
+ ##### <a name="model-resource-basic"></a>Basic Properties
552
+
553
+ ```ruby
554
+ # Get the resource from the list item
555
+ resource = item.resource
556
+
557
+ # List of authors of the resource as an array of strings
558
+ resource.authors
559
+
560
+ # Book jacket image URL
561
+ resource.book_jacket_url
562
+
563
+ # Date of publication as a string
564
+ resource.date
565
+
566
+ # DOI for the resource
567
+ resource.doi
568
+
569
+ # Edition
570
+ resource.edition
571
+
572
+ # true if edition data is available, false if not
573
+ resource.edition_data
574
+
575
+ # Electronic ISSN for the resource
576
+ resource.eissn
577
+
578
+ # Child resources as an array of Aspire::Object::Resource
579
+ # - e.g. the chapters contained of a book or articles of a journal
580
+ resource.has_part
581
+
582
+ # Parent resources as an array of Aspire::Object::Resource
583
+ # - e.g. the book containing a chapter or journal containing an article
584
+ resource.is_part_of
585
+
586
+ # 10-digit ISBN for the resource
587
+ resource.isbn10
588
+
589
+ # 13-digit ISBN for the resource
590
+ resource.isbn13
591
+
592
+ # List of ISBNs for the resource
593
+ resource.isbns
594
+
595
+ # ISSN for the resource
596
+ resource.issn
597
+
598
+ # Issue
599
+ resource.issue
600
+
601
+ # Issue date as a string
602
+ resource.issued
603
+
604
+ # true if this is the latest edition, false otherwise
605
+ resource.latest_edition
606
+
607
+ # Local control number in the library catalogue
608
+ resource.local_control_number
609
+
610
+ # true if this is an online resource, false otherwise
611
+ resource.online_resource
612
+
613
+ # Page range
614
+ resource.page
615
+
616
+ # End page
617
+ resource.page_end
618
+
619
+ # Start page
620
+ resource.page_start
621
+
622
+ # Place of publication
623
+ resource.place_of_publication
624
+
625
+ # Publisher
626
+ resource.publisher
627
+
628
+ # Title of the resource
629
+ resource.title
630
+
631
+ # Type of the resource
632
+ resource.type
633
+
634
+ # URL of the resource
635
+ resource.url
636
+
637
+ # Volume
638
+ resource.volume
639
+ ```
640
+
641
+ ##### <a name="model-resource-linked"></a>Linked Resource Properties
642
+
643
+ Where resources are linked to other resources (e.g. chapters to books or
644
+ articles to journals) a number of shortcut properties are available. These
645
+ methods can be called on either the child or parent resource.
646
+
647
+ ```ruby
648
+ # Article title for a journal or article resource
649
+ resource.article_title
650
+
651
+ # Book title for a book or chapter resource
652
+ resource.book_title
653
+
654
+ # Book chapter title for a book or chapter resource
655
+ resource.chapter_title
656
+
657
+ # Article or chapter title if available, otherwise the resource
658
+ # title
659
+ resource.citation_title
660
+
661
+ # Journal title for an article or journal resource
662
+ resource.journal_title
663
+
664
+ # Parent resource's title (book or journal title)
665
+ resource.part_of_title
666
+
667
+ # Child resource's title (article or chapter title)
668
+ resource.part_title
669
+
670
+ # Any resource property can be prefixed with "citation_"
671
+ # In this case, if the resource property is not set, the property of the
672
+ # parent resource is returned instead if applicable.
673
+ resource.citation_title # Returns resource.title or the parent title
674
+ resource.citation_isbn10 # Returns resource.isbn10 or the parent isbn10
675
+ ```
676
+
677
+ #### <a name="model-digitisation"></a>Digitisation
678
+
679
+ `Aspire::Object::Digitisation` represents a Talis Digitised Content request
680
+ associated with a list item.
681
+
682
+ ```ruby
683
+ # Get the digitisation request details from the list
684
+ digitisation = list.digitisation
685
+
686
+ # Digitisation bundle ID
687
+ digitisation.bundle_id
688
+
689
+ # Digitisation request ID
690
+ digitisation.request_id
691
+
692
+ # Digitisation request status
693
+ digitisation.request_status
694
+ ```
695
+
696
+ #### <a name="model-module"></a>Module
697
+
698
+ `Aspire::Object::Module` represents a course module associated with a list.
699
+
700
+ ```ruby
701
+ # Get the modules from the list
702
+ modules = list.modules
703
+ module = modules[0]
704
+
705
+ # Module code
706
+ module.code
707
+
708
+ # Module name
709
+ module.name
710
+ ```
711
+
712
+ #### <a name="model-timeperiod"></a>TimePeriod
713
+
714
+ `Aspire::Object::TimePeriod` represents the time period covered by a list.
715
+
716
+ ```ruby
717
+ # Get the time period from the list
718
+ period = list.time_period
719
+
720
+ # true if the list is currently within the time period, false otherwise
721
+ period.active
722
+
723
+ # End of the period as a Date
724
+ period.end_date
725
+
726
+ # Start of the period as a Date
727
+ period.start_date
728
+
729
+ # Title of the time period (e.g. "Winter Term 2016/17")
730
+ period.title
731
+ ```
732
+
733
+ #### <a name="model-user"></a>User
734
+
735
+ `Aspire::Object::User` represents an Aspire user profile returned from the
736
+ [User Profile JSON API](http://docs.talisrl.apiary.io/reference/users/user-profile).
737
+
738
+ ```ruby
739
+ # Get the list owner from the list
740
+ user = list.owner
741
+
742
+ # User first and last names
743
+ user.first_name
744
+ user.surname
745
+
746
+ # User email addresses as an array of strings
747
+ user.email
748
+
749
+ # User roles as an array of strings
750
+ user.role
751
+ ```
752
+
753
+ #### Command-Line
754
+
755
+ ### <a name="implementation"></a>Implementation Notes
756
+
757
+ #### <a name="implementation-structure"></a>Preserving List Structure
758
+
759
+ The Aspire Authenticated (JSON) API provides convenient access to resource list
760
+ items but does not preserve the ordering of list items, and supplies only the
761
+ immediately-enclosing section.
762
+
763
+ However, the Linked Data API includes sequencing data properties with keys of
764
+ the form `http://www.w3.org/1999/02/22-rdf-syntax-ns#_N` (where `N` is the
765
+ ordinal position of the item or section within its parent collection) whose
766
+ values are the URIs of the item or section. These properties allow the list
767
+ order to be recreated in the data model, and the nested section structure to be
768
+ recreated by recursively following the section URIs.
769
+
770
+ Consider the list:
771
+ * Item 1
772
+ * Section 2
773
+ * Item 2.1
774
+ * Item 2.2
775
+ * Item 2.3
776
+ * Section 3
777
+ * Item 3.1
778
+ * etc.
779
+
780
+ The Linked Data API response for the list will contain something like:
781
+ ```json
782
+ {
783
+ “http://myinstitution.myreadinglists.org/lists/A56880F3-10B3-45EC-FD16-D29D0198AEE3": {
784
+
785
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#_1": [ {
786
+ "value": "http://myinstitution.myreadinglists.org/items/BEC9F28E-0663-751A-08D5-4729CBDD5991",
787
+ "type": "uri"
788
+ } ],
789
+
790
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#_2": [ {
791
+ "value": "http://myinstitution.myreadinglists.org/sections/5B357D24-3F3B-35FF-6EEC-3FD8964B523C",
792
+ "type": "uri"
793
+ } ],
794
+
795
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#_3": [ {
796
+ "value": "http://myinstitution.myreadinglists.org/sections/96DACBC0-EC2F-03CD-9A50-70082D2C1D83",
797
+ "type": "uri"
798
+ } ]
799
+ }
800
+ }
801
+ ```
802
+
803
+ The Linked Data API response for "Section 2" will contain something like:
804
+ ```json
805
+ {
806
+ "http://myinstitution.myreadinglists.org/sections/5B357D24-3F3B-35FF-6EEC-3FD8964B523C": {
807
+
808
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#_1": [ {
809
+ "value": "http://myinstitution.myreadinglists.org/items/B51508AB-5166-5CD5-30D5-9DF77BA461BB",
810
+ "type": "uri"
811
+ } ],
812
+
813
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#_2": [ {
814
+ "value": "http://myinstitution.myreadinglists.org/items/AD6F8D90-7EB6-9721-0FDE-3C26F7FA932C",
815
+ "type": "uri"
816
+ } ],
817
+
818
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#_3": [ {
819
+ "value": "http://myinstitution.myreadinglists.org/items/11234123-83EA-D529-8DA1-42BAAA64BF6F",
820
+ "type": "uri"
821
+ } ]
822
+ }
823
+ }
824
+ ```
825
+
826
+ The data model for a list is built as follows:
827
+ 1. get the Authenticated (JSON) API data for the list and build a Hash mapping
828
+ item URI to a ListItem instance
829
+ 2. get the Linked Data API data for the list
830
+ 3. build a List instance
831
+ 4. for each sequencing data property in the list data
832
+ * if the value is an item URI, get the ListItem instance from the items Hash
833
+ * if the value is a section URI, build a ListSection instance (this
834
+ recursively creates subsections and list items)
835
+ * add the instance to the List's children at position `N`
836
+
837
+ ## <a name="development"></a>Development
838
+
839
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
840
+
841
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
842
+
843
+ ## <a name="contributing"></a>Contributing
844
+
845
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lulibrary/aspire. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
846
+
847
+
848
+ ## <a name="license"></a>License
849
+
850
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
851
+