atlas_rb 0.0.81 → 0.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/.yardopts +8 -0
- data/Gemfile.lock +3 -1
- data/README.md +110 -17
- data/lib/atlas_rb/authentication.rb +34 -0
- data/lib/atlas_rb/blob.rb +81 -0
- data/lib/atlas_rb/collection.rb +72 -0
- data/lib/atlas_rb/community.rb +79 -0
- data/lib/atlas_rb/faraday_helper.rb +44 -0
- data/lib/atlas_rb/file_set.rb +49 -0
- data/lib/atlas_rb/resource.rb +48 -0
- data/lib/atlas_rb/version.rb +1 -0
- data/lib/atlas_rb/work.rb +78 -0
- data/lib/atlas_rb.rb +61 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a7a8531fb28a840a529a888d8d5b789ea9ba69aed95fa59d5899f4aea42afd8
|
|
4
|
+
data.tar.gz: 2f545fdddc0d22cdb3def311ed73abcb946524bffb0a3d55952e1cee9226eb10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fbc732c7f405ffe14c22169a38e8037eec302a4572f691ca41e2ebd8f5450aad5c33eab8d0b712dacf9295b62d364178ace4caa9af5d932c65f6f2ca04e9044
|
|
7
|
+
data.tar.gz: 4454c5562263f3401d9cf10764594b7acaf8d978f6264d6738d8f9324abe8b1e9542ce009559c59e3ee31ac36e2fee4a32222912efd43c6fdc06c798d8105a0e
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.83
|
data/.yardopts
ADDED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
atlas_rb (0.0.
|
|
4
|
+
atlas_rb (0.0.83)
|
|
5
5
|
faraday (~> 2.7)
|
|
6
6
|
faraday-follow_redirects (~> 0.3.0)
|
|
7
7
|
faraday-multipart (~> 1)
|
|
@@ -40,6 +40,7 @@ GEM
|
|
|
40
40
|
rspec-support (~> 3.13.0)
|
|
41
41
|
rspec-support (3.13.3)
|
|
42
42
|
uri (1.1.1)
|
|
43
|
+
yard (0.9.43)
|
|
43
44
|
|
|
44
45
|
PLATFORMS
|
|
45
46
|
x86_64-linux
|
|
@@ -48,6 +49,7 @@ DEPENDENCIES
|
|
|
48
49
|
atlas_rb!
|
|
49
50
|
rake (~> 13.0)
|
|
50
51
|
rspec (~> 3.0)
|
|
52
|
+
yard (~> 0.9)
|
|
51
53
|
|
|
52
54
|
BUNDLED WITH
|
|
53
55
|
2.2.33
|
data/README.md
CHANGED
|
@@ -1,39 +1,132 @@
|
|
|
1
|
-
#
|
|
1
|
+
# atlas_rb
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ruby client for the **Atlas** API — Northeastern University's institutional
|
|
4
|
+
digital repository.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
The gem wraps Atlas's REST endpoints in a small set of class-method-only
|
|
7
|
+
modules, one per resource type. There is no client object to instantiate;
|
|
8
|
+
calls are made directly on the resource class:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
AtlasRb::Work.find("w-789")
|
|
12
|
+
```
|
|
6
13
|
|
|
7
14
|
## Installation
|
|
8
15
|
|
|
9
|
-
Add
|
|
16
|
+
Add to your Gemfile:
|
|
10
17
|
|
|
11
18
|
```ruby
|
|
12
|
-
gem
|
|
19
|
+
gem "atlas_rb"
|
|
13
20
|
```
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
Then `bundle install`, or install standalone with `gem install atlas_rb`.
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
Every request reads two environment variables:
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
| Variable | Purpose |
|
|
29
|
+
|---------------|---------------------------------------------------------------|
|
|
30
|
+
| `ATLAS_URL` | Base URL of the Atlas API (e.g. `https://atlas.example.edu`). |
|
|
31
|
+
| `ATLAS_TOKEN` | Bearer token used in the `Authorization` header. |
|
|
20
32
|
|
|
21
|
-
|
|
33
|
+
User-scoped calls (currently only `AtlasRb::Authentication`) additionally
|
|
34
|
+
accept an NUID — the Northeastern University ID — which is forwarded in a
|
|
35
|
+
`User: NUID <nuid>` header.
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
```ruby
|
|
38
|
+
ENV["ATLAS_URL"] = "https://atlas.example.edu"
|
|
39
|
+
ENV["ATLAS_TOKEN"] = "..."
|
|
40
|
+
```
|
|
24
41
|
|
|
25
|
-
|
|
42
|
+
## Resource hierarchy
|
|
26
43
|
|
|
27
|
-
|
|
44
|
+
```
|
|
45
|
+
Community → Collection → Work
|
|
46
|
+
↓
|
|
47
|
+
FileSet
|
|
48
|
+
↓
|
|
49
|
+
Blob
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Class | Represents |
|
|
53
|
+
|------------------------|---------------------------------------------------------------------------|
|
|
54
|
+
| `AtlasRb::Community` | Top-level org unit; may nest sub-Communities. |
|
|
55
|
+
| `AtlasRb::Collection` | Holds Works; lives directly under a Community. |
|
|
56
|
+
| `AtlasRb::Work` | Bibliographic unit (article, thesis, dataset…); MODS metadata lives here. |
|
|
57
|
+
| `AtlasRb::FileSet` | Classified slot under a Work (e.g. `"primary"`, `"supplemental"`). |
|
|
58
|
+
| `AtlasRb::Blob` | The binary bytes; supports streaming downloads. |
|
|
59
|
+
| `AtlasRb::Authentication` | NUID → user record / group lookup. |
|
|
60
|
+
| `AtlasRb::Resource` | Generic resolver and permissions lookup. |
|
|
61
|
+
| `AtlasRb::Reset` | Test-only — wipes Atlas state via `GET /reset`. |
|
|
62
|
+
|
|
63
|
+
### A note on `create` argument shapes
|
|
64
|
+
|
|
65
|
+
The CRUD-twin classes look the same but pass different parent IDs:
|
|
28
66
|
|
|
29
|
-
|
|
67
|
+
```ruby
|
|
68
|
+
AtlasRb::Community.create(nil) # top-level community (parent_id: nil)
|
|
69
|
+
AtlasRb::Community.create("c-123") # sub-community of c-123
|
|
70
|
+
AtlasRb::Collection.create("c-123") # collection under community c-123
|
|
71
|
+
AtlasRb::Work.create("col-456") # work under collection col-456 (collection_id, not parent_id)
|
|
72
|
+
AtlasRb::FileSet.create("w-789", "primary") # file_set under work w-789, classification "primary"
|
|
73
|
+
AtlasRb::Blob.create("w-789", path, name) # blob under work w-789 with original filename preserved
|
|
74
|
+
```
|
|
30
75
|
|
|
31
|
-
|
|
76
|
+
## End-to-end example
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
require "atlas_rb"
|
|
32
80
|
|
|
33
|
-
|
|
81
|
+
ENV["ATLAS_URL"] = "https://atlas.example.edu"
|
|
82
|
+
ENV["ATLAS_TOKEN"] = "..."
|
|
83
|
+
|
|
84
|
+
# 1. Build the org structure (each create can optionally seed MODS metadata).
|
|
85
|
+
community = AtlasRb::Community.create(nil, "/tmp/community-mods.xml")
|
|
86
|
+
collection = AtlasRb::Collection.create(community["id"], "/tmp/coll-mods.xml")
|
|
87
|
+
work = AtlasRb::Work.create(collection["id"], "/tmp/work-mods.xml")
|
|
88
|
+
|
|
89
|
+
# 2. Upload a binary attached to the work, preserving the user-facing filename.
|
|
90
|
+
blob = AtlasRb::Blob.create(work["id"], "/tmp/upload.tmp", "thesis.pdf")
|
|
91
|
+
|
|
92
|
+
# 3. List everything attached to the work.
|
|
93
|
+
AtlasRb::Work.files(work["id"])
|
|
94
|
+
|
|
95
|
+
# 4. Stream the binary back without buffering it in memory.
|
|
96
|
+
File.open("out.pdf", "wb") do |f|
|
|
97
|
+
headers = AtlasRb::Blob.content(blob["id"]) { |chunk| f.write(chunk) }
|
|
98
|
+
puts headers["content-type"]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 5. Look up the acting user and their groups.
|
|
102
|
+
AtlasRb::Authentication.login("001234567")
|
|
103
|
+
AtlasRb::Authentication.groups("001234567")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Generated documentation
|
|
107
|
+
|
|
108
|
+
Full API reference, including `@param` / `@return` / `@example` for every
|
|
109
|
+
method, is generated with [YARD](https://yardoc.org/):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bundle exec yard doc
|
|
113
|
+
open doc/index.html
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`yard stats --list-undoc` should report 100% coverage.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
bin/setup # install dependencies
|
|
122
|
+
bin/console # IRB with atlas_rb loaded
|
|
123
|
+
bundle exec rspec # run tests
|
|
124
|
+
bundle exec rubocop # lint
|
|
125
|
+
```
|
|
34
126
|
|
|
35
|
-
|
|
127
|
+
To cut a release, bump the version in `.version` (which `lib/atlas_rb/version.rb`
|
|
128
|
+
reads at load time) and run `bundle exec rake release`.
|
|
36
129
|
|
|
37
130
|
## License
|
|
38
131
|
|
|
39
|
-
|
|
132
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
|
@@ -1,15 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# User-facing identity lookups against the Atlas API.
|
|
5
|
+
#
|
|
6
|
+
# Unlike the resource classes, {Authentication} threads a real NUID into the
|
|
7
|
+
# `User` header via {FaradayHelper#connection}'s second positional argument.
|
|
8
|
+
# The Atlas server uses that NUID — combined with the bearer token from
|
|
9
|
+
# `ATLAS_TOKEN` — to resolve the acting user and their group memberships.
|
|
10
|
+
#
|
|
11
|
+
# No login round-trip happens here today; the bearer token is assumed to be
|
|
12
|
+
# already provisioned out-of-band. The commented-out code in this file
|
|
13
|
+
# reflects an older flow where a `/token` endpoint exchanged an NUID for a
|
|
14
|
+
# session token.
|
|
4
15
|
class Authentication
|
|
5
16
|
extend AtlasRb::FaradayHelper
|
|
6
17
|
|
|
18
|
+
# Look up the Atlas user record for an NUID.
|
|
19
|
+
#
|
|
20
|
+
# @param nuid [String] the user's Northeastern University ID.
|
|
21
|
+
# @return [Hash] the user record returned by `GET /user`, including at
|
|
22
|
+
# minimum `"id"`, `"name"`, and `"groups"`.
|
|
23
|
+
# @raise [JSON::ParserError] if the response body is not valid JSON
|
|
24
|
+
# (typically caused by an auth failure returning HTML).
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# AtlasRb::Authentication.login("001234567")
|
|
28
|
+
# # => { "id" => 42, "name" => "Jane Doe", "groups" => [...] }
|
|
7
29
|
def self.login(nuid)
|
|
8
30
|
# JSON.parse(connection({ nuid: nuid }).post('/token')&.body)["token"]
|
|
9
31
|
# need hash - id, name, token => ...
|
|
10
32
|
JSON.parse(connection({}, nuid).get('/user')&.body)
|
|
11
33
|
end
|
|
12
34
|
|
|
35
|
+
# Fetch only the group memberships for an NUID.
|
|
36
|
+
#
|
|
37
|
+
# Convenience wrapper around the same `GET /user` call as {.login}; useful
|
|
38
|
+
# when authorization checks only need group names.
|
|
39
|
+
#
|
|
40
|
+
# @param nuid [String] the user's Northeastern University ID.
|
|
41
|
+
# @return [Array<Hash>] the `"groups"` array from the user record.
|
|
42
|
+
# @raise [JSON::ParserError] if the response body is not valid JSON.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# AtlasRb::Authentication.groups("001234567")
|
|
46
|
+
# # => [{ "id" => 7, "name" => "Library Staff" }, ...]
|
|
13
47
|
def self.groups(nuid)
|
|
14
48
|
# user_details = login(nuid)
|
|
15
49
|
# token = user_details[:token] ...
|
data/lib/atlas_rb/blob.rb
CHANGED
|
@@ -1,13 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# The binary content backing a {FileSet} (or attached directly to a {Work}).
|
|
5
|
+
#
|
|
6
|
+
# Blobs are the bytes-on-disk layer of the hierarchy. Operations on this
|
|
7
|
+
# class deal with raw octet streams: uploading new content, replacing
|
|
8
|
+
# content on an existing Blob, and **streaming** downloads via a chunk
|
|
9
|
+
# handler so very large files don't have to be buffered in memory.
|
|
10
|
+
#
|
|
11
|
+
# See also: {Work}, {FileSet}.
|
|
4
12
|
class Blob < Resource
|
|
13
|
+
# Atlas REST endpoint prefix for this resource.
|
|
14
|
+
# @api private
|
|
5
15
|
ROUTE = "/files/"
|
|
6
16
|
|
|
17
|
+
# Fetch a single Blob's metadata record (not its bytes — see {.content}).
|
|
18
|
+
#
|
|
19
|
+
# @param id [String] the Blob ID.
|
|
20
|
+
# @return [Hash] the `"blob"` object, already unwrapped — typically
|
|
21
|
+
# includes `"id"`, `"original_filename"`, `"size"`, and a download URL.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# AtlasRb::Blob.find("b-321")
|
|
25
|
+
# # => { "id" => "b-321", "original_filename" => "scan.pdf", ... }
|
|
7
26
|
def self.find(id)
|
|
8
27
|
JSON.parse(connection({}).get(ROUTE + id)&.body)['blob']
|
|
9
28
|
end
|
|
10
29
|
|
|
30
|
+
# Stream the Blob's binary content through a caller-supplied block.
|
|
31
|
+
#
|
|
32
|
+
# The body is **not** buffered — each chunk Faraday receives is yielded
|
|
33
|
+
# to `chunk_handler` immediately, making this safe for files larger than
|
|
34
|
+
# available memory. The first chunk's response headers are captured and
|
|
35
|
+
# returned so callers can inspect `Content-Type`, `Content-Length`, etc.
|
|
36
|
+
#
|
|
37
|
+
# @param id [String] the Blob ID.
|
|
38
|
+
# @yieldparam chunk [String] the next chunk of binary data.
|
|
39
|
+
# @return [Hash] the response headers from `GET /files/<id>/content`.
|
|
40
|
+
#
|
|
41
|
+
# @example Stream to disk
|
|
42
|
+
# File.open("/tmp/out.pdf", "wb") do |f|
|
|
43
|
+
# headers = AtlasRb::Blob.content("b-321") { |chunk| f.write(chunk) }
|
|
44
|
+
# puts headers["content-type"]
|
|
45
|
+
# end
|
|
46
|
+
def self.content(id, &chunk_handler)
|
|
47
|
+
headers = {}
|
|
48
|
+
connection({}).get("#{ROUTE}#{id}/content") do |req|
|
|
49
|
+
req.options.on_data = proc do |chunk, _bytes_received, env|
|
|
50
|
+
headers = env.response_headers if headers.empty? && env
|
|
51
|
+
chunk_handler.call(chunk)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
headers
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Upload a new Blob attached to a Work.
|
|
58
|
+
#
|
|
59
|
+
# `original_filename` is preserved separately from the upload's
|
|
60
|
+
# `File.basename(blob_path)` because the on-disk path is often a temp
|
|
61
|
+
# file name (`RackMultipart...tmp`) — Atlas needs the user-facing name
|
|
62
|
+
# for download UX.
|
|
63
|
+
#
|
|
64
|
+
# @param id [String] the parent Work ID.
|
|
65
|
+
# @param blob_path [String] path to the binary file on disk to upload.
|
|
66
|
+
# @param original_filename [String] the user-facing filename Atlas
|
|
67
|
+
# should record (e.g. `"final_thesis.pdf"`).
|
|
68
|
+
# @return [Hash] the created `"blob"` payload, including its `"id"`.
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# AtlasRb::Blob.create("w-789", "/tmp/upload.tmp", "final_thesis.pdf")
|
|
72
|
+
# # => { "id" => "b-321", "original_filename" => "final_thesis.pdf", ... }
|
|
11
73
|
def self.create(id, blob_path, original_filename)
|
|
12
74
|
payload = { work_id: id,
|
|
13
75
|
original_filename: original_filename,
|
|
@@ -18,10 +80,29 @@ module AtlasRb
|
|
|
18
80
|
JSON.parse(multipart({}).post(ROUTE, payload)&.body)['blob']
|
|
19
81
|
end
|
|
20
82
|
|
|
83
|
+
# Delete a Blob (the bytes *and* the metadata record).
|
|
84
|
+
#
|
|
85
|
+
# @param id [String] the Blob ID.
|
|
86
|
+
# @return [Faraday::Response] the raw delete response.
|
|
87
|
+
#
|
|
88
|
+
# @example
|
|
89
|
+
# AtlasRb::Blob.destroy("b-321")
|
|
21
90
|
def self.destroy(id)
|
|
22
91
|
connection({}).delete(ROUTE + id)
|
|
23
92
|
end
|
|
24
93
|
|
|
94
|
+
# Replace the bytes of an existing Blob in-place.
|
|
95
|
+
#
|
|
96
|
+
# The Blob ID is preserved; only the underlying content changes. The
|
|
97
|
+
# original filename is *not* updated by this call — use a new
|
|
98
|
+
# {.create} if you need a different `original_filename`.
|
|
99
|
+
#
|
|
100
|
+
# @param id [String] the Blob ID.
|
|
101
|
+
# @param blob_path [String] path to the replacement binary on disk.
|
|
102
|
+
# @return [Hash] the parsed JSON response from the patch.
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# AtlasRb::Blob.update("b-321", "/tmp/revised.pdf")
|
|
25
106
|
def self.update(id, blob_path)
|
|
26
107
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(blob_path),
|
|
27
108
|
"application/octet-stream",
|
data/lib/atlas_rb/collection.rb
CHANGED
|
@@ -1,13 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# A grouping of {Work}s within a {Community}.
|
|
5
|
+
#
|
|
6
|
+
# Collections are the leaf containers in the organizational tree — they hold
|
|
7
|
+
# Works directly and cannot contain other Collections. Every Collection has
|
|
8
|
+
# exactly one parent Community.
|
|
9
|
+
#
|
|
10
|
+
# See also: {Community}, {Work}.
|
|
4
11
|
class Collection < Resource
|
|
12
|
+
# Atlas REST endpoint prefix for this resource.
|
|
13
|
+
# @api private
|
|
5
14
|
ROUTE = "/collections/"
|
|
6
15
|
|
|
16
|
+
# Fetch a single Collection by ID.
|
|
17
|
+
#
|
|
18
|
+
# @param id [String] the Collection ID.
|
|
19
|
+
# @return [Hash] the `"collection"` object, already unwrapped from the
|
|
20
|
+
# JSON response.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# AtlasRb::Collection.find("col-456")
|
|
24
|
+
# # => { "id" => "col-456", "title" => "Faculty Publications", ... }
|
|
7
25
|
def self.find(id)
|
|
8
26
|
JSON.parse(connection({}).get(ROUTE + id)&.body)["collection"]
|
|
9
27
|
end
|
|
10
28
|
|
|
29
|
+
# Create a new Collection under an existing Community.
|
|
30
|
+
#
|
|
31
|
+
# **Note**: unlike {Community.create}, the `id` parameter here is the
|
|
32
|
+
# parent **Community** ID (not a parent Collection ID — Collections do
|
|
33
|
+
# not nest).
|
|
34
|
+
#
|
|
35
|
+
# @param id [String] the parent Community ID.
|
|
36
|
+
# @param xml_path [String, nil] optional path to a MODS XML file used to
|
|
37
|
+
# seed metadata. When given, the Collection is created and immediately
|
|
38
|
+
# patched with the metadata in the file.
|
|
39
|
+
# @return [Hash] the created Collection payload (post-update if
|
|
40
|
+
# `xml_path` was supplied).
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# AtlasRb::Collection.create("c-123", "/tmp/collection-mods.xml")
|
|
11
44
|
def self.create(id, xml_path = nil)
|
|
12
45
|
result = JSON.parse(connection({ parent_id: id }).post(ROUTE)&.body)["collection"]
|
|
13
46
|
return result unless xml_path.present?
|
|
@@ -16,14 +49,36 @@ module AtlasRb
|
|
|
16
49
|
find(result["id"])
|
|
17
50
|
end
|
|
18
51
|
|
|
52
|
+
# Delete a Collection.
|
|
53
|
+
#
|
|
54
|
+
# @param id [String] the Collection ID.
|
|
55
|
+
# @return [Faraday::Response] the raw delete response.
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# AtlasRb::Collection.destroy("col-456")
|
|
19
59
|
def self.destroy(id)
|
|
20
60
|
connection({}).delete(ROUTE + id)
|
|
21
61
|
end
|
|
22
62
|
|
|
63
|
+
# List the Works in a Collection.
|
|
64
|
+
#
|
|
65
|
+
# @param id [String] the Collection ID.
|
|
66
|
+
# @return [Hash] the child listing from `GET /collections/<id>/children`.
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# AtlasRb::Collection.children("col-456")
|
|
23
70
|
def self.children(id)
|
|
24
71
|
JSON.parse(connection({}).get(ROUTE + id + '/children')&.body)
|
|
25
72
|
end
|
|
26
73
|
|
|
74
|
+
# Replace a Collection's metadata by uploading a MODS XML document.
|
|
75
|
+
#
|
|
76
|
+
# @param id [String] the Collection ID.
|
|
77
|
+
# @param xml_path [String] path to a MODS XML file on disk.
|
|
78
|
+
# @return [Hash] the parsed JSON response from the patch.
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# AtlasRb::Collection.update("col-456", "/tmp/collection-mods.xml")
|
|
27
82
|
def self.update(id, xml_path)
|
|
28
83
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
|
|
29
84
|
"application/xml",
|
|
@@ -31,10 +86,27 @@ module AtlasRb
|
|
|
31
86
|
JSON.parse(multipart({}).patch(ROUTE + id, payload)&.body)
|
|
32
87
|
end
|
|
33
88
|
|
|
89
|
+
# Patch individual metadata fields without uploading a full MODS document.
|
|
90
|
+
#
|
|
91
|
+
# @param id [String] the Collection ID.
|
|
92
|
+
# @param values [Hash] field-level metadata updates.
|
|
93
|
+
# @return [Hash] the parsed JSON response.
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# AtlasRb::Collection.metadata("col-456", title: "Renamed Collection")
|
|
34
97
|
def self.metadata(id, values)
|
|
35
98
|
JSON.parse(connection({ metadata: values }).patch(ROUTE + id)&.body)
|
|
36
99
|
end
|
|
37
100
|
|
|
101
|
+
# Fetch the Collection's MODS representation in the requested format.
|
|
102
|
+
#
|
|
103
|
+
# @param id [String] the Collection ID.
|
|
104
|
+
# @param kind [String, nil] one of `"json"` (default), `"html"`, or
|
|
105
|
+
# `"xml"`.
|
|
106
|
+
# @return [String] the raw response body in the requested format.
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# AtlasRb::Collection.mods("col-456", "xml")
|
|
38
110
|
def self.mods(id, kind = nil)
|
|
39
111
|
# json default, html, xml
|
|
40
112
|
connection({}).get(
|
data/lib/atlas_rb/community.rb
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# A top-level grouping in the Atlas hierarchy.
|
|
5
|
+
#
|
|
6
|
+
# Communities are organizational containers — they hold {Collection}s and,
|
|
7
|
+
# optionally, sub-Communities. Most institutional structure (departments,
|
|
8
|
+
# programs, projects) is modeled as a tree of Communities with Collections
|
|
9
|
+
# at the leaves.
|
|
10
|
+
#
|
|
11
|
+
# See also: {Collection}, {Work}.
|
|
4
12
|
class Community < Resource
|
|
13
|
+
# Atlas REST endpoint prefix for this resource.
|
|
14
|
+
# @api private
|
|
5
15
|
ROUTE = "/communities/"
|
|
6
16
|
|
|
17
|
+
# Fetch a single Community by ID.
|
|
18
|
+
#
|
|
19
|
+
# @param id [String] the Community ID.
|
|
20
|
+
# @return [Hash] the `"community"` object from the JSON response,
|
|
21
|
+
# already unwrapped.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# AtlasRb::Community.find("c-123")
|
|
25
|
+
# # => { "id" => "c-123", "title" => "College of Engineering", ... }
|
|
7
26
|
def self.find(id)
|
|
8
27
|
JSON.parse(connection({}).get(ROUTE + id)&.body)["community"]
|
|
9
28
|
end
|
|
10
29
|
|
|
30
|
+
# Create a new Community, optionally seeded with MODS metadata.
|
|
31
|
+
#
|
|
32
|
+
# Pass `id = nil` to create a top-level Community; pass a Community ID to
|
|
33
|
+
# nest the new Community beneath an existing one.
|
|
34
|
+
#
|
|
35
|
+
# @param id [String, nil] the parent Community ID, or `nil` for a
|
|
36
|
+
# top-level Community.
|
|
37
|
+
# @param xml_path [String, nil] optional path to a MODS XML file. When
|
|
38
|
+
# given, the Community is created and immediately patched with the
|
|
39
|
+
# metadata in the file; the returned Hash reflects the patched state.
|
|
40
|
+
# @return [Hash] the created Community payload (post-update if `xml_path`
|
|
41
|
+
# was supplied).
|
|
42
|
+
#
|
|
43
|
+
# @example Top-level community, no metadata
|
|
44
|
+
# AtlasRb::Community.create(nil)
|
|
45
|
+
#
|
|
46
|
+
# @example Sub-community seeded from MODS
|
|
47
|
+
# AtlasRb::Community.create("c-parent", "/tmp/dept-mods.xml")
|
|
11
48
|
def self.create(id = nil, xml_path = nil)
|
|
12
49
|
result = JSON.parse(connection({ parent_id: id }).post(ROUTE)&.body)["community"]
|
|
13
50
|
return result unless xml_path.present?
|
|
@@ -16,14 +53,36 @@ module AtlasRb
|
|
|
16
53
|
find(result["id"])
|
|
17
54
|
end
|
|
18
55
|
|
|
56
|
+
# Delete a Community.
|
|
57
|
+
#
|
|
58
|
+
# @param id [String] the Community ID.
|
|
59
|
+
# @return [Faraday::Response] the raw delete response.
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# AtlasRb::Community.destroy("c-123")
|
|
19
63
|
def self.destroy(id)
|
|
20
64
|
connection({}).delete(ROUTE + id)
|
|
21
65
|
end
|
|
22
66
|
|
|
67
|
+
# List the immediate children (sub-Communities and Collections) of a Community.
|
|
68
|
+
#
|
|
69
|
+
# @param id [String] the parent Community ID.
|
|
70
|
+
# @return [Hash] the child listing from `GET /communities/<id>/children`.
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# AtlasRb::Community.children("c-123")
|
|
23
74
|
def self.children(id)
|
|
24
75
|
JSON.parse(connection({}).get(ROUTE + id + '/children')&.body)
|
|
25
76
|
end
|
|
26
77
|
|
|
78
|
+
# Replace a Community's metadata by uploading a MODS XML document.
|
|
79
|
+
#
|
|
80
|
+
# @param id [String] the Community ID.
|
|
81
|
+
# @param xml_path [String] path to a MODS XML file on disk.
|
|
82
|
+
# @return [Hash] the parsed JSON response from the patch.
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# AtlasRb::Community.update("c-123", "/tmp/community-mods.xml")
|
|
27
86
|
def self.update(id, xml_path)
|
|
28
87
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
|
|
29
88
|
"application/xml",
|
|
@@ -31,10 +90,30 @@ module AtlasRb
|
|
|
31
90
|
JSON.parse(multipart({}).patch(ROUTE + id, payload)&.body)
|
|
32
91
|
end
|
|
33
92
|
|
|
93
|
+
# Patch individual metadata fields without uploading a full MODS document.
|
|
94
|
+
#
|
|
95
|
+
# @param id [String] the Community ID.
|
|
96
|
+
# @param values [Hash] field-level metadata updates (shape determined by
|
|
97
|
+
# the Atlas server, typically a mapping from MODS field name to value).
|
|
98
|
+
# @return [Hash] the parsed JSON response.
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# AtlasRb::Community.metadata("c-123", title: "New Name")
|
|
34
102
|
def self.metadata(id, values)
|
|
35
103
|
JSON.parse(connection({ metadata: values }).patch(ROUTE + id)&.body)
|
|
36
104
|
end
|
|
37
105
|
|
|
106
|
+
# Fetch the Community's MODS representation in the requested format.
|
|
107
|
+
#
|
|
108
|
+
# @param id [String] the Community ID.
|
|
109
|
+
# @param kind [String, nil] one of `"json"` (default when omitted),
|
|
110
|
+
# `"html"`, or `"xml"`. When `nil`, Atlas returns its default
|
|
111
|
+
# representation.
|
|
112
|
+
# @return [String] the raw response body (JSON, HTML, or XML serialized
|
|
113
|
+
# as a string).
|
|
114
|
+
#
|
|
115
|
+
# @example HTML rendering for display
|
|
116
|
+
# AtlasRb::Community.mods("c-123", "html")
|
|
38
117
|
def self.mods(id, kind = nil)
|
|
39
118
|
# json default, html, xml
|
|
40
119
|
connection({}).get(
|
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# HTTP transport helpers shared by every resource class.
|
|
5
|
+
#
|
|
6
|
+
# Every Atlas request reads two environment variables:
|
|
7
|
+
#
|
|
8
|
+
# - `ATLAS_URL` — base URL of the Atlas API (e.g. `https://atlas.example.edu`).
|
|
9
|
+
# - `ATLAS_TOKEN` — bearer token used in the `Authorization` header.
|
|
10
|
+
#
|
|
11
|
+
# Most calls also identify the acting user via a `User: NUID <nuid>` header.
|
|
12
|
+
# Resource classes typically pass `nuid = nil` (anonymous / system context);
|
|
13
|
+
# {AtlasRb::Authentication} is the only place where a real NUID is currently
|
|
14
|
+
# threaded through.
|
|
15
|
+
#
|
|
16
|
+
# The module is mixed in via `extend`, so its methods become class methods on
|
|
17
|
+
# the host (e.g. `AtlasRb::Work.connection({})`).
|
|
4
18
|
module FaradayHelper
|
|
19
|
+
# Build a JSON-content Faraday connection to the Atlas API.
|
|
20
|
+
#
|
|
21
|
+
# @param params [Hash] query-string / body params to attach to the request.
|
|
22
|
+
# Resource classes use this to pass things like `parent_id:`, `work_id:`,
|
|
23
|
+
# or `metadata:` without manually serializing.
|
|
24
|
+
# @param nuid [String, nil] optional Northeastern University ID to send in
|
|
25
|
+
# the `User` header. Defaults to `nil` (no NUID context).
|
|
26
|
+
# @return [Faraday::Connection] a connection that follows redirects and
|
|
27
|
+
# uses Faraday's default adapter.
|
|
28
|
+
#
|
|
29
|
+
# @example Fetching a community
|
|
30
|
+
# AtlasRb::Community.connection({}).get('/communities/abc123')
|
|
5
31
|
def connection(params, nuid=nil)
|
|
6
32
|
Faraday.new(
|
|
7
33
|
url: ENV.fetch("ATLAS_URL", nil),
|
|
@@ -17,6 +43,24 @@ module AtlasRb
|
|
|
17
43
|
end
|
|
18
44
|
end
|
|
19
45
|
|
|
46
|
+
# Build a multipart Faraday connection used for binary and XML uploads.
|
|
47
|
+
#
|
|
48
|
+
# The same `ATLAS_URL` / `ATLAS_TOKEN` env vars apply. Unlike {#connection},
|
|
49
|
+
# the `Content-Type` is set automatically by the multipart middleware, and
|
|
50
|
+
# callers pass a payload hash whose values may include
|
|
51
|
+
# `Faraday::Multipart::FilePart` instances.
|
|
52
|
+
#
|
|
53
|
+
# @param nuid [String, nil] optional NUID for the `User` header.
|
|
54
|
+
# @return [Faraday::Connection] a multipart-aware connection.
|
|
55
|
+
#
|
|
56
|
+
# @example Posting a binary blob
|
|
57
|
+
# payload = {
|
|
58
|
+
# work_id: "w-123",
|
|
59
|
+
# binary: Faraday::Multipart::FilePart.new(File.open("scan.pdf"),
|
|
60
|
+
# "application/octet-stream",
|
|
61
|
+
# "scan.pdf")
|
|
62
|
+
# }
|
|
63
|
+
# AtlasRb::Blob.multipart({}).post('/files/', payload)
|
|
20
64
|
def multipart(nuid=nil)
|
|
21
65
|
Faraday.new(
|
|
22
66
|
url: ENV.fetch("ATLAS_URL", nil),
|
data/lib/atlas_rb/file_set.rb
CHANGED
|
@@ -1,21 +1,70 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# An ordered, classified slot under a {Work} that holds a {Blob}.
|
|
5
|
+
#
|
|
6
|
+
# FileSets give a Work multiple distinct files (e.g. a primary PDF, a
|
|
7
|
+
# supplemental dataset, a thumbnail) and tag each with a `classification`
|
|
8
|
+
# so the UI knows how to display it. The actual binary content lives on
|
|
9
|
+
# the associated {Blob}.
|
|
10
|
+
#
|
|
11
|
+
# See also: {Work}, {Blob}.
|
|
4
12
|
class FileSet < Resource
|
|
13
|
+
# Atlas REST endpoint prefix for this resource.
|
|
14
|
+
# @api private
|
|
5
15
|
ROUTE = "/file_sets/"
|
|
6
16
|
|
|
17
|
+
# Fetch a single FileSet by ID.
|
|
18
|
+
#
|
|
19
|
+
# @param id [String] the FileSet ID.
|
|
20
|
+
# @return [Hash] the `"file_set"` object, already unwrapped.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# AtlasRb::FileSet.find("fs-001")
|
|
7
24
|
def self.find(id)
|
|
8
25
|
JSON.parse(connection({}).get(ROUTE + id)&.body)["file_set"]
|
|
9
26
|
end
|
|
10
27
|
|
|
28
|
+
# Create a new FileSet under a Work.
|
|
29
|
+
#
|
|
30
|
+
# @param id [String] the parent Work ID.
|
|
31
|
+
# @param classification [String] role tag for the FileSet — e.g.
|
|
32
|
+
# `"primary"`, `"supplemental"`, `"thumbnail"`. The exact set is
|
|
33
|
+
# defined by the Atlas server.
|
|
34
|
+
# @return [Hash] the created `"file_set"` payload, including its `"id"`
|
|
35
|
+
# which can then be passed to {.update} to attach a binary.
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# fs = AtlasRb::FileSet.create("w-789", "primary")
|
|
39
|
+
# AtlasRb::FileSet.update(fs["id"], "/tmp/article.pdf")
|
|
11
40
|
def self.create(id, classification)
|
|
12
41
|
JSON.parse(connection({ work_id: id, classification: classification }).post(ROUTE)&.body)["file_set"]
|
|
13
42
|
end
|
|
14
43
|
|
|
44
|
+
# Delete a FileSet.
|
|
45
|
+
#
|
|
46
|
+
# @param id [String] the FileSet ID.
|
|
47
|
+
# @return [Faraday::Response] the raw delete response.
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# AtlasRb::FileSet.destroy("fs-001")
|
|
15
51
|
def self.destroy(id)
|
|
16
52
|
connection({}).delete(ROUTE + id)
|
|
17
53
|
end
|
|
18
54
|
|
|
55
|
+
# Attach (or replace) the binary content backing this FileSet.
|
|
56
|
+
#
|
|
57
|
+
# The body is uploaded as `application/octet-stream` regardless of the
|
|
58
|
+
# file's true type — Atlas inspects the content server-side. To upload
|
|
59
|
+
# a binary blob *plus* an original filename, use {Blob.create} directly
|
|
60
|
+
# against the underlying `/files/` endpoint.
|
|
61
|
+
#
|
|
62
|
+
# @param id [String] the FileSet ID.
|
|
63
|
+
# @param blob_path [String] path to the binary file on disk.
|
|
64
|
+
# @return [Hash] the parsed JSON response from the patch.
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# AtlasRb::FileSet.update("fs-001", "/tmp/article.pdf")
|
|
19
68
|
def self.update(id, blob_path)
|
|
20
69
|
# Need to figure out blob vs XML
|
|
21
70
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(blob_path),
|
data/lib/atlas_rb/resource.rb
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# Abstract base for every Atlas resource type.
|
|
5
|
+
#
|
|
6
|
+
# Subclasses define a `ROUTE` constant (e.g. `"/communities/"`) and override
|
|
7
|
+
# whichever of `find / create / destroy / update / metadata / mods` apply.
|
|
8
|
+
# The {Resource} class itself ships three endpoints that are not
|
|
9
|
+
# type-specific: a generic resolver, an XML preview helper, and a
|
|
10
|
+
# permissions lookup.
|
|
11
|
+
#
|
|
12
|
+
# The Atlas resource hierarchy is:
|
|
13
|
+
#
|
|
14
|
+
# {Community} → {Collection} → {Work} → {FileSet} → {Blob}
|
|
15
|
+
#
|
|
16
|
+
# Subclasses extend {FaradayHelper} so that `connection(...)` and
|
|
17
|
+
# `multipart(...)` are available as class methods.
|
|
4
18
|
class Resource
|
|
5
19
|
extend AtlasRb::FaradayHelper
|
|
6
20
|
|
|
21
|
+
# Resolve any Atlas resource by ID without knowing its type up front.
|
|
22
|
+
#
|
|
23
|
+
# The Atlas server returns a single-key JSON object whose key names the
|
|
24
|
+
# resource type (`"community"`, `"collection"`, `"work"`, etc.); this
|
|
25
|
+
# method splits that into a normalized `{ "klass" => ..., "resource" => ... }`
|
|
26
|
+
# pair so callers can dispatch on type.
|
|
27
|
+
#
|
|
28
|
+
# @param id [String] an Atlas resource ID of any type.
|
|
29
|
+
# @return [Hash{String => String, Hash}] hash with two keys:
|
|
30
|
+
# - `"klass"` — the resource type, capitalized (e.g. `"Work"`).
|
|
31
|
+
# - `"resource"` — the resource payload as a Hash.
|
|
32
|
+
#
|
|
33
|
+
# @example Polymorphic lookup
|
|
34
|
+
# AtlasRb::Resource.find("abc123")
|
|
35
|
+
# # => { "klass" => "Work", "resource" => { "id" => "abc123", "title" => "..." } }
|
|
7
36
|
def self.find(id)
|
|
8
37
|
result = JSON.parse(connection({}).get('/resources/' + id)&.body)
|
|
9
38
|
{ "klass" => result.first[0].capitalize,
|
|
10
39
|
"resource" => result.first[1] }
|
|
11
40
|
end
|
|
12
41
|
|
|
42
|
+
# Validate a MODS XML document against Atlas's schema *without* persisting it.
|
|
43
|
+
#
|
|
44
|
+
# Useful for surfacing validation errors in UIs before the user commits.
|
|
45
|
+
#
|
|
46
|
+
# @param xml_path [String] path to a MODS XML file on disk.
|
|
47
|
+
# @return [String] the raw response body from `POST /resources/preview`
|
|
48
|
+
# — typically a JSON or XML error report.
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# AtlasRb::Resource.preview("/tmp/draft-mods.xml")
|
|
13
52
|
def self.preview(xml_path)
|
|
14
53
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
|
|
15
54
|
"application/xml",
|
|
@@ -17,6 +56,15 @@ module AtlasRb
|
|
|
17
56
|
multipart({}).post('/resources/preview', payload)&.body
|
|
18
57
|
end
|
|
19
58
|
|
|
59
|
+
# Fetch the access-control entries for a resource.
|
|
60
|
+
#
|
|
61
|
+
# @param id [String] an Atlas resource ID.
|
|
62
|
+
# @return [Hash] the `"resource"` payload from `GET /resources/<id>/permissions`,
|
|
63
|
+
# typically containing read/write/admin grant lists.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# AtlasRb::Resource.permissions("abc123")
|
|
67
|
+
# # => { "id" => "abc123", "read" => [...], "write" => [...] }
|
|
20
68
|
def self.permissions(id)
|
|
21
69
|
result = JSON.parse(connection({}).get('/resources/' + id + '/permissions')&.body)["resource"]
|
|
22
70
|
end
|
data/lib/atlas_rb/version.rb
CHANGED
data/lib/atlas_rb/work.rb
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
+
# The bibliographic unit in Atlas — an article, thesis, dataset, image, etc.
|
|
5
|
+
#
|
|
6
|
+
# A Work belongs to exactly one {Collection} and aggregates one or more
|
|
7
|
+
# {FileSet}s, each of which holds binary content via a {Blob}. MODS metadata
|
|
8
|
+
# is attached at the Work level.
|
|
9
|
+
#
|
|
10
|
+
# See also: {Collection}, {FileSet}, {Blob}.
|
|
4
11
|
class Work < Resource
|
|
12
|
+
# Atlas REST endpoint prefix for this resource.
|
|
13
|
+
# @api private
|
|
5
14
|
ROUTE = "/works/"
|
|
6
15
|
|
|
16
|
+
# Fetch a single Work by ID.
|
|
17
|
+
#
|
|
18
|
+
# @param id [String] the Work ID.
|
|
19
|
+
# @return [Hash] the `"work"` object, already unwrapped from the JSON
|
|
20
|
+
# response.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# AtlasRb::Work.find("w-789")
|
|
24
|
+
# # => { "id" => "w-789", "title" => "An Article", ... }
|
|
7
25
|
def self.find(id)
|
|
8
26
|
JSON.parse(connection({}).get(ROUTE + id)&.body)["work"]
|
|
9
27
|
end
|
|
10
28
|
|
|
29
|
+
# Create a new Work in an existing Collection.
|
|
30
|
+
#
|
|
31
|
+
# **Note**: unlike {Community.create} and {Collection.create}, the `id`
|
|
32
|
+
# parameter here is the parent **Collection** ID. The underlying request
|
|
33
|
+
# uses the `collection_id` query param rather than `parent_id`.
|
|
34
|
+
#
|
|
35
|
+
# @param id [String] the parent Collection ID.
|
|
36
|
+
# @param xml_path [String, nil] optional path to a MODS XML file. When
|
|
37
|
+
# given, the Work is created and immediately patched with the metadata
|
|
38
|
+
# in the file.
|
|
39
|
+
# @return [Hash] the created Work payload (post-update if `xml_path` was
|
|
40
|
+
# supplied).
|
|
41
|
+
#
|
|
42
|
+
# @example Empty work, metadata to be added later
|
|
43
|
+
# AtlasRb::Work.create("col-456")
|
|
44
|
+
#
|
|
45
|
+
# @example Work seeded from MODS
|
|
46
|
+
# AtlasRb::Work.create("col-456", "/tmp/work-mods.xml")
|
|
11
47
|
def self.create(id, xml_path = nil)
|
|
12
48
|
result = JSON.parse(connection({ collection_id: id }).post(ROUTE)&.body)["work"]
|
|
13
49
|
return result unless xml_path.present?
|
|
@@ -16,10 +52,25 @@ module AtlasRb
|
|
|
16
52
|
find(result["id"])
|
|
17
53
|
end
|
|
18
54
|
|
|
55
|
+
# Delete a Work.
|
|
56
|
+
#
|
|
57
|
+
# @param id [String] the Work ID.
|
|
58
|
+
# @return [Faraday::Response] the raw delete response.
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# AtlasRb::Work.destroy("w-789")
|
|
19
62
|
def self.destroy(id)
|
|
20
63
|
connection({}).delete(ROUTE + id)
|
|
21
64
|
end
|
|
22
65
|
|
|
66
|
+
# Replace a Work's metadata by uploading a MODS XML document.
|
|
67
|
+
#
|
|
68
|
+
# @param id [String] the Work ID.
|
|
69
|
+
# @param xml_path [String] path to a MODS XML file on disk.
|
|
70
|
+
# @return [Hash] the parsed JSON response from the patch.
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# AtlasRb::Work.update("w-789", "/tmp/work-mods.xml")
|
|
23
74
|
def self.update(id, xml_path)
|
|
24
75
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
|
|
25
76
|
"application/xml",
|
|
@@ -27,14 +78,41 @@ module AtlasRb
|
|
|
27
78
|
JSON.parse(multipart({}).patch(ROUTE + id, payload)&.body)
|
|
28
79
|
end
|
|
29
80
|
|
|
81
|
+
# Patch individual metadata fields without uploading a full MODS document.
|
|
82
|
+
#
|
|
83
|
+
# @param id [String] the Work ID.
|
|
84
|
+
# @param values [Hash] field-level metadata updates.
|
|
85
|
+
# @return [Hash] the parsed JSON response.
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# AtlasRb::Work.metadata("w-789", title: "Revised Title")
|
|
30
89
|
def self.metadata(id, values)
|
|
31
90
|
JSON.parse(connection({ metadata: values }).patch(ROUTE + id)&.body)
|
|
32
91
|
end
|
|
33
92
|
|
|
93
|
+
# List the {FileSet}s and {Blob}s attached to a Work.
|
|
94
|
+
#
|
|
95
|
+
# Useful for building download UIs — the response includes enough to
|
|
96
|
+
# render each file's display name, size, and download URL.
|
|
97
|
+
#
|
|
98
|
+
# @param id [String] the Work ID.
|
|
99
|
+
# @return [Hash] the listing from `GET /works/<id>/files`.
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# AtlasRb::Work.files("w-789")
|
|
34
103
|
def self.files(id)
|
|
35
104
|
JSON.parse(connection({}).get(ROUTE + id + '/files')&.body)
|
|
36
105
|
end
|
|
37
106
|
|
|
107
|
+
# Fetch the Work's MODS representation in the requested format.
|
|
108
|
+
#
|
|
109
|
+
# @param id [String] the Work ID.
|
|
110
|
+
# @param kind [String, nil] one of `"json"` (default), `"html"`, or
|
|
111
|
+
# `"xml"`.
|
|
112
|
+
# @return [String] the raw response body in the requested format.
|
|
113
|
+
#
|
|
114
|
+
# @example
|
|
115
|
+
# AtlasRb::Work.mods("w-789", "html")
|
|
38
116
|
def self.mods(id, kind = nil)
|
|
39
117
|
# json default, html, xml
|
|
40
118
|
connection({}).get(
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -13,13 +13,73 @@ require_relative "atlas_rb/work"
|
|
|
13
13
|
require_relative "atlas_rb/file_set"
|
|
14
14
|
require_relative "atlas_rb/blob"
|
|
15
15
|
|
|
16
|
+
# Ruby client for the Atlas API — Northeastern University's institutional
|
|
17
|
+
# digital repository (the successor to Cerberus).
|
|
18
|
+
#
|
|
19
|
+
# ## Configuration
|
|
20
|
+
#
|
|
21
|
+
# Two environment variables drive every request:
|
|
22
|
+
#
|
|
23
|
+
# - `ATLAS_URL` — base URL of the Atlas API (e.g. `https://atlas.example.edu`).
|
|
24
|
+
# - `ATLAS_TOKEN` — bearer token sent in the `Authorization` header.
|
|
25
|
+
#
|
|
26
|
+
# {AtlasRb::Authentication} additionally accepts an NUID (Northeastern
|
|
27
|
+
# University ID) which is forwarded in a `User: NUID <nuid>` header so the
|
|
28
|
+
# server can resolve the acting user.
|
|
29
|
+
#
|
|
30
|
+
# ## Resource hierarchy
|
|
31
|
+
#
|
|
32
|
+
# {AtlasRb::Community} → {AtlasRb::Collection} → {AtlasRb::Work}
|
|
33
|
+
# ↓
|
|
34
|
+
# {AtlasRb::FileSet}
|
|
35
|
+
# ↓
|
|
36
|
+
# {AtlasRb::Blob}
|
|
37
|
+
#
|
|
38
|
+
# - **Community** — top-level org unit; may nest sub-Communities.
|
|
39
|
+
# - **Collection** — holds Works; lives directly under a Community.
|
|
40
|
+
# - **Work** — the bibliographic unit (article, thesis, dataset…); MODS
|
|
41
|
+
# metadata is attached here.
|
|
42
|
+
# - **FileSet** — classified slot under a Work that owns one Blob.
|
|
43
|
+
# - **Blob** — the binary bytes themselves; supports streaming downloads
|
|
44
|
+
# via {AtlasRb::Blob.content}.
|
|
45
|
+
#
|
|
46
|
+
# ## Quick start
|
|
47
|
+
#
|
|
48
|
+
# @example End-to-end: create a Work and attach a file
|
|
49
|
+
# ENV["ATLAS_URL"] = "https://atlas.example.edu"
|
|
50
|
+
# ENV["ATLAS_TOKEN"] = "..."
|
|
51
|
+
#
|
|
52
|
+
# community = AtlasRb::Community.create(nil, "/tmp/community-mods.xml")
|
|
53
|
+
# collection = AtlasRb::Collection.create(community["id"], "/tmp/coll-mods.xml")
|
|
54
|
+
# work = AtlasRb::Work.create(collection["id"], "/tmp/work-mods.xml")
|
|
55
|
+
# blob = AtlasRb::Blob.create(work["id"],
|
|
56
|
+
# "/tmp/upload.tmp",
|
|
57
|
+
# "thesis.pdf")
|
|
58
|
+
#
|
|
59
|
+
# @example Streaming a download
|
|
60
|
+
# File.open("out.pdf", "wb") do |f|
|
|
61
|
+
# AtlasRb::Blob.content(blob["id"]) { |chunk| f.write(chunk) }
|
|
62
|
+
# end
|
|
16
63
|
module AtlasRb
|
|
64
|
+
# Generic error raised by future code paths; not currently used by any
|
|
65
|
+
# resource class. Atlas errors today surface as raw `Faraday::Response`
|
|
66
|
+
# objects or `JSON::ParserError`s on malformed bodies.
|
|
17
67
|
class Error < StandardError; end
|
|
18
|
-
# Your code goes here...
|
|
19
68
|
|
|
69
|
+
# Test-environment helper that wipes Atlas state via `GET /reset`.
|
|
70
|
+
#
|
|
71
|
+
# **Do not call against production.** This exists so RSpec suites running
|
|
72
|
+
# against a disposable Atlas instance can return to a clean baseline
|
|
73
|
+
# between examples.
|
|
20
74
|
class Reset
|
|
21
75
|
extend AtlasRb::FaradayHelper
|
|
22
76
|
|
|
77
|
+
# Reset the connected Atlas instance to a clean state.
|
|
78
|
+
#
|
|
79
|
+
# @return [String, nil] the raw response body from `GET /reset`.
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# AtlasRb::Reset.clean
|
|
23
83
|
def self.clean
|
|
24
84
|
connection({}).get("/reset")&.body
|
|
25
85
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: atlas_rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.83
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '3.12'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: yard
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.9'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.9'
|
|
69
83
|
description:
|
|
70
84
|
email:
|
|
71
85
|
- david.g.cliff@gmail.com
|
|
@@ -77,6 +91,7 @@ files:
|
|
|
77
91
|
- ".rspec"
|
|
78
92
|
- ".rubocop.yml"
|
|
79
93
|
- ".version"
|
|
94
|
+
- ".yardopts"
|
|
80
95
|
- Gemfile
|
|
81
96
|
- Gemfile.lock
|
|
82
97
|
- LICENSE.txt
|