atlas_rb 1.3.3 → 1.3.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: a36183e05b8c32292a5b0201db841ad961bcffcd51d546c0203d591cfbcfefce
4
- data.tar.gz: 81d85278032e45d983dd5007b25ca3688cea9f23e3c8b5ce94a04727ef5d9497
3
+ metadata.gz: 3f1d65e6631474391c3971b939e6394049b6b5f4198a14ffd55bd5e9c51d2bbf
4
+ data.tar.gz: d3371babc6a6aca8141a3692b399fd9989d5f8f29341ac376e3d1bed5850d0f4
5
5
  SHA512:
6
- metadata.gz: f4866c59b9c8a8321a0cc440139a5382ac03129e29e0130447da6ade868059dd484bed4029de404a61292058a0f32407266b20f707aa8757e65112d231c57374
7
- data.tar.gz: ff821a348a98120bfe12da5f0e0de0594810ab93848f3db20e549951eabb205b330df5954d2e375836dab316d50178e80545cd1b69133165aee46567600f02b7
6
+ metadata.gz: f9450d1ed3d4c77708b619c4c48686c35606cee0d8633d9fcd448440127398b613f2d664fb672bbfaccb0bf44143ccbf8731aac29e3d967f09b56177ab7517ae
7
+ data.tar.gz: acb806ac1e60026594bac52eb124415439daa77ceb219cc5465dc961d16ab78b2dc481fe64256a5cbf109e67347170ac15e2f484fb7d19ea248b69406a950834
data/.version CHANGED
@@ -1 +1 @@
1
- 1.3.3
1
+ 1.3.4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.4
4
+
5
+ ### Added — Compilation (DRS "Sets") bindings
6
+
7
+ Bindings for Atlas's Compilation surface (Atlas v0.6.57) — personal,
8
+ curated, recipe-based groupings of Works and Collections, the persistence
9
+ behind the Cerberus Sets UI.
10
+
11
+ ```ruby
12
+ set = AtlasRb::Compilation.create("Course readings", nuid: "000000002")
13
+
14
+ # Recipe lines — each mutation returns the full updated compilation
15
+ AtlasRb::Compilation.add_included_collection(set["id"], "col-456", nuid: "000000002")
16
+ AtlasRb::Compilation.add_included_work(set["id"], "w-789", nuid: "000000002")
17
+ AtlasRb::Compilation.add_exclusion(set["id"], "w-790", nuid: "000000002") # set aside
18
+ AtlasRb::Compilation.remove_exclusion(set["id"], "w-790", nuid: "000000002") # put back
19
+
20
+ # Make public, resolve the recipe
21
+ AtlasRb::Compilation.update(set["id"],
22
+ permissions: { read: ["public"], edit: [], edit_users: [] },
23
+ nuid: "000000002")
24
+ AtlasRb::Compilation.contents(set["id"]).contents.map(&:noid)
25
+ ```
26
+
27
+ - `Compilation.create / find / update / destroy / list` — owner-scoped
28
+ CRUD. The depositor is stamped server-side from the acting NUID and is
29
+ immutable; `list(owner:)` (cross-owner) is admin-only. `update` takes
30
+ `title:` / `description:` / `permissions:` (the ACL hash replaces all
31
+ three grant lists; ACL changes are audited server-side, no-ops
32
+ suppressed).
33
+ - Six membership calls (`add/remove_included_collection`,
34
+ `add/remove_included_work`, `add/remove_exclusion`) — each returns the
35
+ updated `"compilation"` object so chip counts refresh without a
36
+ follow-up `find`. Adds and removes are idempotent; the type rules
37
+ (Works and Collections only, no Communities) are enforced by Atlas.
38
+ - `Compilation.contents` wraps `GET /compilations/<id>/contents` — the
39
+ recipe resolved to `find_many`-style digests with Solr-side pagination
40
+ (`{ total, page, per_page, pages }`). Included for completeness; CERES
41
+ hits the endpoint directly and Cerberus resolves contents via its own
42
+ Blacklight query.
43
+ - New `AtlasRb::CompilationError` (422 — blank title, wrong-type or
44
+ unknown membership noid), the Compilation sibling of
45
+ `LinkedMemberError`. `AtlasRb::ForbiddenError` now also covers 403s on
46
+ the Compilation surface, so a non-grantee reading a private Set gets a
47
+ typed refusal instead of a swallowed `nil`.
48
+
3
49
  ## 1.3.3
4
50
 
5
51
  ### Added — multipage bindings (FileSet ordinality)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.3.3)
4
+ atlas_rb (1.3.4)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
@@ -23,7 +23,7 @@ GEM
23
23
  net-http (~> 0.5)
24
24
  hashie (5.1.0)
25
25
  logger
26
- json (2.19.8)
26
+ json (2.19.9)
27
27
  logger (1.7.0)
28
28
  multipart-post (2.4.1)
29
29
  net-http (0.9.1)
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtlasRb
4
+ # A Compilation (DRS "Set") — a personal, curated, recipe-based grouping
5
+ # of {Work}s and {Collection}s.
6
+ #
7
+ # The recipe is three noid lists: included collections (resolved
8
+ # transitively — the collection plus everything beneath it), individually
9
+ # included works, and excluded works ("set-asides", subtracted from the
10
+ # resolved union). Atlas resolves the recipe at read time via
11
+ # {.contents}; nothing is materialized.
12
+ #
13
+ # Compilations are Atlas-side ActiveRecord (ephemeral curation, not
14
+ # repository content), but carry a minted NOID as their public id — so
15
+ # ids here look exactly like every other resource's. There is no `/mods`,
16
+ # thumbnail, or tombstone surface to bind. Membership rules (Works and
17
+ # Collections only, no Communities) are enforced server-side; a rejected
18
+ # add surfaces as {AtlasRb::CompilationError} (422), an authorization
19
+ # refusal as {AtlasRb::ForbiddenError} (403).
20
+ #
21
+ # See also: {Work.add_linked_member} — the membership add/remove pairs
22
+ # here mirror that precedent.
23
+ class Compilation < Resource
24
+ # Atlas REST endpoint prefix for this resource.
25
+ # @api private
26
+ ROUTE = "/compilations/"
27
+
28
+ # Fetch a single Compilation by ID.
29
+ #
30
+ # Visibility is per-row: the owner, holders of an explicit read/edit
31
+ # grant, and (for public Sets) anyone — a private Set read by a
32
+ # non-grantee raises {AtlasRb::ForbiddenError}.
33
+ #
34
+ # @param id [String] the Compilation ID (NOID).
35
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
36
+ # `User:` header. Required for cerberus-token requests; legacy bearer
37
+ # tokens still resolve without it.
38
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
39
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
40
+ # omitted.
41
+ # @return [Hash] the `"compilation"` object, already unwrapped — `id`,
42
+ # `title`, `description`, `depositor`, the three recipe arrays
43
+ # (`included_collections`, `included_works`, `excluded_works`), the
44
+ # ACL arrays, and timestamps.
45
+ # @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
46
+ #
47
+ # @example
48
+ # AtlasRb::Compilation.find("c-123", nuid: "000000002")
49
+ # # => { "id" => "c-123", "title" => "Course readings", ... }
50
+ def self.find(id, nuid: nil, on_behalf_of: nil)
51
+ AtlasRb::Mash.new(JSON.parse(
52
+ connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
53
+ ))["compilation"]
54
+ end
55
+
56
+ # List Compilations, owner-scoped and paginated (newest first).
57
+ #
58
+ # Defaults to the acting user's own Sets. Pass `owner:` to list another
59
+ # user's — that is admin-only and raises {AtlasRb::ForbiddenError} for
60
+ # anyone else. There is no public browse surface.
61
+ #
62
+ # @param owner [String, nil] NUID whose Sets to list (admin-only when it
63
+ # isn't the acting user). Omit for "my Sets".
64
+ # @param page [Integer, nil] 1-indexed page number.
65
+ # @param per_page [Integer, nil] page size override.
66
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
67
+ # `User:` header. Required for cerberus-token requests; legacy bearer
68
+ # tokens still resolve without it.
69
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
70
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
71
+ # omitted.
72
+ # @return [AtlasRb::Mash] `{ "compilations" => [...], "pagination" => {...} }`.
73
+ # Each entry wraps the same `"compilation"` object {.find} returns.
74
+ # @raise [AtlasRb::ForbiddenError] on a cross-owner listing without admin.
75
+ #
76
+ # @example My Sets
77
+ # AtlasRb::Compilation.list(nuid: "000000002")
78
+ #
79
+ # @example Another user's Sets (admin)
80
+ # AtlasRb::Compilation.list(owner: "000000002", nuid: "000000004")
81
+ def self.list(owner: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
82
+ params = {}
83
+ params[:owner] = owner if owner
84
+ params[:page] = page if page
85
+ params[:per_page] = per_page if per_page
86
+ AtlasRb::Mash.new(JSON.parse(
87
+ connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE)&.body
88
+ ))
89
+ end
90
+
91
+ # Create a Compilation owned by the acting user.
92
+ #
93
+ # The depositor (owner) is stamped server-side from the authenticated
94
+ # NUID — it is not a parameter and is immutable post-create. New Sets
95
+ # are born private: empty ACLs, no staff default.
96
+ #
97
+ # @param title [String] the Set's title (required; blank is a 422).
98
+ # @param description [String, nil] optional free-text description.
99
+ # @param nuid [String, nil] the acting user's NUID, forwarded as the
100
+ # `User:` header — the created Set's owner. Required for
101
+ # cerberus-token requests.
102
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
103
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
104
+ # omitted.
105
+ # @return [Hash] the created `"compilation"` object, already unwrapped.
106
+ # @raise [AtlasRb::CompilationError] if Atlas rejects the create (422 —
107
+ # e.g. a blank title).
108
+ # @raise [AtlasRb::ForbiddenError] if the caller may not create Sets
109
+ # (guests cannot).
110
+ #
111
+ # @example
112
+ # AtlasRb::Compilation.create("Course readings",
113
+ # description: "HIST 1101",
114
+ # nuid: "000000002")
115
+ def self.create(title, description: nil, nuid: nil, on_behalf_of: nil)
116
+ params = { title: title }
117
+ params[:description] = description if description
118
+ AtlasRb::Mash.new(JSON.parse(
119
+ connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
120
+ ))["compilation"]
121
+ end
122
+
123
+ # Update a Compilation's title / description / ACL.
124
+ #
125
+ # Only the keys you pass are written. The `permissions:` hash replaces
126
+ # all three grant lists at once (`read:` / `edit:` group lists plus
127
+ # `edit_users:` NUIDs); the depositor is never writable. Server-side,
128
+ # an ACL change emits a `permissions` audit event (no-op ACL writes are
129
+ # suppressed); recipe membership has its own calls and emits nothing.
130
+ #
131
+ # @param id [String] the Compilation ID.
132
+ # @param title [String, nil] new title.
133
+ # @param description [String, nil] new description.
134
+ # @param permissions [Hash, nil] ACL replacement, e.g.
135
+ # `{ read: ["public"], edit: [], edit_users: ["000000003"] }`.
136
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
137
+ # `User:` header. Required for cerberus-token requests; legacy bearer
138
+ # tokens still resolve without it.
139
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
140
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
141
+ # omitted.
142
+ # @return [Hash] the updated `"compilation"` object, already unwrapped.
143
+ # @raise [AtlasRb::CompilationError] if Atlas rejects the update (422).
144
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights
145
+ # (owner / explicit grant / admin).
146
+ #
147
+ # @example Rename
148
+ # AtlasRb::Compilation.update("c-123", title: "Renamed", nuid: "000000002")
149
+ #
150
+ # @example Make public (the CERES case)
151
+ # AtlasRb::Compilation.update("c-123",
152
+ # permissions: { read: ["public"], edit: [], edit_users: [] },
153
+ # nuid: "000000002")
154
+ def self.update(id, title: nil, description: nil, permissions: nil, nuid: nil, on_behalf_of: nil)
155
+ params = {}
156
+ params[:title] = title if title
157
+ params[:description] = description if description
158
+ params[:permissions] = permissions if permissions
159
+ AtlasRb::Mash.new(JSON.parse(
160
+ connection(params, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)&.body
161
+ ))["compilation"]
162
+ end
163
+
164
+ # Destroy a Compilation.
165
+ #
166
+ # Owner (or edit-grantee / admin) only. The recipe rows go with it; the
167
+ # Works and Collections it referenced are untouched — a Set is a view,
168
+ # not a container.
169
+ #
170
+ # @param id [String] the Compilation ID.
171
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
172
+ # `User:` header. Required for cerberus-token requests; legacy bearer
173
+ # tokens still resolve without it.
174
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
175
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
176
+ # omitted.
177
+ # @return [Faraday::Response] the raw response. Status `204` on success.
178
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
179
+ #
180
+ # @example
181
+ # AtlasRb::Compilation.destroy("c-123", nuid: "000000002")
182
+ def self.destroy(id, nuid: nil, on_behalf_of: nil)
183
+ connection({}, nuid, on_behalf_of: on_behalf_of).delete(ROUTE + id)
184
+ end
185
+
186
+ # Add an include-collection recipe line: everything under the
187
+ # Collection (transitively) joins the Set's resolved contents.
188
+ #
189
+ # Idempotent — re-adding an included collection is a no-op. The noid
190
+ # must resolve to a Collection: Communities and unknown ids are
191
+ # rejected server-side as a 422.
192
+ #
193
+ # @param id [String] the Compilation ID.
194
+ # @param collection_id [String] the Collection NOID to include.
195
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
196
+ # `User:` header. Required for cerberus-token requests; legacy bearer
197
+ # tokens still resolve without it.
198
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
199
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
200
+ # omitted.
201
+ # @return [Hash] the updated `"compilation"` object — the response is
202
+ # the full recipe, so chip counts refresh without a follow-up {.find}.
203
+ # @raise [AtlasRb::CompilationError] if the noid is not a Collection (422).
204
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
205
+ #
206
+ # @example
207
+ # AtlasRb::Compilation.add_included_collection("c-123", "col-456", nuid: "000000002")
208
+ def self.add_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
209
+ AtlasRb::Mash.new(JSON.parse(
210
+ connection({ collection_id: collection_id }, nuid, on_behalf_of: on_behalf_of)
211
+ .post(ROUTE + id + '/included_collections')&.body
212
+ ))["compilation"]
213
+ end
214
+
215
+ # Remove an include-collection recipe line.
216
+ #
217
+ # Idempotent — removing a collection that is not in the recipe is a
218
+ # 200 no-op (nothing for a client to recover from).
219
+ #
220
+ # @param id [String] the Compilation ID.
221
+ # @param collection_id [String] the Collection NOID to remove.
222
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
223
+ # `User:` header. Required for cerberus-token requests; legacy bearer
224
+ # tokens still resolve without it.
225
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
226
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
227
+ # omitted.
228
+ # @return [Hash] the updated `"compilation"` object.
229
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
230
+ #
231
+ # @example
232
+ # AtlasRb::Compilation.remove_included_collection("c-123", "col-456", nuid: "000000002")
233
+ def self.remove_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
234
+ AtlasRb::Mash.new(JSON.parse(
235
+ connection({}, nuid, on_behalf_of: on_behalf_of)
236
+ .delete(ROUTE + id + '/included_collections/' + collection_id)&.body
237
+ ))["compilation"]
238
+ end
239
+
240
+ # Add an include-work recipe line: one Work, included individually.
241
+ #
242
+ # Idempotent. The noid must resolve to a Work; anything else is a 422.
243
+ #
244
+ # @param id [String] the Compilation ID.
245
+ # @param work_id [String] the Work NOID to include.
246
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
247
+ # `User:` header. Required for cerberus-token requests; legacy bearer
248
+ # tokens still resolve without it.
249
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
250
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
251
+ # omitted.
252
+ # @return [Hash] the updated `"compilation"` object.
253
+ # @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
254
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
255
+ #
256
+ # @example
257
+ # AtlasRb::Compilation.add_included_work("c-123", "w-789", nuid: "000000002")
258
+ def self.add_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
259
+ AtlasRb::Mash.new(JSON.parse(
260
+ connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
261
+ .post(ROUTE + id + '/included_works')&.body
262
+ ))["compilation"]
263
+ end
264
+
265
+ # Remove an include-work recipe line. Idempotent.
266
+ #
267
+ # @param id [String] the Compilation ID.
268
+ # @param work_id [String] the Work NOID to remove.
269
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
270
+ # `User:` header. Required for cerberus-token requests; legacy bearer
271
+ # tokens still resolve without it.
272
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
273
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
274
+ # omitted.
275
+ # @return [Hash] the updated `"compilation"` object.
276
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
277
+ #
278
+ # @example
279
+ # AtlasRb::Compilation.remove_included_work("c-123", "w-789", nuid: "000000002")
280
+ def self.remove_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
281
+ AtlasRb::Mash.new(JSON.parse(
282
+ connection({}, nuid, on_behalf_of: on_behalf_of)
283
+ .delete(ROUTE + id + '/included_works/' + work_id)&.body
284
+ ))["compilation"]
285
+ end
286
+
287
+ # Set a Work aside: subtract it from the Set's resolved contents even
288
+ # though an included collection covers it.
289
+ #
290
+ # Idempotent. The noid must resolve to a Work. Setting aside a Work
291
+ # that no inclusion currently covers is legal — the recipe lines are
292
+ # independent; the subtraction just matches nothing.
293
+ #
294
+ # @param id [String] the Compilation ID.
295
+ # @param work_id [String] the Work NOID to set aside.
296
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
297
+ # `User:` header. Required for cerberus-token requests; legacy bearer
298
+ # tokens still resolve without it.
299
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
300
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
301
+ # omitted.
302
+ # @return [Hash] the updated `"compilation"` object.
303
+ # @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
304
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
305
+ #
306
+ # @example
307
+ # AtlasRb::Compilation.add_exclusion("c-123", "w-789", nuid: "000000002")
308
+ def self.add_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
309
+ AtlasRb::Mash.new(JSON.parse(
310
+ connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
311
+ .post(ROUTE + id + '/exclusions')&.body
312
+ ))["compilation"]
313
+ end
314
+
315
+ # Put a set-aside Work back. Idempotent.
316
+ #
317
+ # @param id [String] the Compilation ID.
318
+ # @param work_id [String] the Work NOID to restore to the resolved set.
319
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
320
+ # `User:` header. Required for cerberus-token requests; legacy bearer
321
+ # tokens still resolve without it.
322
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
323
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
324
+ # omitted.
325
+ # @return [Hash] the updated `"compilation"` object.
326
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
327
+ #
328
+ # @example
329
+ # AtlasRb::Compilation.remove_exclusion("c-123", "w-789", nuid: "000000002")
330
+ def self.remove_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
331
+ AtlasRb::Mash.new(JSON.parse(
332
+ connection({}, nuid, on_behalf_of: on_behalf_of)
333
+ .delete(ROUTE + id + '/exclusions/' + work_id)&.body
334
+ ))["compilation"]
335
+ end
336
+
337
+ # Resolve a Compilation's recipe into the Works it currently denotes.
338
+ #
339
+ # Wraps `GET /compilations/<id>/contents` — included for completeness;
340
+ # the endpoint's primary consumer is CERES (which calls Atlas directly),
341
+ # and Cerberus resolves Set contents via its own Blacklight query.
342
+ # Results are gated to what the caller may discover (public + the
343
+ # caller's groups; admins see everything; tombstoned works excluded) —
344
+ # the same semantics as Cerberus gated discovery.
345
+ #
346
+ # @param id [String] the Compilation ID.
347
+ # @param page [Integer, nil] 1-indexed page number (default 1).
348
+ # @param per_page [Integer, nil] page size (default 25, capped at 100).
349
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
350
+ # `User:` header. Required for cerberus-token requests; legacy bearer
351
+ # tokens still resolve without it.
352
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
353
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
354
+ # omitted.
355
+ # @return [AtlasRb::Mash] `{ "contents" => [...], "pagination" =>
356
+ # { "total", "page", "per_page", "pages" } }`. Each entry is a
357
+ # lightweight digest in the {Resource.find_many} vocabulary —
358
+ # `id` / `noid` / `klass` / `title` / `thumbnail`.
359
+ # @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
360
+ #
361
+ # @example
362
+ # page = AtlasRb::Compilation.contents("c-123", nuid: "000000002")
363
+ # page.contents.map(&:noid)
364
+ # page.pagination.total
365
+ def self.contents(id, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
366
+ params = {}
367
+ params[:page] = page if page
368
+ params[:per_page] = per_page if per_page
369
+ AtlasRb::Mash.new(JSON.parse(
370
+ connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/contents')&.body
371
+ ))
372
+ end
373
+ end
374
+ end
@@ -101,14 +101,48 @@ module AtlasRb
101
101
  end
102
102
  end
103
103
 
104
- # Raised when Atlas refuses a re-parent or linked-member write with an
105
- # HTTP `403`, whose envelope is `{ "error", "action", "subject" }`. Lets
106
- # callers distinguish "you may not do this" from a structural rejection
107
- # ({ReparentError} / {LinkedMemberError}) or a not-found.
104
+ # Raised when Atlas rejects a Compilation (Set) write with a `422`
105
+ # carrying a machine-readable `error` discriminator a blank title on
106
+ # create/update (`invalid_record`), or a membership add whose noid does
107
+ # not resolve to the expected type (a Community where a Collection is
108
+ # required, an unknown id, a Collection where a Work is required).
108
109
  #
109
- # @note Scoped to the re-parent / linked-member write paths — `403`s on
110
- # other endpoints still surface as raw responses for the caller's own
111
- # rescue layer, unchanged.
110
+ # The Compilation sibling of {LinkedMemberError}; same shape, same
111
+ # rationale (the binding's `["compilation"]` unwrap would otherwise
112
+ # discard the envelope on a non-2xx).
113
+ #
114
+ # rescue AtlasRb::CompilationError => e
115
+ # flash.now[:alert] = e.message
116
+ #
117
+ # @note Authorization failures surface as {ForbiddenError} (HTTP 403).
118
+ class CompilationError < Error
119
+ # @return [String, nil] the machine-readable error code from the
120
+ # envelope (currently `"invalid_record"`).
121
+ attr_reader :code
122
+
123
+ # @return [String, nil] the rejected resource's ID, from the envelope
124
+ # (may be nil — validation envelopes don't always carry one).
125
+ attr_reader :resource_id
126
+
127
+ # @param message [String] human-readable rejection description.
128
+ # @param code [String, nil] the envelope's `error` discriminator.
129
+ # @param resource_id [String, nil] the rejected resource's ID.
130
+ def initialize(message, code: nil, resource_id: nil)
131
+ super(message)
132
+ @code = code
133
+ @resource_id = resource_id
134
+ end
135
+ end
136
+
137
+ # Raised when Atlas refuses a re-parent, linked-member, or Compilation
138
+ # request with an HTTP `403`, whose envelope is
139
+ # `{ "error", "action", "subject" }`. Lets callers distinguish "you may
140
+ # not do this" from a structural rejection ({ReparentError} /
141
+ # {LinkedMemberError} / {CompilationError}) or a not-found.
142
+ #
143
+ # @note Scoped to the re-parent / linked-member write paths and the
144
+ # Compilation surface — `403`s on other endpoints still surface as raw
145
+ # responses for the caller's own rescue layer, unchanged.
112
146
  class ForbiddenError < Error
113
147
  # @return [String, nil] the envelope's `error` value.
114
148
  attr_reader :code
@@ -15,30 +15,34 @@ module AtlasRb
15
15
  # {RaiseOnStaleResource}.
16
16
  #
17
17
  # It is intentionally narrow — it only fires on the re-parent
18
- # (`.../parent`) and linked-member (`.../linked_members...`) write paths,
19
- # and only on `403` / `422` bodies carrying an `error` discriminator.
18
+ # (`.../parent`) and linked-member (`.../linked_members...`) write paths
19
+ # and the Compilation surface (`/compilations...`), and only on
20
+ # `403` / `422` bodies carrying an `error` discriminator.
20
21
  # Everything else (other paths, other statuses, a `422` whose body uses a
21
22
  # different discriminator such as `tombstone`'s `code: "has_live_children"`)
22
23
  # passes through untouched, so atlas_rb stays a thin Faraday binding that
23
24
  # translates only the wire signals callers genuinely need to discriminate.
24
25
  #
25
26
  # Mapping:
26
- # - `403` (either path) → {AtlasRb::ForbiddenError} (`error`/`action`/`subject`)
27
+ # - `403` (any covered path) → {AtlasRb::ForbiddenError} (`error`/`action`/`subject`)
27
28
  # - `422` on `.../parent` → {AtlasRb::ReparentError} (`error`/`resource_id`)
28
29
  # - `422` on `.../linked_members...` → {AtlasRb::LinkedMemberError}
30
+ # - `422` on `/compilations...` → {AtlasRb::CompilationError}
29
31
  class RaiseOnResourceError < Faraday::Middleware
30
32
  # @param env [Faraday::Env] the completed response environment.
31
- # @raise [AtlasRb::ForbiddenError] on a 403 to a re-parent / linked-member path.
33
+ # @raise [AtlasRb::ForbiddenError] on a 403 to a covered path.
32
34
  # @raise [AtlasRb::ReparentError] on a 422 to a re-parent path.
33
35
  # @raise [AtlasRb::LinkedMemberError] on a 422 to a linked-member path.
36
+ # @raise [AtlasRb::CompilationError] on a 422 to a Compilation path.
34
37
  # @return [void]
35
38
  def on_complete(env)
36
39
  return unless env.status == 403 || env.status == 422
37
40
 
38
- path = env.url&.path.to_s
39
- reparent = path.end_with?("/parent")
40
- linked = path.include?("/linked_members")
41
- return unless reparent || linked
41
+ path = env.url&.path.to_s
42
+ reparent = path.end_with?("/parent")
43
+ linked = path.include?("/linked_members")
44
+ compilation = path.start_with?("/compilations")
45
+ return unless reparent || linked || compilation
42
46
 
43
47
  body = parse_json(env.body)
44
48
  return unless body.is_a?(Hash) && body["error"]
@@ -56,12 +60,18 @@ module AtlasRb
56
60
  code: body["error"],
57
61
  resource_id: body["resource_id"]
58
62
  )
