atlas_rb 1.8.2 → 1.8.3

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: 0a0fac04427bc3f23f92da8a5241a263111f45b28fa01cc8829da0de3b6b48f9
4
- data.tar.gz: 8e95150d0f2ecd0a33c52179e384f8462546b422dcd6997c69886163215811c8
3
+ metadata.gz: d25a6bf4fdadf6bacf1de67390aeb15343547dba8384c511d8c34b62c1fb058e
4
+ data.tar.gz: f002dfcb1638bd8c5c5cb668121b9a8515f85765aaab4b229770d8d7b6b3587c
5
5
  SHA512:
6
- metadata.gz: 4ced2b3f0f3fee473712f38ae3161ea8f678d9468abaf1250c9577e13261b5688be16ea687a69f6b28931a6e8920ad737d7a2057059c14751cceb683e463c2ef
7
- data.tar.gz: 017e1004f59a5529184e3f07f93c815a463da07937863ff72b2063b3e1986f624e5e8ea83c0b5750b57885e7942ef981b32e66f7d3e5931ea6545108c643162d
6
+ metadata.gz: 9b7f605825d8d4ebb6c44c93c16bc8a7f16a6ee5cb3d7a83c2cba05c7de59e9c25fc92811f35a332ff326dc929b63c057c2743ed8d5fdeb807288f49f2cb2c79
7
+ data.tar.gz: 20066bd2c7f096a70d3ace6d3c36ed888d135f91e2e31dec7f7828f6eb1f0298e46e4fb0b06fe112be840846675240f6a026787f2479e8e7c73599414f3830d9
data/.version CHANGED
@@ -1 +1 @@
1
- 1.8.2
1
+ 1.8.3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.3
4
+
5
+ ### Changed — `find` is status-aware (stops swallowing Atlas error envelopes)
6
+
7
+ Every typed single-resource reader — `Resource.find` (the polymorphic resolver)
8
+ and the `Work` / `Collection` / `Community` / `FileSet` / `Person` /
9
+ `Compilation` / `Blob` / `Delegate` overrides — now routes through a shared
10
+ status-aware read path instead of blindly `JSON.parse`-ing the body and
11
+ unwrapping a fixed key.
12
+
13
+ Previously, any non-2xx that wasn't a re-parent/linked-member/Compilation/upload
14
+ signal (which `RaiseOnResourceError` already translates) passed straight through:
15
+ Atlas renders its auth/validation failures as a JSON envelope (`{ "error" => …}`,
16
+ status 400/401/403/422), so `JSON.parse(body)["work"]` found no `"work"` key and
17
+ returned **`nil`**. Callers then dereferenced that `nil` far from the cause — the
18
+ canonical symptom being `undefined method 'tombstoned' for nil`, a 500 surfaced
19
+ nowhere near the request that actually failed.
20
+
21
+ Now:
22
+
23
+ - **`404` → `nil`** — a clean "not found" (and no more `JSON::ParserError` on the
24
+ empty `head :not_found` body, which is what a missing id used to raise).
25
+ - **any other non-2xx → `AtlasRb::ResourceError`** — a new typed error carrying
26
+ Atlas's `status`, `body`, and `response`, raised **at the boundary** so the
27
+ real cause (e.g. `GET /works/abc → 401: {"error":"invalid bearer token"}`) is
28
+ attributable everywhere `find` is used.
29
+ - **`2xx`** → unchanged (the unwrapped resource).
30
+
31
+ Authorization failures on the narrow re-parent / linked-member / Compilation
32
+ write paths still surface as `AtlasRb::ForbiddenError` via
33
+ `RaiseOnResourceError`; `ResourceError` is the catch-all for the read path that
34
+ middleware intentionally does not cover.
35
+
36
+ **Behavior change to note:** `find` of a missing id now returns `nil` rather than
37
+ raising `JSON::ParserError`. Callers that relied on the parse error (none known)
38
+ should nil-check instead.
39
+
3
40
  ## 1.8.0
4
41
 
5
42
  ### Added — `Work.set_full_text` (full-text search seam)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.8.2)
4
+ atlas_rb (1.8.3)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
data/lib/atlas_rb/blob.rb CHANGED
@@ -23,20 +23,22 @@ module AtlasRb
23
23
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
24
24
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
25
25
  # omitted.
26
- # @return [Hash] the `"blob"` object, already unwrapped — typically
26
+ # @return [Hash, nil] the `"blob"` object, already unwrapped — typically
27
27
  # includes `"id"`, `"original_filename"`, `"size"`, `"digest"` (the
28
28
  # recorded fixity digest `"sha512:<hex>"`, or `nil` for a Blob with no
29
29
  # held bytes — reconciliation compares this against the v1 manifest
