aspire 0.1.0

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