atlas_rb 1.15.0 → 1.16.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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +178 -0
- data/Gemfile.lock +5 -2
- data/README.md +88 -0
- data/lib/atlas_rb/authentication.rb +15 -8
- data/lib/atlas_rb/blob.rb +37 -15
- data/lib/atlas_rb/collection.rb +16 -8
- data/lib/atlas_rb/community.rb +16 -8
- data/lib/atlas_rb/compilation.rb +18 -8
- data/lib/atlas_rb/configuration.rb +50 -0
- data/lib/atlas_rb/faraday_helper.rb +210 -32
- data/lib/atlas_rb/maintenance.rb +9 -3
- data/lib/atlas_rb/middleware/raise_on_read_error.rb +84 -0
- data/lib/atlas_rb/person.rb +18 -8
- data/lib/atlas_rb/resource.rb +73 -75
- data/lib/atlas_rb/transport.rb +78 -7
- data/lib/atlas_rb/user.rb +33 -16
- data/lib/atlas_rb/work.rb +56 -28
- data/lib/atlas_rb.rb +10 -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: ed32d60a2b3c2a562ee64d095a266c6b45848122ce4d75a0201c6736aeb52c2f
|
|
4
|
+
data.tar.gz: 229e5722c7b2036e490df5c06a5ad7afe496164fe7105a2ee232b7cb64f5d970
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e63d081a6b2f67297e95e2feb361ed232e585b3c22a84b68c0d35dd0a816aae1cfca0106613ddc9f097644bd8b02a098285ce48f7ee930126102037c2dff808c
|
|
7
|
+
data.tar.gz: 70931eed6c2ceaf7fd901a07005df4dec7570a5f90649b0bfb277826f927922bfb3ad30f73a226254d8059fb1c82f4e114e11a4a7977ddd1ff7d7e291e846d8f
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.16.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,183 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.0
|
|
4
|
+
|
|
5
|
+
### Fixed — every read binding consults the HTTP status before it reads the body
|
|
6
|
+
|
|
7
|
+
Twenty-nine read bindings parsed or returned a response body without checking
|
|
8
|
+
the status. `Resource.fetch_resource` had enforced that contract for the typed
|
|
9
|
+
single-resource `find`s since 1.8.3, but a binding had to opt in by routing
|
|
10
|
+
through it, and the rest of the read surface never did. Four failure shapes
|
|
11
|
+
came of it:
|
|
12
|
+
|
|
13
|
+
| Binding shape | What the caller got on an error response |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `JSON.parse(body).map { Mash.new(...) }` | `NoMethodError: undefined method 'each_pair' for [...]:Array` — the error envelope iterated as pairs |
|
|
16
|
+
| `JSON.parse(body)["key"].map` | `NoMethodError ... for nil:NilClass`, raised far from the cause |
|
|
17
|
+
| `Mash.new(JSON.parse(body))` | **no error at all** — the envelope came back as if it were data |
|
|
18
|
+
| raw `resp.body` (the `mods` bindings) | **no error at all** — the error text came back as a `String`, ready to be rendered |
|
|
19
|
+
| any of the above, non-JSON body | `JSON::ParserError`, naming neither the status nor the endpoint |
|
|
20
|
+
|
|
21
|
+
Three of those were worse than a wrong error class:
|
|
22
|
+
|
|
23
|
+
- **`Work.mods`** (and `Collection.mods`, `Community.mods`, `Resource.mods`,
|
|
24
|
+
`Resource.mods_version`) returned Atlas's error body as a `String`. Atlas
|
|
25
|
+
renders MODS to HTML server-side, so a consumer marks that body HTML-safe
|
|
26
|
+
and writes it into its own page. Pointing `ATLAS_URL` at another service in
|
|
27
|
+
the stack produced that service's stack trace by this route.
|
|
28
|
+
- **`Maintenance.read`** failed open. A `401` parsed into a `Mash` with no
|
|
29
|
+
`read_only` key, so a host read "no maintenance window" precisely when it
|
|
30
|
+
could not read the flag — the opposite of the endpoint's contract, which is
|
|
31
|
+
that a client unable to read the window cannot honour it.
|
|
32
|
+
- **`Blob.version_content`** discarded the status entirely, so an error body
|
|
33
|
+
became the bytes of the file an admin had just downloaded.
|
|
34
|
+
|
|
35
|
+
The fix is one Faraday middleware plus one shared binding-level guard, rather
|
|
36
|
+
than twenty-nine near-identical call-site edits:
|
|
37
|
+
|
|
38
|
+
- **`Middleware::RaiseOnReadError`** raises `AtlasRb::ResourceError` on a
|
|
39
|
+
non-2xx read, at the transport, where a binding cannot forget it. Registered
|
|
40
|
+
on the `:json` and `:system` connections, ahead of the typed translators so
|
|
41
|
+
their `on_complete` still runs first — a maintenance `503` stays a
|
|
42
|
+
`ReadOnlyModeError`, a refused re-parent stays a `ForbiddenError`.
|
|
43
|
+
- **`FaradayHelper#read_body` / `#read_raw`** apply the same mapping where a
|
|
44
|
+
binding can see it, which keeps the guarantee true for a caller who has
|
|
45
|
+
stubbed the transport. Every read binding now funnels through one of them,
|
|
46
|
+
and `Resource.fetch_resource` delegates to `read_body`.
|
|
47
|
+
|
|
48
|
+
The mapping, uniform across the read surface:
|
|
49
|
+
|
|
50
|
+
| Atlas answers | Read binding |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `2xx` | the parsed body (or the raw body, for `mods`) |
|
|
53
|
+
| `404` | `nil` |
|
|
54
|
+
| `410` | the tombstone body |
|
|
55
|
+
| anything else | raises `AtlasRb::ResourceError`, carrying the status, verb, path and body |
|
|
56
|
+
|
|
57
|
+
**Two behaviour changes to note when upgrading.**
|
|
58
|
+
|
|
59
|
+
`404` now returns `nil` where a binding used to raise `JSON::ParserError` on
|
|
60
|
+
the empty body, and list reads return `nil` rather than `[]` — so a caller can
|
|
61
|
+
tell "no such container" from "an empty container", which mean different
|
|
62
|
+
things to a UI. Callers that iterate a list read need `&.each` or a nil check.
|
|
63
|
+
|
|
64
|
+
`Blob.version_content` now returns `{ status:, headers: }`, matching
|
|
65
|
+
`Blob.content`. Its status cannot be raised on: chunks reach the caller as
|
|
66
|
+
they arrive, so by the time the status is known an error body would already
|
|
67
|
+
have been streamed. A caller passing those chunks to an HTTP response must
|
|
68
|
+
consult the status, or an Atlas error page becomes the downloaded file.
|
|
69
|
+
|
|
70
|
+
`Reset.clean` raises where the env gate that guards `GET /reset` is closed,
|
|
71
|
+
instead of returning the refusal body. A test suite that opens with a reset and
|
|
72
|
+
then carries on against un-wiped state fails somewhere else entirely, so this
|
|
73
|
+
one has to be loud.
|
|
74
|
+
|
|
75
|
+
`Authentication.login` and `.groups` no longer document
|
|
76
|
+
`@raise [JSON::ParserError]`. An auth failure is a `401` and now raises
|
|
77
|
+
`ResourceError`; a host rescuing `JSON::ParserError` as a stand-in for "the
|
|
78
|
+
read failed" can drop it.
|
|
79
|
+
|
|
80
|
+
The streaming reads (`Blob.content`, `Blob.version_content`) are exempt from
|
|
81
|
+
the middleware, which is keyed off `on_data` — see the middleware's note on
|
|
82
|
+
why raising after the chunks have gone out is too late to help.
|
|
83
|
+
|
|
84
|
+
### Added — a request deadline and a bounded retry on every connection
|
|
85
|
+
|
|
86
|
+
`lib/` set no timeout anywhere, so `Net::HTTP`'s defaults applied: 60s open,
|
|
87
|
+
60s read. Worse, the pooled adapter's `max_retries = 1` made `Net::HTTP`
|
|
88
|
+
replay an idempotent GET after a read timeout, so a hung Atlas held a single
|
|
89
|
+
`GET` for **120 seconds**. A consumer that fans out four reads per request
|
|
90
|
+
thread parks that thread — and four sockets from a sixteen-socket pool — for
|
|
91
|
+
that whole time, which turns one degraded backend into a front-end outage. No
|
|
92
|
+
host can fix this from outside: it can rescue what the gem raises, but it
|
|
93
|
+
cannot impose a deadline on a socket the gem owns.
|
|
94
|
+
|
|
95
|
+
Four new configuration slots, following the `connection_pool_size` pattern
|
|
96
|
+
(optional, read at connection-build time):
|
|
97
|
+
|
|
98
|
+
| Slot | Default | Applies to |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `open_timeout` | `2` seconds | all three connection shapes |
|
|
101
|
+
| `read_timeout` | `10` seconds | `:json`, `:system` |
|
|
102
|
+
| `upload_read_timeout` | `nil` (uncapped) | `:multipart` |
|
|
103
|
+
| `read_retries` | `2` (three attempts) | `:json`, `:system` |
|
|
104
|
+
|
|
105
|
+
`read_timeout` is sized off the measured read path — the slowest single call
|
|
106
|
+
on a consumer's Work page was ~200ms and the whole eight-call page ~450ms — so
|
|
107
|
+
ten seconds is roughly fifty times the observed per-call cost. It is
|
|
108
|
+
`Net::HTTP`'s per-read deadline rather than a whole-response budget, so a
|
|
109
|
+
streaming download is unaffected as long as bytes keep arriving. Set a slot to
|
|
110
|
+
`false` to remove a deadline, or override one call:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
connection({}, nuid).get(path) { |req| req.options.timeout = 120 }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The multipart shape takes no read deadline and no retry: a multi-gigabyte
|
|
117
|
+
upload legitimately outlives any page-sized budget, and a partially-streamed
|
|
118
|
+
`POST` must not be replayed blindly — even under an `Idempotency-Key`, which
|
|
119
|
+
the transport cannot see.
|
|
120
|
+
|
|
121
|
+
**Retries now live in exactly one layer.** `faraday-retry ~> 2.4` is a new
|
|
122
|
+
runtime dependency, registered as the outermost handler on the `:json` and
|
|
123
|
+
`:system` connections, and `Net::HTTP`'s own `max_retries` drops from `1` to
|
|
124
|
+
`0`. The 1.15.0 reasoning for that `1` was sound — a server that closed an
|
|
125
|
+
idle pooled socket errors on the next write, and a replay is what turns that
|
|
126
|
+
into a reconnect — and the middleware covers the same case better:
|
|
127
|
+
`EOFError`, `Errno::ECONNRESET` and `Faraday::ConnectionFailed` are all in its
|
|
128
|
+
exception list, now with jittered backoff, a bounded attempt count, and a
|
|
129
|
+
`request.atlas_rb` notification per attempt instead of a silent replay.
|
|
130
|
+
Stacking both layers would have given six timeout waits per call.
|
|
131
|
+
|
|
132
|
+
The policy is deliberately narrow:
|
|
133
|
+
|
|
134
|
+
- **Exceptions only** (`retry_statuses: []`). A response Atlas actually sent
|
|
135
|
+
is never retried. The maintenance `503` is why: its `Retry-After` is
|
|
136
|
+
measured in minutes, so an in-band retry would ignore it and hammer the
|
|
137
|
+
window, and `ReadOnlyModeError` must reach the caller on the first response.
|
|
138
|
+
- **`GET` / `HEAD` / `OPTIONS` only**, narrower than HTTP idempotency. `PUT`
|
|
139
|
+
and `DELETE` are idempotent in the spec, but Atlas's `DELETE` purges an OCFL
|
|
140
|
+
object and its `PATCH`/`PUT` writes carry optimistic-lock semantics. A
|
|
141
|
+
replay there should be a call site's decision.
|
|
142
|
+
- **`Faraday::ConnectionFailed` added explicitly.** It is not in the
|
|
143
|
+
middleware's own default list, and it is the failure that matters most here:
|
|
144
|
+
an Atlas restart mid-page-load is a refused connect, which one retry hides
|
|
145
|
+
completely.
|
|
146
|
+
|
|
147
|
+
Worst case per `GET` is now three attempts of ten seconds plus backoff, about
|
|
148
|
+
**30 seconds**, down from 120.
|
|
149
|
+
|
|
150
|
+
## 1.15.1
|
|
151
|
+
|
|
152
|
+
### Fixed — `Resource.permissions` no longer coerces a refused read to `nil`
|
|
153
|
+
|
|
154
|
+
Atlas gates `GET /resources/:id/permissions` on the caller's read right over
|
|
155
|
+
the resource itself, because the envelope names the Grouper groups and the
|
|
156
|
+
depositor's NUID. A caller who may not read the resource gets a real `403`
|
|
157
|
+
carrying `{ "error", "action", "subject" }`.
|
|
158
|
+
|
|
159
|
+
The binding parsed that body without consulting the status. The error envelope
|
|
160
|
+
has no `"resource"` key, so `JSON.parse(body)["resource"]` returned the same
|
|
161
|
+
`nil` an unknown id returns — the status, the message and the distinction were
|
|
162
|
+
all gone before the caller saw anything. A host reading this to drive its own
|
|
163
|
+
gate rendered "not found" for a resource the reader was merely not allowed to
|
|
164
|
+
see.
|
|
165
|
+
|
|
166
|
+
`permissions` now routes through `fetch_resource`, the guarded read path every
|
|
167
|
+
typed `find` already uses:
|
|
168
|
+
|
|
169
|
+
| Atlas answers | `permissions` |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `200` | the ACL `Mash` |
|
|
172
|
+
| `404` | `nil` — unchanged |
|
|
173
|
+
| `403` | raises `AtlasRb::ResourceError`, `status == 403` |
|
|
174
|
+
|
|
175
|
+
A host that wants the old "nil for anything unreadable" behaviour rescues
|
|
176
|
+
`AtlasRb::ResourceError` and returns `nil` itself.
|
|
177
|
+
|
|
178
|
+
The YARD example was also wrong: the envelope carries `type`, `depositor`,
|
|
179
|
+
`proxy_uploader`, `edit_users`, `read`, `edit` and `embargo`, never `id`.
|
|
180
|
+
|
|
3
181
|
## 1.15.0
|
|
4
182
|
|
|
5
183
|
### Changed — the transport reuses connections instead of opening one per request
|
data/Gemfile.lock
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
atlas_rb (1.
|
|
4
|
+
atlas_rb (1.16.0)
|
|
5
5
|
faraday (~> 2.7)
|
|
6
6
|
faraday-follow_redirects (~> 0.3.0)
|
|
7
7
|
faraday-multipart (~> 1)
|
|
8
8
|
faraday-net_http_persistent (~> 2.3)
|
|
9
|
+
faraday-retry (~> 2.4)
|
|
9
10
|
hashie (~> 5.0)
|
|
10
11
|
jwt (~> 2.7)
|
|
11
12
|
|
|
@@ -28,9 +29,11 @@ GEM
|
|
|
28
29
|
faraday-net_http_persistent (2.3.1)
|
|
29
30
|
faraday (~> 2.5)
|
|
30
31
|
net-http-persistent (>= 4.0.4, < 5)
|
|
32
|
+
faraday-retry (2.4.0)
|
|
33
|
+
faraday (~> 2.0)
|
|
31
34
|
hashie (5.1.0)
|
|
32
35
|
logger
|
|
33
|
-
json (
|
|
36
|
+
json (3.0.2)
|
|
34
37
|
jwt (2.10.3)
|
|
35
38
|
base64
|
|
36
39
|
logger (1.7.0)
|
data/README.md
CHANGED
|
@@ -192,6 +192,62 @@ pool between examples, or a socket from one example stays open into the next:
|
|
|
192
192
|
config.after(:each, :atlas_rb_server) { AtlasRb::Transport.reset_connections! }
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
### Deadlines and retries
|
|
196
|
+
|
|
197
|
+
Every connection the gem builds carries a deadline, because a host cannot
|
|
198
|
+
retrofit one: it can rescue what the gem raises, but it cannot impose a
|
|
199
|
+
timeout on a socket the gem owns. Left to `Net::HTTP`'s defaults a hung Atlas
|
|
200
|
+
held a single `GET` for two minutes, which parks a request thread and its
|
|
201
|
+
share of the socket pool for that whole time.
|
|
202
|
+
|
|
203
|
+
Four knobs, all optional:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
AtlasRb.configure do |config|
|
|
207
|
+
# Seconds to wait for a socket to open. Defaults to 2 — Atlas is on the same
|
|
208
|
+
# host or the same overlay network, so a healthy connect is sub-millisecond.
|
|
209
|
+
config.open_timeout = 2
|
|
210
|
+
|
|
211
|
+
# Seconds to wait for a response on the JSON and system connections.
|
|
212
|
+
# Defaults to 10. This is a per-socket-read deadline, not a whole-response
|
|
213
|
+
# budget, so a streaming download is fine as long as bytes keep arriving.
|
|
214
|
+
config.read_timeout = 10
|
|
215
|
+
|
|
216
|
+
# The same, for binary uploads. Defaults to nil — uncapped, because a
|
|
217
|
+
# multi-gigabyte upload has no defensible cap and those calls run in jobs.
|
|
218
|
+
config.upload_read_timeout = nil
|
|
219
|
+
|
|
220
|
+
# Extra attempts an idempotent read gets after a transport failure, so 2
|
|
221
|
+
# means three attempts in all. Defaults to 2; set 0 to disable.
|
|
222
|
+
config.read_retries = 2
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Set a timeout slot to `false` to remove the deadline rather than change it.
|
|
227
|
+
All four are read when a connection is first built, so set them before the
|
|
228
|
+
first Atlas call.
|
|
229
|
+
|
|
230
|
+
A single call that legitimately outlives the budget overrides it on the
|
|
231
|
+
request — the block the transport forwards runs last, so it wins:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
# A bulk export paging a large Collection, or a metadata dump:
|
|
235
|
+
AtlasRb::Work.connection({}, nuid).get("/resources/#{id}/descendant_works") do |req|
|
|
236
|
+
req.options.timeout = 120
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Only reads are retried, and only when the request failed in transport — a
|
|
241
|
+
refused or reset connection, or a timeout. A response Atlas actually sent is
|
|
242
|
+
never retried, which is what keeps the maintenance `503` honest: its
|
|
243
|
+
`Retry-After` is measured in minutes, so `ReadOnlyModeError` reaches you on
|
|
244
|
+
the first response instead of the gem waiting in band. Writes are never
|
|
245
|
+
retried; a `POST` that may have landed is the call site's decision, not the
|
|
246
|
+
transport's.
|
|
247
|
+
|
|
248
|
+
Each attempt emits its own `request.atlas_rb` notification, so a host counting
|
|
249
|
+
round-trips sees a retry as the second round-trip it really is.
|
|
250
|
+
|
|
195
251
|
## Resource hierarchy
|
|
196
252
|
|
|
197
253
|
```
|
|
@@ -507,6 +563,38 @@ question, and callers already nil-check a `find`:
|
|
|
507
563
|
AtlasRb::Work.find("doesnotexist") # => nil
|
|
508
564
|
```
|
|
509
565
|
|
|
566
|
+
That holds across the whole read surface, not just `find`. A **list** read
|
|
567
|
+
returns `nil` too, rather than an empty array, so you can tell "no such
|
|
568
|
+
container" from "an empty container" — two answers that mean different things
|
|
569
|
+
to a UI:
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
AtlasRb::Work.assets("doesnotexist") # => nil
|
|
573
|
+
AtlasRb::Work.assets(real_work_noid) # => [] for a Work with no assets
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Anything else a read gets back raises `AtlasRb::ResourceError`, which carries
|
|
577
|
+
the status, the endpoint and Atlas's body. That is the point: the failure is
|
|
578
|
+
attributable where it happened, instead of surfacing as a `NoMethodError` on
|
|
579
|
+
`nil` several frames later, or — for the `mods` bindings, which pass Atlas's
|
|
580
|
+
rendered body through by design — as an error page returned to you as a
|
|
581
|
+
`String` and rendered:
|
|
582
|
+
|
|
583
|
+
```ruby
|
|
584
|
+
begin
|
|
585
|
+
AtlasRb::Work.mods(noid, "html")
|
|
586
|
+
rescue AtlasRb::ResourceError => e
|
|
587
|
+
e.status # => 403
|
|
588
|
+
e.message # => "GET /works/9zw3s1h/mods.html → 403: {\"error\":\"forbidden\",…}"
|
|
589
|
+
end
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
A `410 Gone` is returned rather than raised, the same way `find` returns a
|
|
593
|
+
tombstone. And the two streaming reads (`Blob.content`,
|
|
594
|
+
`Blob.version_content`) hand you `{ status:, headers: }` instead of raising,
|
|
595
|
+
because their chunks reach you as they arrive — check the status before you
|
|
596
|
+
treat those bytes as a file.
|
|
597
|
+
|
|
510
598
|
A **write** raises `AtlasRb::NotFoundError`. The caller asked for a change and
|
|
511
599
|
did not get one, so `nil` would invite the silent failure the read guard was
|
|
512
600
|
written to prevent:
|
|
@@ -25,16 +25,19 @@ module AtlasRb
|
|
|
25
25
|
# @param nuid [String] the user's Northeastern University ID.
|
|
26
26
|
# @param email [String, nil] optional account email to act as (the `acct`
|
|
27
27
|
# selector); nil resolves the preferred account.
|
|
28
|
-
# @return [
|
|
29
|
-
# minimum `"id"`, `"name"`, and `"groups"`.
|
|
30
|
-
#
|
|
31
|
-
#
|
|
28
|
+
# @return [AtlasRb::Mash, nil] the user record returned by `GET /user`,
|
|
29
|
+
# including at minimum `"id"`, `"name"`, and `"groups"`.
|
|
30
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
31
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
32
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
33
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
34
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
32
35
|
#
|
|
33
36
|
# @example
|
|
34
37
|
# AtlasRb::Authentication.login("001234567")
|
|
35
38
|
# # => { "id" => 42, "name" => "Jane Doe", "groups" => [...] }
|
|
36
39
|
def self.login(nuid, email: nil)
|
|
37
|
-
|
|
40
|
+
read_body(connection({}, nuid, account: email).get('/user')) { |body| AtlasRb::Mash.new(body) }
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
# Fetch only the group memberships for an NUID.
|
|
@@ -43,8 +46,12 @@ module AtlasRb
|
|
|
43
46
|
# when authorization checks only need group names.
|
|
44
47
|
#
|
|
45
48
|
# @param nuid [String] the user's Northeastern University ID.
|
|
46
|
-
# @return [Array<Hash
|
|
47
|
-
#
|
|
49
|
+
# @return [Array<Hash>, nil] the `"groups"` array from the user record.
|
|
50
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
51
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
52
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
53
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
54
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
48
55
|
#
|
|
49
56
|
# @example
|
|
50
57
|
# AtlasRb::Authentication.groups("001234567")
|
|
@@ -54,7 +61,7 @@ module AtlasRb
|
|
|
54
61
|
# token = user_details[:token] ...
|
|
55
62
|
# TODO - need to update atlas login to give back name, id, and token upon logging in
|
|
56
63
|
# result = JSON.parse(connection({ token: token }).post('/users/2/groups')&.body)["user"]["groups"]
|
|
57
|
-
|
|
64
|
+
read_body(connection({}, nuid).get('/user')) { |body| AtlasRb::Mash.new(body)["groups"] }
|
|
58
65
|
end
|
|
59
66
|
end
|
|
60
67
|
end
|
data/lib/atlas_rb/blob.rb
CHANGED
|
@@ -57,16 +57,21 @@ module AtlasRb
|
|
|
57
57
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
58
58
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
59
59
|
# omitted.
|
|
60
|
-
# @return [AtlasRb::Mash] `{ "file_set" => "<noid>", "work" => "<noid>" }`
|
|
60
|
+
# @return [AtlasRb::Mash, nil] `{ "file_set" => "<noid>", "work" => "<noid>" }`
|
|
61
61
|
# (either value `nil` when unresolvable).
|
|
62
62
|
#
|
|
63
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
64
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
65
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
66
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
67
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
63
68
|
# @example
|
|
64
69
|
# AtlasRb::Blob.ancestry("b-321")
|
|
65
70
|
# # => { "file_set" => "fs-654", "work" => "w-789" }
|
|
66
71
|
def self.ancestry(id, nuid: nil, on_behalf_of: nil)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get("#{ROUTE}#{id}/ancestry")) do |body|
|
|
73
|
+
AtlasRb::Mash.new(body)
|
|
74
|
+
end
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
# Convenience over {.ancestry}: the containing Work's noid for a content
|
|
@@ -277,17 +282,22 @@ module AtlasRb
|
|
|
277
282
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
278
283
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
279
284
|
# omitted.
|
|
280
|
-
# @return [AtlasRb::Mash] the parsed envelope, with `"blob_id"` and a
|
|
285
|
+
# @return [AtlasRb::Mash, nil] the parsed envelope, with `"blob_id"` and a
|
|
281
286
|
# `"versions"` array (reverse chronological).
|
|
282
287
|
#
|
|
288
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
289
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
290
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
291
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
292
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
283
293
|
# @example
|
|
284
294
|
# history = AtlasRb::Blob.versions("b-321")
|
|
285
295
|
# history["versions"].first["version_id"] # => "v5"
|
|
286
296
|
# history["versions"].first["digest"] # => "sha512:9f86d0…"
|
|
287
297
|
def self.versions(id, nuid: nil, on_behalf_of: nil)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get("#{ROUTE}#{id}/versions")) do |body|
|
|
299
|
+
AtlasRb::Mash.new(body)
|
|
300
|
+
end
|
|
291
301
|
end
|
|
292
302
|
|
|
293
303
|
# Read binary version history for many Blobs in one round-trip.
|
|
@@ -316,20 +326,25 @@ module AtlasRb
|
|
|
316
326
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
317
327
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
318
328
|
# omitted.
|
|
319
|
-
# @return [Array<AtlasRb::Mash
|
|
329
|
+
# @return [Array<AtlasRb::Mash>, nil] one {.versions}-shaped envelope per resolved
|
|
320
330
|
# Blob (`"blob_id"` plus a reverse-chronological `"versions"` array); empty
|
|
321
331
|
# when none resolved.
|
|
322
332
|
#
|
|
333
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
334
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
335
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
336
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
337
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
323
338
|
# @example Render a Work's files with their histories in two calls
|
|
324
339
|
# assets = AtlasRb::Work.assets(work_noid).reject { |a| a[:uri].present? }
|
|
325
340
|
# history = AtlasRb::Blob.find_many_versions(assets.map(&:noid))
|
|
326
341
|
# .index_by { |h| h["blob_id"] }
|
|
327
342
|
# history[assets.first.noid]["versions"].first["revision"] # => 3
|
|
328
343
|
def self.find_many_versions(ids, nuid: nil, on_behalf_of: nil)
|
|
329
|
-
|
|
344
|
+
read_body(
|
|
330
345
|
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
331
|
-
.post("#{ROUTE}find_many_versions", JSON.dump(ids: Array(ids)))
|
|
332
|
-
).map { |envelope| AtlasRb::Mash.new(envelope) }
|
|
346
|
+
.post("#{ROUTE}find_many_versions", JSON.dump(ids: Array(ids)))
|
|
347
|
+
) { |body| body.map { |envelope| AtlasRb::Mash.new(envelope) } }
|
|
333
348
|
end
|
|
334
349
|
|
|
335
350
|
# Stream the bytes of a *prior* version of a Blob through a block.
|
|
@@ -353,7 +368,13 @@ module AtlasRb
|
|
|
353
368
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
354
369
|
# omitted.
|
|
355
370
|
# @yieldparam chunk [String] the next chunk of binary data.
|
|
356
|
-
# @return [Hash]
|
|
371
|
+
# @return [Hash] `{ status:, headers: }` — the HTTP status and response
|
|
372
|
+
# headers. The status is handed back rather than raised on, because
|
|
373
|
+
# chunks reach the caller as they arrive: by the time the status could
|
|
374
|
+
# be checked, an error body would already have been streamed. A caller
|
|
375
|
+
# writing the chunks to an HTTP response (or a file) must consult it, or
|
|
376
|
+
# an Atlas error page becomes the bytes of the downloaded file. Matches
|
|
377
|
+
# {.content}'s contract.
|
|
357
378
|
#
|
|
358
379
|
# @example Download a superseded version to disk
|
|
359
380
|
# File.open("/tmp/old.pdf", "wb") do |f|
|
|
@@ -361,13 +382,14 @@ module AtlasRb
|
|
|
361
382
|
# end
|
|
362
383
|
def self.version_content(id, version_id, nuid: nil, on_behalf_of: nil, &chunk_handler)
|
|
363
384
|
headers = {}
|
|
364
|
-
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
385
|
+
response = connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
386
|
+
.get("#{ROUTE}#{id}/versions/#{version_id}/content") do |req|
|
|
365
387
|
req.options.on_data = proc do |chunk, _bytes_received, env|
|
|
366
388
|
headers = env.response_headers if headers.empty? && env
|
|
367
389
|
chunk_handler.call(chunk)
|
|
368
390
|
end
|
|
369
391
|
end
|
|
370
|
-
headers
|
|
392
|
+
{ status: response.status, headers: headers }
|
|
371
393
|
end
|
|
372
394
|
|
|
373
395
|
# Roll a Blob back to a prior version.
|
data/lib/atlas_rb/collection.rb
CHANGED
|
@@ -196,15 +196,18 @@ module AtlasRb
|
|
|
196
196
|
# round-trip per child. For a whole subtree flattened to Works, use
|
|
197
197
|
# {AtlasRb::Resource.descendant_works}.
|
|
198
198
|
#
|
|
199
|
-
# @return [Array<String
|
|
199
|
+
# @return [Array<String>, nil] child noids from `GET /collections/<id>/children`.
|
|
200
200
|
#
|
|
201
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
202
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
203
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
204
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
205
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
201
206
|
# @example
|
|
202
207
|
# AtlasRb::Collection.children("col-456")
|
|
203
208
|
# # => ["w-789", "w-790"]
|
|
204
209
|
def self.children(id, nuid: nil, on_behalf_of: nil)
|
|
205
|
-
|
|
206
|
-
connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/children')&.body
|
|
207
|
-
)
|
|
210
|
+
read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/children'))
|
|
208
211
|
end
|
|
209
212
|
|
|
210
213
|
# Replace a Collection's metadata by uploading a MODS XML document.
|
|
@@ -312,15 +315,20 @@ module AtlasRb
|
|
|
312
315
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
313
316
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
314
317
|
# omitted.
|
|
315
|
-
# @return [String] the raw response body in the requested format.
|
|
318
|
+
# @return [String, nil] the raw response body in the requested format.
|
|
316
319
|
#
|
|
320
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
321
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
322
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
323
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
324
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
317
325
|
# @example
|
|
318
326
|
# AtlasRb::Collection.mods("col-456", "xml")
|
|
319
327
|
def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil)
|
|
320
328
|
# json default, html, xml
|
|
321
|
-
connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
322
|
-
|
|
323
|
-
|
|
329
|
+
read_raw(connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
330
|
+
ROUTE + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}")
|
|
331
|
+
))
|
|
324
332
|
end
|
|
325
333
|
end
|
|
326
334
|
end
|
data/lib/atlas_rb/community.rb
CHANGED
|
@@ -175,15 +175,18 @@ module AtlasRb
|
|
|
175
175
|
# round-trip per child. For a whole subtree flattened to Works, use
|
|
176
176
|
# {AtlasRb::Resource.descendant_works}.
|
|
177
177
|
#
|
|
178
|
-
# @return [Array<String
|
|
178
|
+
# @return [Array<String>, nil] child noids from `GET /communities/<id>/children`.
|
|
179
179
|
#
|
|
180
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
181
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
182
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
183
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
184
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
180
185
|
# @example
|
|
181
186
|
# AtlasRb::Community.children("c-123")
|
|
182
187
|
# # => ["fn106x926", "kw52j804p"]
|
|
183
188
|
def self.children(id, nuid: nil, on_behalf_of: nil)
|
|
184
|
-
|
|
185
|
-
connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/children')&.body
|
|
186
|
-
)
|
|
189
|
+
read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/children'))
|
|
187
190
|
end
|
|
188
191
|
|
|
189
192
|
# Replace a Community's metadata by uploading a MODS XML document.
|
|
@@ -293,16 +296,21 @@ module AtlasRb
|
|
|
293
296
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
294
297
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
295
298
|
# omitted.
|
|
296
|
-
# @return [String] the raw response body (JSON, HTML, or XML serialized
|
|
299
|
+
# @return [String, nil] the raw response body (JSON, HTML, or XML serialized
|
|
297
300
|
# as a string).
|
|
298
301
|
#
|
|
302
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
303
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
304
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
305
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
306
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
299
307
|
# @example HTML rendering for display
|
|
300
308
|
# AtlasRb::Community.mods("c-123", "html")
|
|
301
309
|
def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil)
|
|
302
310
|
# json default, html, xml
|
|
303
|
-
connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
read_raw(connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
312
|
+
ROUTE + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}")
|
|
313
|
+
))
|
|
306
314
|
end
|
|
307
315
|
end
|
|
308
316
|
end
|
data/lib/atlas_rb/compilation.rb
CHANGED
|
@@ -88,9 +88,14 @@ module AtlasRb
|
|
|
88
88
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
89
89
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
90
90
|
# omitted.
|
|
91
|
-
# @return [AtlasRb::Mash] `{ "compilations" => [...], "pagination" => {...} }`.
|
|
91
|
+
# @return [AtlasRb::Mash, nil] `{ "compilations" => [...], "pagination" => {...} }`.
|
|
92
92
|
# Each entry in `"compilations"` is a flat Compilation, carrying the
|
|
93
93
|
# same keys {.find} returns.
|
|
94
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
95
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
96
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
97
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
98
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
94
99
|
# @raise [AtlasRb::ForbiddenError] on a cross-owner listing without admin.
|
|
95
100
|
#
|
|
96
101
|
# @example My Sets
|
|
@@ -114,9 +119,9 @@ module AtlasRb
|
|
|
114
119
|
params[:q] = q if q
|
|
115
120
|
params[:page] = page if page
|
|
116
121
|
params[:per_page] = per_page if per_page
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
read_body(connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE)) do |body|
|
|
123
|
+
AtlasRb::Mash.new(body)
|
|
124
|
+
end
|
|
120
125
|
end
|
|
121
126
|
|
|
122
127
|
# Create a Compilation owned by the acting user.
|
|
@@ -414,10 +419,15 @@ module AtlasRb
|
|
|
414
419
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
415
420
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
416
421
|
# omitted.
|
|
417
|
-
# @return [AtlasRb::Mash] `{ "contents" => [...], "pagination" =>
|
|
422
|
+
# @return [AtlasRb::Mash, nil] `{ "contents" => [...], "pagination" =>
|
|
418
423
|
# { "total", "page", "per_page", "pages" } }`. Each entry is a
|
|
419
424
|
# lightweight digest in the {Resource.find_many} vocabulary —
|
|
420
425
|
# `id` / `noid` / `klass` / `title` / `thumbnail`.
|
|
426
|
+
# `nil` when Atlas answers `404` — nothing is there to read, or, with a
|
|
427
|
+
# misconfigured `ATLAS_URL`, the route is not Atlas's at all.
|
|
428
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`
|
|
429
|
+
# (an auth or validation envelope, a `5xx`, a proxy's `503`), carrying
|
|
430
|
+
# Atlas's status and body so the failure is attributable at the boundary.
|
|
421
431
|
# @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
|
|
422
432
|
#
|
|
423
433
|
# @example
|
|
@@ -428,9 +438,9 @@ module AtlasRb
|
|
|
428
438
|
params = {}
|
|
429
439
|
params[:page] = page if page
|
|
430
440
|
params[:per_page] = per_page if per_page
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
441
|
+
read_body(connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/contents')) do |body|
|
|
442
|
+
AtlasRb::Mash.new(body)
|
|
443
|
+
end
|
|
434
444
|
end
|
|
435
445
|
end
|
|
436
446
|
end
|