30
- # without re-downloading), and a download URL.
30
+ # without re-downloading), and a download URL — or `nil` when the Blob
31
+ # does not exist (`404`).
32
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
33
+ # auth/validation error envelope), carrying Atlas's status + body.
31
34
  #
32
35
  # @example
33
36
  # AtlasRb::Blob.find("b-321")
34
37
  # # => { "id" => "b-321", "original_filename" => "scan.pdf",
35
38
  # # "digest" => "sha512:9f86d0…", ... }
36
39
  def self.find(id, nuid: nil, on_behalf_of: nil)
37
- AtlasRb::Mash.new(JSON.parse(
38
- connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
39
- ))['blob']
40
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
41
+ body && AtlasRb::Mash.new(body)['blob']
40
42
  end
41
43
 
42
44
  # Resolve a content Blob to its parent FileSet and containing Work noids.
@@ -22,16 +22,17 @@ module AtlasRb
22
22
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
23
23
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
24
24
  # omitted.
25
- # @return [Hash] the `"collection"` object, already unwrapped from the
26
- # JSON response.
25
+ # @return [Hash, nil] the `"collection"` object, already unwrapped from the
26
+ # JSON response, or `nil` when the Collection does not exist (`404`).
27
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
28
+ # auth/validation error envelope), carrying Atlas's status + body.
27
29
  #
28
30
  # @example
29
31
  # AtlasRb::Collection.find("col-456")
30
32
  # # => { "id" => "col-456", "title" => "Faculty Publications", ... }
31
33
  def self.find(id, nuid: nil, on_behalf_of: nil)
32
- AtlasRb::Mash.new(JSON.parse(
33
- connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
34
- ))["collection"]
34
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
35
+ body && AtlasRb::Mash.new(body)["collection"]
35
36
  end
36
37
 
37
38
  # Create a new Collection under an existing Community.
@@ -23,16 +23,17 @@ module AtlasRb
23
23
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
24
24
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
25
25
  # omitted.
26
- # @return [Hash] the `"community"` object from the JSON response,
27
- # already unwrapped.
26
+ # @return [Hash, nil] the `"community"` object from the JSON response,
27
+ # already unwrapped, or `nil` when the Community does not exist (`404`).
28
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
29
+ # auth/validation error envelope), carrying Atlas's status + body.
28
30
  #
29
31
  # @example
30
32
  # AtlasRb::Community.find("c-123")
31
33
  # # => { "id" => "c-123", "title" => "College of Engineering", ... }
32
34
  def self.find(id, nuid: nil, on_behalf_of: nil)
33
- AtlasRb::Mash.new(JSON.parse(
34
- connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
35
- ))["community"]
35
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
36
+ body && AtlasRb::Mash.new(body)["community"]
36
37
  end
37
38
 
38
39
  # Create a new Community, optionally seeded with MODS metadata.
@@ -38,19 +38,20 @@ module AtlasRb
38
38
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
39
39
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
40
40
  # omitted.
41
- # @return [Hash] the `"compilation"` object, already unwrapped — `id`,
41
+ # @return [Hash, nil] the `"compilation"` object, already unwrapped — `id`,
42
42
  # `title`, `description`, `depositor`, the three recipe arrays
43
43
  # (`included_collections`, `included_works`, `excluded_works`), the
44
- # ACL arrays, and timestamps.
44
+ # ACL arrays, and timestamps — or `nil` when the Set does not exist (`404`).
45
45
  # @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
46
+ # @raise [AtlasRb::ResourceError] on any other non-2xx (e.g. `401`),
47
+ # carrying Atlas's status + body.
46
48
  #
47
49
  # @example
48
50
  # AtlasRb::Compilation.find("c-123", nuid: "000000002")
49
51
  # # => { "id" => "c-123", "title" => "Course readings", ... }
50
52
  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"]
53
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
54
+ body && AtlasRb::Mash.new(body)["compilation"]
54
55
  end
55
56
 
56
57
  # List Compilations, paginated (newest first), in one of three modes.
@@ -29,16 +29,18 @@ module AtlasRb
29
29
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
30
30
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
31
31
  # omitted.
32
- # @return [AtlasRb::Mash] the `"delegate"` object, already unwrapped —
32
+ # @return [AtlasRb::Mash, nil] the `"delegate"` object, already unwrapped —
33
33
  # includes `id`, `valkyrie_id`, `use`, `uri`, `mime_type`,
34
- # `original_filename`, `label`, and tombstone fields.
34
+ # `original_filename`, `label`, and tombstone fields — or `nil` when the
35
+ # Delegate does not exist (`404`).
36
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
37
+ # auth/validation error envelope), carrying Atlas's status + body.
35
38
  #
36
39
  # @example
37
40
  # AtlasRb::Delegate.find("d-555")
38
41
  def self.find(id, nuid: nil, on_behalf_of: nil)