59
- else
63
+ elsif linked
60
64
  raise AtlasRb::LinkedMemberError.new(
61
65
  body["message"] || "Atlas rejected the linked-member write",
62
66
  code: body["error"],
63
67
  resource_id: body["resource_id"]
64
68
  )
69
+ else
70
+ raise AtlasRb::CompilationError.new(
71
+ body["message"] || "Atlas rejected the compilation write",
72
+ code: body["error"],
73
+ resource_id: body["resource_id"]
74
+ )
65
75
  end
66
76
  end
67
77
 
data/lib/atlas_rb.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "atlas_rb/work"
18
18
  require_relative "atlas_rb/file_set"
19
19
  require_relative "atlas_rb/blob"
20
20
  require_relative "atlas_rb/delegate"
21
+ require_relative "atlas_rb/compilation"
21
22
  require_relative "atlas_rb/user"
22
23
  require_relative "atlas_rb/admin"
23
24
  require_relative "atlas_rb/admin/work"
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: 1.3.3
4
+ version: 1.3.4
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-06-10 00:00:00.000000000 Z
11
+ date: 2026-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -124,6 +124,7 @@ files:
124
124
  - lib/atlas_rb/blob.rb
125
125
  - lib/atlas_rb/collection.rb
126
126
  - lib/atlas_rb/community.rb
127
+ - lib/atlas_rb/compilation.rb
127
128
  - lib/atlas_rb/configuration.rb
128
129
  - lib/atlas_rb/delegate.rb
129
130
  - lib/atlas_rb/errors.rb