atlas_rb 1.3.3 → 1.3.5

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: 21def65708de4960bb66f35c923b37b4caff75a7eb4c17721abb2e20982c8afc
4
+ data.tar.gz: e75048782fac64edb37a5e8a4798b126a2939e70a38770f4b78848bfd77ca160
5
5
  SHA512:
6
- metadata.gz: f4866c59b9c8a8321a0cc440139a5382ac03129e29e0130447da6ade868059dd484bed4029de404a61292058a0f32407266b20f707aa8757e65112d231c57374
7
- data.tar.gz: ff821a348a98120bfe12da5f0e0de0594810ab93848f3db20e549951eabb205b330df5954d2e375836dab316d50178e80545cd1b69133165aee46567600f02b7
6
+ metadata.gz: 4e9a70d364d40dfc8f21b42d29c0eae9905b52d76858d855375af12590d2250f13b70af67be75d95384658041ac5676c37d4d78664cc8d8d623fab07cc45b246
7
+ data.tar.gz: ea5156c1b8f0c842ed2ad3dc342fafd3153c0d9fd733c73a840a6aa6f8ff404bdd1854e026502a6a1f0e4a94b8c7959c0f8596a961c6aa204a82c1f5ca559653
data/.version CHANGED
@@ -1 +1 @@
1
- 1.3.3
1
+ 1.3.5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.5
4
+
5
+ ### Added — `Compilation.list(q:)` title filter
6
+
7
+ `Compilation.list` accepts `q:`, a case-insensitive title substring
8
+ filter (Atlas v0.6.60, `GET /compilations?q=<term>`). The filter applies
9
+ before pagination, so the returned `"pagination"` block describes the
10
+ filtered result. Backs the Cerberus Add-to-set typeahead.
11
+
12
+ ```ruby
13
+ AtlasRb::Compilation.list(q: "course", nuid: "000000002")
14
+ ```
15
+
16
+ ## 1.3.4
17
+
18
+ ### Added — Compilation (DRS "Sets") bindings
19
+
20
+ Bindings for Atlas's Compilation surface (Atlas v0.6.57) — personal,
21
+ curated, recipe-based groupings of Works and Collections, the persistence
22
+ behind the Cerberus Sets UI.
23
+
24
+ ```ruby
25
+ set = AtlasRb::Compilation.create("Course readings", nuid: "000000002")
26
+
27
+ # Recipe lines — each mutation returns the full updated compilation
28
+ AtlasRb::Compilation.add_included_collection(set["id"], "col-456", nuid: "000000002")
29
+ AtlasRb::Compilation.add_included_work(set["id"], "w-789", nuid: "000000002")
30
+ AtlasRb::Compilation.add_exclusion(set["id"], "w-790", nuid: "000000002") # set aside
31
+ AtlasRb::Compilation.remove_exclusion(set["id"], "w-790", nuid: "000000002") # put back
32
+
33
+ # Make public, resolve the recipe
34
+ AtlasRb::Compilation.update(set["id"],
35
+ permissions: { read: ["public"], edit: [], edit_users: [] },
36
+ nuid: "000000002")
37
+ AtlasRb::Compilation.contents(set["id"]).contents.map(&:noid)
38
+ ```
39
+
40
+ - `Compilation.create / find / update / destroy / list` — owner-scoped
41
+ CRUD. The depositor is stamped server-side from the acting NUID and is
42
+ immutable; `list(owner:)` (cross-owner) is admin-only. `update` takes
43
+ `title:` / `description:` / `permissions:` (the ACL hash replaces all
44
+ three grant lists; ACL changes are audited server-side, no-ops
45
+ suppressed).
46
+ - Six membership calls (`add/remove_included_collection`,
47
+ `add/remove_included_work`, `add/remove_exclusion`) — each returns the
48
+ updated `"compilation"` object so chip counts refresh without a
49
+ follow-up `find`. Adds and removes are idempotent; the type rules
50
+ (Works and Collections only, no Communities) are enforced by Atlas.
51
+ - `Compilation.contents` wraps `GET /compilations/<id>/contents` — the
52
+ recipe resolved to `find_many`-style digests with Solr-side pagination
53
+ (`{ total, page, per_page, pages }`). Included for completeness; CERES
54
+ hits the endpoint directly and Cerberus resolves contents via its own
55
+ Blacklight query.
56
+ - New `AtlasRb::CompilationError` (422 — blank title, wrong-type or
57
+ unknown membership noid), the Compilation sibling of
58
+ `LinkedMemberError`. `AtlasRb::ForbiddenError` now also covers 403s on
59
+ the Compilation surface, so a non-grantee reading a private Set gets a
60
+ typed refusal instead of a swallowed `nil`.
61
+
3
62
  ## 1.3.3
4
63
 
5
64
  ### 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.5)
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,381 @@
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. Pass `q:` to narrow
61
+ # by case-insensitive title substring; the filter applies before
62
+ # pagination, so the `"pagination"` block describes the filtered result.
63
+ #
64
+ # @param owner [String, nil] NUID whose Sets to list (admin-only when it
65
+ # isn't the acting user). Omit for "my Sets".
66
+ # @param q [String, nil] case-insensitive title substring filter.
67
+ # @param page [Integer, nil] 1-indexed page number.
68
+ # @param per_page [Integer, nil] page size override.
69
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
70
+ # `User:` header. Required for cerberus-token requests; legacy bearer
71
+ # tokens still resolve without it.
72
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
73
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
74
+ # omitted.
75
+ # @return [AtlasRb::Mash] `{ "compilations" => [...], "pagination" => {...} }`.
76
+ # Each entry wraps the same `"compilation"` object {.find} returns.
77
+ # @raise [AtlasRb::ForbiddenError] on a cross-owner listing without admin.
78
+ #
79
+ # @example My Sets
80
+ # AtlasRb::Compilation.list(nuid: "000000002")
81
+ #
82
+ # @example Another user's Sets (admin)
83
+ # AtlasRb::Compilation.list(owner: "000000002", nuid: "000000004")
84
+ #
85
+ # @example Title typeahead
86
+ # AtlasRb::Compilation.list(q: "course", nuid: "000000002")
87
+ def self.list(owner: nil, q: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
88
+ params = {}
89
+ params[:owner] = owner if owner
90
+ params[:q] = q if q
91
+ params[:page] = page if page
92
+ params[:per_page] = per_page if per_page
93
+ AtlasRb::Mash.new(JSON.parse(
94
+ connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE)&.body
95
+ ))
96
+ end
97
+
98
+ # Create a Compilation owned by the acting user.
99
+ #
100
+ # The depositor (owner) is stamped server-side from the authenticated
101
+ # NUID — it is not a parameter and is immutable post-create. New Sets
102
+ # are born private: empty ACLs, no staff default.
103
+ #
104
+ # @param title [String] the Set's title (required; blank is a 422).
105
+ # @param description [String, nil] optional free-text description.
106
+ # @param nuid [String, nil] the acting user's NUID, forwarded as the
107
+ # `User:` header — the created Set's owner. Required for
108
+ # cerberus-token requests.
109
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
110
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
111
+ # omitted.
112
+ # @return [Hash] the created `"compilation"` object, already unwrapped.
113
+ # @raise [AtlasRb::CompilationError] if Atlas rejects the create (422 —
114
+ # e.g. a blank title).
115
+ # @raise [AtlasRb::ForbiddenError] if the caller may not create Sets
116
+ # (guests cannot).
117
+ #
118
+ # @example
119
+ # AtlasRb::Compilation.create("Course readings",
120
+ # description: "HIST 1101",
121
+ # nuid: "000000002")
122
+ def self.create(title, description: nil, nuid: nil, on_behalf_of: nil)
123
+ params = { title: title }
124
+ params[:description] = description if description
125
+ AtlasRb::Mash.new(JSON.parse(
126
+ connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
127
+ ))["compilation"]
128
+ end
129
+
130
+ # Update a Compilation's title / description / ACL.
131
+ #
132
+ # Only the keys you pass are written. The `permissions:` hash replaces
133
+ # all three grant lists at once (`read:` / `edit:` group lists plus
134
+ # `edit_users:` NUIDs); the depositor is never writable. Server-side,
135
+ # an ACL change emits a `permissions` audit event (no-op ACL writes are
136
+ # suppressed); recipe membership has its own calls and emits nothing.
137
+ #
138
+ # @param id [String] the Compilation ID.
139
+ # @param title [String, nil] new title.
140
+ # @param description [String, nil] new description.
141
+ # @param permissions [Hash, nil] ACL replacement, e.g.
142
+ # `{ read: ["public"], edit: [], edit_users: ["000000003"] }`.
143
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
144
+ # `User:` header. Required for cerberus-token requests; legacy bearer
145
+ # tokens still resolve without it.
146
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
147
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
148
+ # omitted.
149
+ # @return [Hash] the updated `"compilation"` object, already unwrapped.
150
+ # @raise [AtlasRb::CompilationError] if Atlas rejects the update (422).
151
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights
152
+ # (owner / explicit grant / admin).
153
+ #
154
+ # @example Rename
155
+ # AtlasRb::Compilation.update("c-123", title: "Renamed", nuid: "000000002")
156
+ #
157
+ # @example Make public (the CERES case)
158
+ # AtlasRb::Compilation.update("c-123",
159
+ # permissions: { read: ["public"], edit: [], edit_users: [] },
160
+ # nuid: "000000002")
161
+ def self.update(id, title: nil, description: nil, permissions: nil, nuid: nil, on_behalf_of: nil)
162
+ params = {}
163
+ params[:title] = title if title
164
+ params[:description] = description if description
165
+ params[:permissions] = permissions if permissions
166
+ AtlasRb::Mash.new(JSON.parse(
167
+ connection(params, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)&.body
168
+ ))["compilation"]
169
+ end
170
+
171
+ # Destroy a Compilation.
172
+ #
173
+ # Owner (or edit-grantee / admin) only. The recipe rows go with it; the
174
+ # Works and Collections it referenced are untouched — a Set is a view,
175
+ # not a container.
176
+ #
177
+ # @param id [String] the Compilation ID.
178
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
179
+ # `User:` header. Required for cerberus-token requests; legacy bearer
180
+ # tokens still resolve without it.
181
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
182
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
183
+ # omitted.
184
+ # @return [Faraday::Response] the raw response. Status `204` on success.
185
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
186
+ #
187
+ # @example
188
+ # AtlasRb::Compilation.destroy("c-123", nuid: "000000002")
189
+ def self.destroy(id, nuid: nil, on_behalf_of: nil)
190
+ connection({}, nuid, on_behalf_of: on_behalf_of).delete(ROUTE + id)
191
+ end
192
+
193
+ # Add an include-collection recipe line: everything under the
194
+ # Collection (transitively) joins the Set's resolved contents.
195
+ #
196
+ # Idempotent — re-adding an included collection is a no-op. The noid
197
+ # must resolve to a Collection: Communities and unknown ids are
198
+ # rejected server-side as a 422.
199
+ #
200
+ # @param id [String] the Compilation ID.
201
+ # @param collection_id [String] the Collection NOID to include.
202
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
203
+ # `User:` header. Required for cerberus-token requests; legacy bearer
204
+ # tokens still resolve without it.
205
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
206
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
207
+ # omitted.
208
+ # @return [Hash] the updated `"compilation"` object — the response is
209
+ # the full recipe, so chip counts refresh without a follow-up {.find}.
210
+ # @raise [AtlasRb::CompilationError] if the noid is not a Collection (422).
211
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
212
+ #
213
+ # @example
214
+ # AtlasRb::Compilation.add_included_collection("c-123", "col-456", nuid: "000000002")
215
+ def self.add_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
216
+ AtlasRb::Mash.new(JSON.parse(
217
+ connection({ collection_id: collection_id }, nuid, on_behalf_of: on_behalf_of)
218
+ .post(ROUTE + id + '/included_collections')&.body
219
+ ))["compilation"]
220
+ end
221
+
222
+ # Remove an include-collection recipe line.
223
+ #
224
+ # Idempotent — removing a collection that is not in the recipe is a
225
+ # 200 no-op (nothing for a client to recover from).
226
+ #
227
+ # @param id [String] the Compilation ID.
228
+ # @param collection_id [String] the Collection NOID to remove.
229
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
230
+ # `User:` header. Required for cerberus-token requests; legacy bearer
231
+ # tokens still resolve without it.
232
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
233
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
234
+ # omitted.
235
+ # @return [Hash] the updated `"compilation"` object.
236
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
237
+ #
238
+ # @example
239
+ # AtlasRb::Compilation.remove_included_collection("c-123", "col-456", nuid: "000000002")
240
+ def self.remove_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
241
+ AtlasRb::Mash.new(JSON.parse(
242
+ connection({}, nuid, on_behalf_of: on_behalf_of)
243
+ .delete(ROUTE + id + '/included_collections/' + collection_id)&.body
244
+ ))["compilation"]
245
+ end
246
+
247
+ # Add an include-work recipe line: one Work, included individually.
248
+ #
249
+ # Idempotent. The noid must resolve to a Work; anything else is a 422.
250
+ #
251
+ # @param id [String] the Compilation ID.
252
+ # @param work_id [String] the Work NOID to include.
253
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
254
+ # `User:` header. Required for cerberus-token requests; legacy bearer
255
+ # tokens still resolve without it.
256
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
257
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
258
+ # omitted.
259
+ # @return [Hash] the updated `"compilation"` object.
260
+ # @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
261
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
262
+ #
263
+ # @example
264
+ # AtlasRb::Compilation.add_included_work("c-123", "w-789", nuid: "000000002")
265
+ def self.add_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
266
+ AtlasRb::Mash.new(JSON.parse(
267
+ connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
268
+ .post(ROUTE + id + '/included_works')&.body
269
+ ))["compilation"]
270
+ end
271
+
272
+ # Remove an include-work recipe line. Idempotent.
273
+ #
274
+ # @param id [String] the Compilation ID.
275
+ # @param work_id [String] the Work NOID to remove.
276
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
277
+ # `User:` header. Required for cerberus-token requests; legacy bearer
278
+ # tokens still resolve without it.
279
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
280
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
281
+ # omitted.
282
+ # @return [Hash] the updated `"compilation"` object.
283
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
284
+ #
285
+ # @example
286
+ # AtlasRb::Compilation.remove_included_work("c-123", "w-789", nuid: "000000002")
287
+ def self.remove_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
288
+ AtlasRb::Mash.new(JSON.parse(
289
+ connection({}, nuid, on_behalf_of: on_behalf_of)
290
+ .delete(ROUTE + id + '/included_works/' + work_id)&.body
291
+ ))["compilation"]
292
+ end
293
+
294
+ # Set a Work aside: subtract it from the Set's resolved contents even
295
+ # though an included collection covers it.
296
+ #
297
+ # Idempotent. The noid must resolve to a Work. Setting aside a Work
298
+ # that no inclusion currently covers is legal — the recipe lines are
299
+ # independent; the subtraction just matches nothing.
300
+ #
301
+ # @param id [String] the Compilation ID.
302
+ # @param work_id [String] the Work NOID to set aside.
303
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
304
+ # `User:` header. Required for cerberus-token requests; legacy bearer
305
+ # tokens still resolve without it.
306
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
307
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
308
+ # omitted.
309
+ # @return [Hash] the updated `"compilation"` object.
310
+ # @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
311
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
312
+ #
313
+ # @example
314
+ # AtlasRb::Compilation.add_exclusion("c-123", "w-789", nuid: "000000002")
315
+ def self.add_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
316
+ AtlasRb::Mash.new(JSON.parse(
317
+ connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
318
+ .post(ROUTE + id + '/exclusions')&.body
319
+ ))["compilation"]
320
+ end
321
+
322
+ # Put a set-aside Work back. Idempotent.
323
+ #
324
+ # @param id [String] the Compilation ID.
325
+ # @param work_id [String] the Work NOID to restore to the resolved set.
326
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
327
+ # `User:` header. Required for cerberus-token requests; legacy bearer
328
+ # tokens still resolve without it.
329
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
330
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
331
+ # omitted.
332
+ # @return [Hash] the updated `"compilation"` object.
333
+ # @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
334
+ #
335
+ # @example
336
+ # AtlasRb::Compilation.remove_exclusion("c-123", "w-789", nuid: "000000002")
337
+ def self.remove_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
338
+ AtlasRb::Mash.new(JSON.parse(
339
+ connection({}, nuid, on_behalf_of: on_behalf_of)
340
+ .delete(ROUTE + id + '/exclusions/' + work_id)&.body
341
+ ))["compilation"]
342
+ end
343
+
344
+ # Resolve a Compilation's recipe into the Works it currently denotes.
345
+ #
346
+ # Wraps `GET /compilations/<id>/contents` — included for completeness;
347
+ # the endpoint's primary consumer is CERES (which calls Atlas directly),
348
+ # and Cerberus resolves Set contents via its own Blacklight query.
349
+ # Results are gated to what the caller may discover (public + the
350
+ # caller's groups; admins see everything; tombstoned works excluded) —
351
+ # the same semantics as Cerberus gated discovery.
352
+ #
353
+ # @param id [String] the Compilation ID.
354
+ # @param page [Integer, nil] 1-indexed page number (default 1).
355
+ # @param per_page [Integer, nil] page size (default 25, capped at 100).
356
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
357
+ # `User:` header. Required for cerberus-token requests; legacy bearer
358
+ # tokens still resolve without it.
359
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
360
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
361
+ # omitted.
362
+ # @return [AtlasRb::Mash] `{ "contents" => [...], "pagination" =>
363
+ # { "total", "page", "per_page", "pages" } }`. Each entry is a
364
+ # lightweight digest in the {Resource.find_many} vocabulary —
365
+ # `id` / `noid` / `klass` / `title` / `thumbnail`.
366
+ # @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
367
+ #
368
+ # @example
369
+ # page = AtlasRb::Compilation.contents("c-123", nuid: "000000002")
370
+ # page.contents.map(&:noid)
371
+ # page.pagination.total
372
+ def self.contents(id, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
373
+ params = {}
374
+ params[:page] = page if page
375
+ params[:per_page] = per_page if per_page
376
+ AtlasRb::Mash.new(JSON.parse(
377
+ connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/contents')&.body
378
+ ))
379
+ end
380
+ end
381
+ 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.5
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