39
- AtlasRb::Mash.new(JSON.parse(
40
- connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
41
- ))["delegate"]
42
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
43
+ body && AtlasRb::Mash.new(body)["delegate"]
42
44
  end
43
45
  end
44
46
  end
@@ -200,6 +200,50 @@ module AtlasRb
200
200
  end
201
201
  end
202
202
 
203
+ # Raised by the typed single-resource readers ({Resource.find} and the
204
+ # `Work` / `Collection` / `Community` / `FileSet` / `Person` / `Compilation`
205
+ # / `Blob` / `Delegate` overrides) when Atlas answers the `GET` with a
206
+ # non-2xx that is **not** a `404` — i.e. an error envelope
207
+ # (`{ "error" => ... }`, status 400/401/403/422) on what the caller treated
208
+ # as a plain read.
209
+ #
210
+ # Before this existed, `find` unwrapped the success body by a fixed key
211
+ # (`["work"]`, `["collection"]`, …); on an error envelope that key is
212
+ # absent, so `find` returned `nil` and silently discarded Atlas's status and
213
+ # message. The caller then dereferenced the `nil` far from the cause (the
214
+ # canonical symptom: `undefined method 'tombstoned' for nil`). This error
215
+ # keeps the failure **at the boundary**, carrying the status and body so the
216
+ # real cause (e.g. `… → 401: {"error":"invalid bearer token"}`) is
217
+ # attributable everywhere `find` is used.
218
+ #
219
+ # A genuine `404` is **not** this — it stays a clean `nil` return, since
220
+ # "not found" is a normal `find` outcome callers already nil-check.
221
+ #
222
+ # @note Authorization failures on the narrow re-parent / linked-member /
223
+ # Compilation write paths surface as {ForbiddenError} via
224
+ # {Middleware::RaiseOnResourceError}; this is the catch-all for the read
225
+ # path, which that middleware intentionally does not cover.
226
+ class ResourceError < Error
227
+ # @return [Faraday::Response, nil] the originating response, when available.
228
+ attr_reader :response
229
+
230
+ # @return [Integer, nil] Atlas's HTTP status.
231
+ attr_reader :status
232
+
233
+ # @return [String, nil] Atlas's raw response body (the error envelope).
234
+ attr_reader :body
235
+
236
+ # @param message [String] human-readable failure description.
237
+ # @param response [Faraday::Response, nil] the originating response; its
238
+ # status and body are captured for callers that rescue this.
239
+ def initialize(message, response: nil)
240
+ super(message)
241
+ @response = response
242
+ @status = response&.status
243
+ @body = response&.body
244
+ end
245
+ end
246
+
203
247
  # Raised when the transport has no way to authenticate a relay request:
204
248
  # neither `ATLAS_JWT` (BYO-JWT mode) nor a signing key
205
249
  # ({AtlasRb.config#assertion_signing_key}, relay-signing mode) is configured.
@@ -23,14 +23,16 @@ module AtlasRb
23
23
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
24
24
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
25
25
  # omitted.
26
- # @return [Hash] the `"file_set"` object, already unwrapped.
26
+ # @return [Hash, nil] the `"file_set"` object, already unwrapped, or `nil`
27
+ # when the FileSet does not exist (`404`).
28
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
29
+ # auth/validation error envelope), carrying Atlas's status + body.
27
30
  #
28
31
  # @example
29
32
  # AtlasRb::FileSet.find("fs-001")
30
33
  def self.find(id, nuid: nil, on_behalf_of: nil)
31
- AtlasRb::Mash.new(JSON.parse(
32
- connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
33
- ))["file_set"]
34
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
35
+ body && AtlasRb::Mash.new(body)["file_set"]
34
36
  end
35
37
 
36
38
  # Create a new FileSet under a Work.
@@ -33,13 +33,15 @@ module AtlasRb
33
33
  # @param id [String] the person's NOID.
34
34
  # @param nuid [String, nil] acting principal (signed into the assertion sub).
35
35
  # @param on_behalf_of [String, nil] acting-as target.
36
- # @return [AtlasRb::Mash] the unwrapped `"person"` object (carries the
36
+ # @return [AtlasRb::Mash, nil] the unwrapped `"person"` object (carries the
37
37
  # server-side `nuid` for callers that need it, e.g. depositor gating, and
38
- # `personal_root_id` for the publish-conduit parent).
38
+ # `personal_root_id` for the publish-conduit parent), or `nil` when the
39
+ # Person does not exist (`404`).
40
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
41
+ # auth/validation error envelope), carrying Atlas's status + body.
39
42
  def self.find(id, nuid: nil, on_behalf_of: nil)
40
- AtlasRb::Mash.new(JSON.parse(
41
- connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
42
- ))["person"]
43
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
44
+ body && AtlasRb::Mash.new(body)["person"]
43
45
  end
44
46
 
45
47
  # List people — the NOID-keyed People-index source. Returns the page's
