atlas_rb 1.9.2 → 1.9.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 235448544b8690ecf3a941cc259ab5f049442f609766666297c571e336ac3eae
4
- data.tar.gz: d3dc2e59aab57397176e959eec0a28478d5a15dbc0ebe2be459c775a42ec00ce
3
+ metadata.gz: 808bf4695fcba808f36a2c808885529bdff8b8dae50ddfed71cd605115aa9014
4
+ data.tar.gz: 532e5bac353cceeff1806829f4e8d8f2421051799b3a249bf3119c4001c144fc
5
5
  SHA512:
6
- metadata.gz: 786316240b620bed1a107503c2162d0add7979bbc70574a9f0d1b7cbc473e616e28001d8db9abc75edc972c544211207cbd93f1eee93e674f365d2b0cbecc5e0
7
- data.tar.gz: aa6f59d62916ce00cb59ae8cca4e8e029ff96ff4c1fcb40041a4c4fd27d675dfcf98b7a2a04cca5a6f9876bdec846415196d09499f91ee170abb636964b22d25
6
+ metadata.gz: 177bfa1e9c486473285eba7fac243719f08af97ad58999fdb08c9d08725981ba8dce66143943768ff12a8e08d994ca34eb02a791cd567b7f98294732fd461a2a
7
+ data.tar.gz: 0eba14aa8d5ac54b4153fd48810c90fb979ac608cc3bf2b0fb68bdb039ed9b4db763d6610839a0656374f684fc9821bde4d3f7ae336f58ac9f894aa4aee2ad81
data/.version CHANGED
@@ -1 +1 @@
1
- 1.9.2
1
+ 1.9.4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.4
4
+
5
+ ### Fixed — write bindings parsed the response body without checking the status
6
+
7
+ A write aimed at a resource that is not there raised
8
+ `JSON::ParserError: unexpected end of input at line 1 column 1`. Atlas is
9
+ correct here: it answers `head :not_found`, a `404` with an empty body. The
10
+ bindings then handed that empty body to `JSON.parse`.
11
+
12
+ The message named neither the verb nor the resource, so an operator whose XML
13
+ manifest carried a PID from another environment got five identical parser
14
+ errors and nothing to act on. `Resource.fetch_resource` already guarded the
15
+ **read** path against exactly this; the write half never got the same
16
+ treatment.
17
+
18
+ Every `create` / `update` / `metadata` / `parent` / `rollback` and sibling
19
+ across `Work`, `Collection`, `Community`, `Blob`, `FileSet`, `Compilation` and
20
+ `Person` now goes through a `write_resource` companion:
21
+
22
+ - `404` → {AtlasRb::NotFoundError}, naming the verb and path.
23
+ - `410` → the parsed body, as on the read path: an Idempotency-Key replay whose
24
+ resource has since been tombstoned answers `410 Gone` *with* the tombstone.
25
+ - any other non-2xx → {AtlasRb::ResourceError}, carrying Atlas's status + body.
26
+ - `2xx` → the parsed body, exactly as before.
27
+
28
+ `NotFoundError` subclasses `ResourceError`, so a caller that only wants "the
29
+ write failed" rescues the parent.
30
+
31
+ Note the deliberate asymmetry with the read path, which keeps returning `nil`
32
+ on a `404`: that answers a read, but not a write — the caller asked for a
33
+ change and did not get one.
34
+
35
+ Blast radius is which exception surfaces, not whether one does. The typed
36
+ `403` / `422` translations raise inside the Faraday stack, so they still fire
37
+ first; a `422` the middleware passes through on purpose (`tombstone`'s
38
+ `has_live_children`) is unaffected, because `tombstone`, `destroy` and
39
+ `complete` return the raw response and never parsed.
40
+
41
+ ## 1.9.3
42
+
43
+ ### Added — `depositor:` on `Collection.create` and `Community.create`
44
+
45
+ `AtlasRb::Collection.create(parent_id, depositor: "000000099")` (and the same
46
+ on `Community.create`) stamps a container's intellectual owner, matching what
47
+ `Work.create` has always supported. Atlas already read the param; only the
48
+ bindings couldn't pass it.
49
+
50
+ The case it unblocks is seeding an institutional tree: the caller acts as an
51
+ admin (the only identity whose wildcard carries a whole seed sequence) while
52
+ attributing the containers to the anonymous NUID, since nobody personally owns
53
+ them and access is via Grouper groups. That matters now that a depositor
54
+ carries edit rights on their own resource — a container stamped with a real
55
+ person's NUID would hand them edit over that whole subtree.
56
+
57
+ Omitted, the depositor still falls through to the acting user, so existing
58
+ callers are unaffected.
59
+
60
+ ### Added — typed errors for Atlas's container-create `403` and ACL-write `422`
61
+
62
+ Atlas gained two refusals that the bindings previously swallowed, because
63
+ `RaiseOnResourceError` was scoped to the re-parent / linked-member /
64
+ Compilation / derivative-permissions / upload paths:
65
+
66
+ - **A refused create.** `POST /{works,collections,communities}` now `403`s when
67
+ the caller holds no edit rights on the destination container. The binding's
68
+ `["collection"]` unwrap returned `nil`, so the caller's next `.id` raised
69
+ `NoMethodError` — an unhandled 500 instead of an authorization failure. Now
70
+ {AtlasRb::ForbiddenError}, carrying `action: "create_child"` and the parent
71
+ container's class as `subject`.
72
+ - **A refused ACL write.** `PATCH /{type}/:id` with `metadata[permissions]`
73
+ now `422`s with `visibility_exceeds_parent` when the read audience would
74
+ exceed the structural container. The parsed envelope looked like a success
75
+ payload, so a user's visibility edit was discarded silently. Now
76
+ {AtlasRb::PermissionsError}, keyed on the `error` code rather than the path —
77
+ the same endpoint's other `422`s (tombstone's `has_live_children`) still pass
78
+ through untouched, which a path rule could not distinguish.
79
+
80
+ The create branch matches the three paths exactly and only on `POST`, so member
81
+ actions under the same prefix and the index `GET` are unaffected.
82
+
3
83
  ## 1.9.2
4
84
 
5
85
  ### Added — `read_only:` on `System::Token.mint`
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.9.2)
4
+ atlas_rb (1.9.4)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
data/README.md CHANGED
@@ -222,6 +222,11 @@ AtlasRb::FileSet.create("w-789", "primary") # file_set under work w-789, classif
222
222
  AtlasRb::Blob.create("w-789", path, name) # blob under work w-789 with original filename preserved
223
223
  ```
224
224
 
225
+ `Community.create`, `Collection.create`, and `Work.create` each accept an
226
+ optional `depositor:` kwarg — the NUID to stamp as the resource's intellectual
227
+ owner, independent of who authorizes the call. Omitted, Atlas falls through to
228
+ the acting user.
229
+
225
230
  `Work.create`, `FileSet.create`, and `Blob.create` each accept an optional
226
231
  `idempotency_key:` kwarg for retry-safe bulk-deposit jobs. The caller
227
232
  generates the UUID; the Atlas server enforces uniqueness scoped to the
@@ -433,6 +438,43 @@ tombstoned resources come back flagged (`"tombstoned" => true`), so index
433
438
  by `"noid"` rather than assuming positional correspondence. NOIDs only —
434
439
  raw Valkyrie ids are not a supported input.
435
440
 
441
+ ### Missing resources: `nil` on a read, a raise on a write
442
+
443
+ The two halves of the API answer an absent resource differently, on purpose.
444
+
445
+ A **read** returns `nil`. "There is nothing there" is a legitimate answer to a
446
+ question, and callers already nil-check a `find`:
447
+
448
+ ```ruby
449
+ AtlasRb::Work.find("doesnotexist") # => nil
450
+ ```
451
+
452
+ A **write** raises `AtlasRb::NotFoundError`. The caller asked for a change and
453
+ did not get one, so `nil` would invite the silent failure the read guard was
454
+ written to prevent:
455
+
456
+ ```ruby
457
+ begin
458
+ AtlasRb::Work.update("doesnotexist", "/tmp/mods.xml")
459
+ rescue AtlasRb::NotFoundError => e
460
+ e.status # => 404
461
+ e.message # => "PATCH /works/doesnotexist → 404 (no such resource)"
462
+ end
463
+ ```
464
+
465
+ This covers every `create` / `update` / `metadata` / `parent` / `rollback` and
466
+ their siblings on `Work`, `Collection`, `Community`, `Blob`, `FileSet`,
467
+ `Compilation` and `Person`. Any other non-2xx a write gets raises
468
+ `AtlasRb::ResourceError`, which `NotFoundError` subclasses — so a caller that
469
+ only wants "the write failed" rescues the parent.
470
+
471
+ Two neighbours are deliberately untouched. A `410 Gone` is returned, not
472
+ raised, the same way a read returns it: an `Idempotency-Key` replay whose
473
+ resource has since been tombstoned answers `410` *with* the tombstone, and the
474
+ caller reads `tombstoned`. And the typed `403` / `422` translations above still
475
+ fire first, while `tombstone` / `destroy` / `complete` keep returning the raw
476
+ Faraday response for the caller to read.
477
+
436
478
  ## End-to-end example
437
479
 
438
480
  JSON responses come back as `AtlasRb::Mash` (a `Hashie::Mash` subclass), so
data/lib/atlas_rb/blob.rb CHANGED
@@ -157,6 +157,10 @@ module AtlasRb
157
157
  # `"digest"` (the recorded fixity digest, `"sha512:<hex>"`).
158
158
  # @raise [AtlasRb::FixityMismatchError] if `expected_digest` was supplied and
159
159
  # the uploaded bytes did not match (or the algorithm is unsupported).
160
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
161
+ # resource, so the write did not happen.
162
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
163
+ # and body.
160
164
  #
161
165
  # @note Streams the file (FD closed deterministically); a multi-GB upload is
162
166
  # not buffered in memory. See {AtlasRb::FaradayHelper#with_file_part}.
@@ -175,9 +179,9 @@ module AtlasRb
175
179
  payload = { work_id: id, original_filename: original_filename, binary: part }
176
180
  payload[:expected_digest] = expected_digest if expected_digest
177
181
 
178
- AtlasRb::Mash.new(JSON.parse(
182
+ AtlasRb::Mash.new(write_resource(
179
183
  multipart(nuid, on_behalf_of: on_behalf_of, idempotency_key: idempotency_key)
180
- .post(ROUTE, payload)&.body
184
+ .post(ROUTE, payload)
181
185
  ))['blob']
182
186
  end
183
187
  end
@@ -223,6 +227,10 @@ module AtlasRb
223
227
  # `"blob"`, with a refreshed `"digest"` for the new revision).
224
228
  # @raise [AtlasRb::FixityMismatchError] if `expected_digest` was supplied and
225
229
  # the uploaded bytes did not match (or the algorithm is unsupported).
230
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
231
+ # resource, so the write did not happen.
232
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
233
+ # and body.
226
234
  #
227
235
  # @note Streams the file with the FD closed deterministically — see {.create}.
228
236
  #
@@ -236,9 +244,9 @@ module AtlasRb
236
244
  payload = { binary: part }
237
245
  payload[:expected_digest] = expected_digest if expected_digest
238
246
 
239
- AtlasRb::Mash.new(JSON.parse(
247
+ AtlasRb::Mash.new(write_resource(
240
248
  multipart(nuid, on_behalf_of: on_behalf_of, idempotency_key: idempotency_key)
241
- .patch(ROUTE + id, payload)&.body
249
+ .patch(ROUTE + id, payload)
242
250
  ))
243
251
  end
244
252
  end
@@ -324,8 +332,9 @@ module AtlasRb
324
332
  # recopied. Avoids a full round-trip of the bytes back through the caller
325
333
  # (vs. re-streaming {.version_content} into {.update}).
326
334
  #
327
- # Pass a `version_id` obtained from {.versions}. An unknown id or version
328
- # yields a `404` (raw Faraday response).
335
+ # Pass a `version_id` obtained from {.versions}. Atlas answers an unknown id
336
+ # or version with a `404`, which raises {AtlasRb::NotFoundError} — a write
337
+ # that did not happen must not read like one that did.
329
338
  #
330
339
  # @param id [String] the Blob ID.
331
340
  # @param version_id [String] the OCFL version label to reinstate, e.g. `"v1"`.
@@ -337,13 +346,17 @@ module AtlasRb
337
346
  # omitted.
338
347
  # @return [AtlasRb::Mash] the updated `"blob"` payload (NOID unchanged,
339
348
  # `"digest"` refreshed to the reinstated bytes).
349
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
350
+ # resource, so the write did not happen.
351
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
352
+ # and body.
340
353
  #
341
354
  # @example
342
355
  # AtlasRb::Blob.rollback("b-321", "v1")
343
356
  def self.rollback(id, version_id, nuid: nil, on_behalf_of: nil)
344
- AtlasRb::Mash.new(JSON.parse(
357
+ AtlasRb::Mash.new(write_resource(
345
358
  connection({}, nuid, on_behalf_of: on_behalf_of)
346
- .post("#{ROUTE}#{id}/rollback", JSON.dump(version_id: version_id))&.body
359
+ .post("#{ROUTE}#{id}/rollback", JSON.dump(version_id: version_id))
347
360
  ))['blob']
348
361
  end
349
362
  end
@@ -57,14 +57,30 @@ 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
+ # @param depositor [String, nil] NUID to stamp as the Collection's
61
+ # intellectual owner. Omit it and Atlas falls through to the acting user.
62
+ # Supply it to attribute a container to someone other than whoever is
63
+ # authorizing the call — e.g. seeding an institutional tree as an admin
64
+ # while attributing it to the anonymous NUID, since nobody personally owns
65
+ # those containers and access to them is via Grouper groups. The depositor
66
+ # is immutable post-create; there is no setter on the update surface.
60
67
  # @return [Hash] the created Collection payload (post-update if
61
68
  # `xml_path` was supplied).
69
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
70
+ # resource, so the write did not happen.
71
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
72
+ # and body.
62
73
  #
63
74
  # @example A featured showcase collection
64
75
  # AtlasRb::Collection.create("c-123", featured: true)
65
- def self.create(id, xml_path = nil, featured: false, nuid: nil, on_behalf_of: nil)
66
- result = AtlasRb::Mash.new(JSON.parse(
67
- connection({ parent_id: id, featured: featured }, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
76
+ #
77
+ # @example An institutional container owned by nobody
78
+ # AtlasRb::Collection.create("c-123", depositor: "000000099")
79
+ def self.create(id, xml_path = nil, featured: false, nuid: nil, on_behalf_of: nil, depositor: nil)
80
+ params = { parent_id: id, featured: featured }
81
+ params[:depositor] = depositor if depositor
82
+ result = AtlasRb::Mash.new(write_resource(
83
+ connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)
68
84
  ))["collection"]
69
85
  return result if xml_path.to_s.empty?
70
86
 
@@ -84,12 +100,16 @@ module AtlasRb
84
100
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
85
101
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when omitted.
86
102
  # @return [AtlasRb::Mash] the updated `"collection"` object, already unwrapped.
103
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
104
+ # resource, so the write did not happen.
105
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
106
+ # and body.
87
107
  #
88
108
  # @example
89
109
  # AtlasRb::Collection.set_featured("col-456", true)
90
110
  def self.set_featured(id, featured, nuid: nil, on_behalf_of: nil)
91
- AtlasRb::Mash.new(JSON.parse(
92
- connection({ featured: featured }, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)&.body
111
+ AtlasRb::Mash.new(write_resource(
112
+ connection({ featured: featured }, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)
93
113
  ))["collection"]
94
114
  end
95
115
 
@@ -122,13 +142,17 @@ module AtlasRb
122
142
  # envelope's `error` code is exposed as `#code`.
123
143
  # @raise [AtlasRb::ForbiddenError] if Atlas refuses the move on
124
144
  # authorization grounds (HTTP 403).
145
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
146
+ # resource, so the write did not happen.
147
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
148
+ # and body.
125
149
  #
126
150
  # @example
127
151
  # AtlasRb::Collection.reparent("col-456", "c-999")
128
152
  def self.reparent(id, new_parent_id, nuid: nil, on_behalf_of: nil)
129
- AtlasRb::Mash.new(JSON.parse(
153
+ AtlasRb::Mash.new(write_resource(
130
154
  connection({ parent_id: new_parent_id }, nuid, on_behalf_of: on_behalf_of)
131
- .patch(ROUTE + id + '/parent')&.body
155
+ .patch(ROUTE + id + '/parent')
132
156
  ))["collection"]
133
157
  end
134
158
 
@@ -188,6 +212,10 @@ module AtlasRb
188
212
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
189
213
  # omitted.
190
214
  # @return [Hash] the parsed JSON response from the patch.
215
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
216
+ # resource, so the write did not happen.
217
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
218
+ # and body.
191
219
  #
192
220
  # @example
193
221
  # AtlasRb::Collection.update("col-456", "/tmp/collection-mods.xml")
@@ -195,8 +223,8 @@ module AtlasRb
195
223
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
196
224
  "application/xml",
197
225
  File.basename(xml_path)) }
198
- AtlasRb::Mash.new(JSON.parse(
199
- multipart(nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id, payload)&.body
226
+ AtlasRb::Mash.new(write_resource(
227
+ multipart(nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id, payload)
200
228
  ))
201
229
  end
202
230
 
@@ -216,12 +244,16 @@ module AtlasRb
216
244
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
217
245
  # omitted.
218
246
  # @return [Hash] the parsed JSON response.
247
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
248
+ # resource, so the write did not happen.
249
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
250
+ # and body.
219
251
  #
220
252
  # @example
221
253
  # AtlasRb::Collection.metadata("col-456", title: "Renamed Collection")
222
254
  def self.metadata(id, values, nuid: nil, on_behalf_of: nil)
223
- AtlasRb::Mash.new(JSON.parse(
224
- connection({ metadata: values }, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)&.body
255
+ AtlasRb::Mash.new(write_resource(
256
+ connection({ metadata: values }, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)
225
257
  ))
226
258
  end
227
259
 
@@ -243,6 +275,10 @@ module AtlasRb
243
275
  # @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
244
276
  # conflict that exhausted its internal retry budget (HTTP 409 with
245
277
  # `error: "stale_resource"`).
278
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
279
+ # resource, so the write did not happen.
280
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
281
+ # and body.
246
282
  #
247
283
  # @example
248
284
  # AtlasRb::Collection.set_thumbnails(
@@ -253,9 +289,9 @@ module AtlasRb
253
289
  # )
254
290
  def self.set_thumbnails(id, thumbnail: nil, thumbnail_2x: nil, preview: nil, nuid: nil, on_behalf_of: nil)
255
291
  body = { thumbnail: thumbnail, thumbnail_2x: thumbnail_2x, preview: preview }.compact
256
- AtlasRb::Mash.new(JSON.parse(
292
+ AtlasRb::Mash.new(write_resource(
257
293
  connection({}, nuid, on_behalf_of: on_behalf_of)
258
- .patch(ROUTE + id + '/thumbnails', JSON.dump(body))&.body
294
+ .patch(ROUTE + id + '/thumbnails', JSON.dump(body))
259
295
  ))
260
296
  end
261
297
 
@@ -52,17 +52,33 @@ module AtlasRb
52
52
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
53
53
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
54
54
  # omitted.
55
+ # @param depositor [String, nil] NUID to stamp as the Community's
56
+ # intellectual owner. Omit it and Atlas falls through to the acting user.
57
+ # Supply it to attribute a container to someone other than whoever is
58
+ # authorizing the call — e.g. seeding an institutional tree as an admin
59
+ # while attributing it to the anonymous NUID, since nobody personally owns
60
+ # those containers and access to them is via Grouper groups. The depositor
61
+ # is immutable post-create; there is no setter on the update surface.
55
62
  # @return [Hash] the created Community payload (post-update if `xml_path`
56
63
  # was supplied).
64
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
65
+ # resource, so the write did not happen.
66
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
67
+ # and body.
57
68
  #
58
69
  # @example Top-level community, no metadata
59
70
  # AtlasRb::Community.create(nil)
60
71
  #
61
72
  # @example Sub-community seeded from MODS
62
73
  # AtlasRb::Community.create("c-parent", "/tmp/dept-mods.xml")
63
- def self.create(id = nil, xml_path = nil, nuid: nil, on_behalf_of: nil)
64
- result = AtlasRb::Mash.new(JSON.parse(
65
- connection({ parent_id: id }, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
74
+ #
75
+ # @example An institutional container owned by nobody
76
+ # AtlasRb::Community.create("c-parent", depositor: "000000099")
77
+ def self.create(id = nil, xml_path = nil, nuid: nil, on_behalf_of: nil, depositor: nil)
78
+ params = { parent_id: id }
79
+ params[:depositor] = depositor if depositor
80
+ result = AtlasRb::Mash.new(write_resource(
81
+ connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)
66
82
  ))["community"]
67
83
  return result if xml_path.to_s.empty?
68
84
 
@@ -101,6 +117,10 @@ module AtlasRb
101
117
  # `parent_not_found`). The envelope's `error` code is exposed as `#code`.
102
118
  # @raise [AtlasRb::ForbiddenError] if Atlas refuses the move on
103
119
  # authorization grounds (HTTP 403).
120
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
121
+ # resource, so the write did not happen.
122
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
123
+ # and body.
104
124
  #
105
125
  # @example Move under another Community
106
126
  # AtlasRb::Community.reparent("c-123", "c-999")
@@ -108,9 +128,9 @@ module AtlasRb
108
128
  # @example Promote to a top-level Community
109
129
  # AtlasRb::Community.reparent("c-123", nil)
110
130
  def self.reparent(id, new_parent_id, nuid: nil, on_behalf_of: nil)
111
- AtlasRb::Mash.new(JSON.parse(
131
+ AtlasRb::Mash.new(write_resource(
112
132
  connection({ parent_id: new_parent_id }, nuid, on_behalf_of: on_behalf_of)
113
- .patch(ROUTE + id + '/parent')&.body
133
+ .patch(ROUTE + id + '/parent')
114
134
  ))["community"]
115
135
  end
116
136
 
@@ -171,6 +191,10 @@ module AtlasRb
171
191
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
172
192
  # omitted.
173
193
  # @return [Hash] the parsed JSON response from the patch.
194
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
195
+ # resource, so the write did not happen.
196
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
197
+ # and body.
174
198
  #
175
199
  # @example
176
200
  # AtlasRb::Community.update("c-123", "/tmp/community-mods.xml")
@@ -178,8 +202,8 @@ module AtlasRb
178
202
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
179
203
  "application/xml",
180
204
  File.basename(xml_path)) }
181
- AtlasRb::Mash.new(JSON.parse(
182
- multipart(nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id, payload)&.body
205
+ AtlasRb::Mash.new(write_resource(
206
+ multipart(nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id, payload)
183
207
  ))
184
208
  end
185
209
 
@@ -200,12 +224,16 @@ module AtlasRb
200
224
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
201
225
  # omitted.
202
226
  # @return [Hash] the parsed JSON response.
227
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
228
+ # resource, so the write did not happen.
229
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
230
+ # and body.
203
231
  #
204
232
  # @example
205
233
  # AtlasRb::Community.metadata("c-123", title: "New Name")
206
234
  def self.metadata(id, values, nuid: nil, on_behalf_of: nil)
207
- AtlasRb::Mash.new(JSON.parse(
208
- connection({ metadata: values }, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)&.body
235
+ AtlasRb::Mash.new(write_resource(
236
+ connection({ metadata: values }, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)
209
237
  ))
210
238
  end
211
239
 
@@ -227,6 +255,10 @@ module AtlasRb
227
255
  # @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
228
256
  # conflict that exhausted its internal retry budget (HTTP 409 with
229
257
  # `error: "stale_resource"`).
258
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
259
+ # resource, so the write did not happen.
260
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
261
+ # and body.
230
262
  #
231
263
  # @example
232
264
  # AtlasRb::Community.set_thumbnails(
@@ -237,9 +269,9 @@ module AtlasRb
237
269
  # )
238
270
  def self.set_thumbnails(id, thumbnail: nil, thumbnail_2x: nil, preview: nil, nuid: nil, on_behalf_of: nil)
239
271
  body = { thumbnail: thumbnail, thumbnail_2x: thumbnail_2x, preview: preview }.compact
240
- AtlasRb::Mash.new(JSON.parse(
272
+ AtlasRb::Mash.new(write_resource(
241
273
  connection({}, nuid, on_behalf_of: on_behalf_of)
242
- .patch(ROUTE + id + '/thumbnails', JSON.dump(body))&.body
274
+ .patch(ROUTE + id + '/thumbnails', JSON.dump(body))
243
275
  ))
244
276
  end
245
277
 
@@ -136,6 +136,10 @@ module AtlasRb
136
136
  # e.g. a blank title).
137
137
  # @raise [AtlasRb::ForbiddenError] if the caller may not create Sets
138
138
  # (guests cannot).
139
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
140
+ # resource, so the write did not happen.
141
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
142
+ # and body.
139
143
  #
140
144
  # @example
141
145
  # AtlasRb::Compilation.create("Course readings",
@@ -144,8 +148,8 @@ module AtlasRb
144
148
  def self.create(title, description: nil, nuid: nil, on_behalf_of: nil)
145
149
  params = { title: title }
146
150
  params[:description] = description if description
147
- AtlasRb::Mash.new(JSON.parse(
148
- connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
151
+ AtlasRb::Mash.new(write_resource(
152
+ connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)
149
153
  ))["compilation"]
150
154
  end
151
155
 
@@ -172,6 +176,10 @@ module AtlasRb
172
176
  # @raise [AtlasRb::CompilationError] if Atlas rejects the update (422).
173
177
  # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights
174
178
  # (owner / explicit grant / admin).
179
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
180
+ # resource, so the write did not happen.
181
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
182
+ # and body.
175
183
  #
176
184
  # @example Rename
177
185
  # AtlasRb::Compilation.update("c-123", title: "Renamed", nuid: "000000002")
@@ -185,8 +193,8 @@ module AtlasRb
185
193
  params[:title] = title if title
186
194
  params[:description] = description if description
187
195
  params[:permissions] = permissions if permissions
188
- AtlasRb::Mash.new(JSON.parse(
189
- connection(params, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)&.body
196
+ AtlasRb::Mash.new(write_resource(
197
+ connection(params, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)
190
198
  ))["compilation"]
191
199
  end
192
200
 
@@ -231,13 +239,17 @@ module AtlasRb
231
239
  # the full recipe, so chip counts refresh without a follow-up {.find}.
232
240
  # @raise [AtlasRb::CompilationError] if the noid is not a Collection (422).
233
241
  # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
242
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
243
+ # resource, so the write did not happen.
244
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
245
+ # and body.
234
246
  #
235
247
  # @example
236
248
  # AtlasRb::Compilation.add_included_collection("c-123", "col-456", nuid: "000000002")
237
249
  def self.add_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
238
- AtlasRb::Mash.new(JSON.parse(
250
+ AtlasRb::Mash.new(write_resource(
239
251
  connection({ collection_id: collection_id }, nuid, on_behalf_of: on_behalf_of)
240
- .post(ROUTE + id + '/included_collections')&.body
252
+ .post(ROUTE + id + '/included_collections')
241
253
  ))["compilation"]
242
254
  end
243
255
 
@@ -256,13 +268,17 @@ module AtlasRb
256
268
  # omitted.
257
269
  # @return [Hash] the updated `"compilation"` object.
258
270
  # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
271
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
272
+ # resource, so the write did not happen.
273
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
274
+ # and body.
259
275
  #
260
276
  # @example
261
277
  # AtlasRb::Compilation.remove_included_collection("c-123", "col-456", nuid: "000000002")
262
278
  def self.remove_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
263
- AtlasRb::Mash.new(JSON.parse(
279
+ AtlasRb::Mash.new(write_resource(
264
280
  connection({}, nuid, on_behalf_of: on_behalf_of)
265
- .delete(ROUTE + id + '/included_collections/' + collection_id)&.body
281
+ .delete(ROUTE + id + '/included_collections/' + collection_id)
266
282
  ))["compilation"]
267
283
  end
268
284
 
@@ -281,13 +297,17 @@ module AtlasRb
281
297
  # @return [Hash] the updated `"compilation"` object.
282
298
  # @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
283
299
  # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
300
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
301
+ # resource, so the write did not happen.
302
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
303
+ # and body.
284
304
  #
285
305
  # @example
286
306
  # AtlasRb::Compilation.add_included_work("c-123", "w-789", nuid: "000000002")
287
307
  def self.add_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
288
- AtlasRb::Mash.new(JSON.parse(
308
+ AtlasRb::Mash.new(write_resource(
289
309
  connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
290
- .post(ROUTE + id + '/included_works')&.body
310
+ .post(ROUTE + id + '/included_works')
291
311
  ))["compilation"]
292
312
  end
293
313
 
@@ -303,13 +323,17 @@ module AtlasRb
303
323
  # omitted.
304
324
  # @return [Hash] the updated `"compilation"` object.
305
325
  # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
326
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
327
+ # resource, so the write did not happen.
328
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
329
+ # and body.
306
330
  #
307
331
  # @example
308
332
  # AtlasRb::Compilation.remove_included_work("c-123", "w-789", nuid: "000000002")
309
333
  def self.remove_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
310
- AtlasRb::Mash.new(JSON.parse(
334
+ AtlasRb::Mash.new(write_resource(
311
335
  connection({}, nuid, on_behalf_of: on_behalf_of)
312
- .delete(ROUTE + id + '/included_works/' + work_id)&.body
336
+ .delete(ROUTE + id + '/included_works/' + work_id)
313
337
  ))["compilation"]
314
338
  end
315
339
 
@@ -331,13 +355,17 @@ module AtlasRb
331
355
  # @return [Hash] the updated `"compilation"` object.
332
356
  # @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
333
357
  # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
358
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
359
+ # resource, so the write did not happen.
360
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
361
+ # and body.
334
362
  #
335
363
  # @example
336
364
  # AtlasRb::Compilation.add_exclusion("c-123", "w-789", nuid: "000000002")
337
365
  def self.add_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
338
- AtlasRb::Mash.new(JSON.parse(
366
+ AtlasRb::Mash.new(write_resource(
339
367
  connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
340
- .post(ROUTE + id + '/exclusions')&.body
368
+ .post(ROUTE + id + '/exclusions')
341
369
  ))["compilation"]
342
370
  end
343
371
 
@@ -353,13 +381,17 @@ module AtlasRb
353
381
  # omitted.
354
382
  # @return [Hash] the updated `"compilation"` object.
355
383
  # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
384
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
385
+ # resource, so the write did not happen.
386
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
387
+ # and body.
356
388
  #
357
389
  # @example
358
390
  # AtlasRb::Compilation.remove_exclusion("c-123", "w-789", nuid: "000000002")
359
391
  def self.remove_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
360
- AtlasRb::Mash.new(JSON.parse(
392
+ AtlasRb::Mash.new(write_resource(
361
393
  connection({}, nuid, on_behalf_of: on_behalf_of)
362
- .delete(ROUTE + id + '/exclusions/' + work_id)&.body
394
+ .delete(ROUTE + id + '/exclusions/' + work_id)
363
395
  ))["compilation"]
364
396
  end
365
397