@@ -32,17 +32,20 @@ module AtlasRb
32
32
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
33
33
  # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
34
34
  # omitted.
35
- # @return [Hash{String => String, Hash}] hash with two keys:
35
+ # @return [Hash{String => String, Hash}, nil] hash with two keys, or `nil`
36
+ # when the id resolves to nothing (`404`):
36
37
  # - `"klass"` — the resource type, capitalized (e.g. `"Work"`).
37
38
  # - `"resource"` — the resource payload as a Hash.
39
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
40
+ # auth/validation error envelope), carrying Atlas's status + body.
38
41
  #
39
42
  # @example Polymorphic lookup
40
43
  # AtlasRb::Resource.find("abc123")
41
44
  # # => { "klass" => "Work", "resource" => { "id" => "abc123", "title" => "..." } }
42
45
  def self.find(id, nuid: nil, on_behalf_of: nil)
43
- result = JSON.parse(
44
- connection({}, nuid, on_behalf_of: on_behalf_of).get('/resources/' + id)&.body
45
- )
46
+ result = fetch_resource('/resources/' + id, nuid: nuid, on_behalf_of: on_behalf_of)
47
+ return nil if result.nil?
48
+
46
49
  AtlasRb::Mash.new("klass" => result.first[0].capitalize,
47
50
  "resource" => result.first[1])
48
51
  end
@@ -242,5 +245,40 @@ module AtlasRb
242
245
  (kind.present? ? ".#{kind}" : '')
243
246
  )&.body
244
247
  end
248
+
249
+ # Shared read path behind every typed `find`: perform a single-resource
250
+ # `GET` and return the parsed JSON envelope, or `nil` when Atlas reports
251
+ # the resource is absent (`404`).
252
+ #
253
+ # Its job is to stop `find` from silently coercing an error *envelope*
254
+ # into `nil`. Atlas renders auth/validation failures as a JSON body
255
+ # (`{ "error" => ... }`, status 400/401/403/422); a naive
256
+ # `JSON.parse(body)["work"]` returns `nil` for the missing `"work"` key —
257
+ # losing the status and message, and surfacing as a baffling
258
+ # `NoMethodError` far from the cause. Mapping:
259
+ #
260
+ # - `404` → `nil` (clean "not found"; also avoids the
261
+ # `JSON::ParserError` the old code raised on the empty `head :not_found`
262
+ # body).
263
+ # - any other non-2xx → {AtlasRb::ResourceError} carrying Atlas's status +
264
+ # body, so the failure is attributable at the boundary.
265
+ # - `2xx` → the parsed JSON Hash, for the caller to unwrap.
266
+ #
267
+ # @param path [String] the resource path to GET (e.g. `"/works/abc123"`).
268
+ # @param nuid [String, nil] optional acting user's NUID (see {find}).
269
+ # @param on_behalf_of [String, nil] optional `On-Behalf-Of` NUID (see {find}).
270
+ # @return [Hash, nil] the parsed JSON envelope, or `nil` on `404`.
271
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404`.
272
+ # @api private
273
+ def self.fetch_resource(path, nuid: nil, on_behalf_of: nil)
274
+ resp = connection({}, nuid, on_behalf_of: on_behalf_of).get(path)
275
+ return nil if resp.status == 404
276
+ unless resp.success?
277
+ raise AtlasRb::ResourceError.new("GET #{path} → #{resp.status}: #{resp.body}", response: resp)
278
+ end
279
+
280
+ JSON.parse(resp.body)
281
+ end
282
+ private_class_method :fetch_resource
245
283
  end
246
284
  end
data/lib/atlas_rb/work.rb CHANGED
@@ -22,16 +22,17 @@ module AtlasRb
22
22
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
23
23
  # header (acting-as / view-as). Falls through to
24
24
  # {AtlasRb.config}.default_on_behalf_of when omitted.
25
- # @return [Hash] the `"work"` object, already unwrapped from the JSON
26
- # response.
25
+ # @return [Hash, nil] the `"work"` object, already unwrapped from the JSON
26
+ # response, or `nil` when the Work does not exist (`404`).
27
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
28
+ # auth/validation error envelope), carrying Atlas's status + body.
27
29
  #
28
30
  # @example
29
31
  # AtlasRb::Work.find("w-789")
30
32
  # # => { "id" => "w-789", "title" => "An Article", ... }
31
33
  def self.find(id, nuid: nil, on_behalf_of: nil)
32
- AtlasRb::Mash.new(JSON.parse(
33
- connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
34
- ))["work"]
34
+ body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
35
+ body && AtlasRb::Mash.new(body)["work"]
35
36
  end
36
37
 
37
38
  # List Works, paginated.
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.8.2
4
+ version: 1.8.3
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-26 00:00:00.000000000 Z
11
+ date: 2026-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday