atlas_rb 1.16.1 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03b277e8683662db4b0809d548d274bce6e5cd51c64e242f606d0f911237ffe5
4
- data.tar.gz: b010988a48817296d3e2ab24ae3ff4e5dcaa68239159e36d19cecfe826d85b64
3
+ metadata.gz: 3a418fc64c12474c618d8bcffbaf01f116f0bb7c80c999f4c3bd4514cf47c879
4
+ data.tar.gz: ebfa9f0d7e85b884f6f96a16dd59861d01a0d03bbf6a25c94a0e92665add8d2d
5
5
  SHA512:
6
- metadata.gz: 6560e0da51abf54fc2f6de456814022573626b2528e0592f1539f23e29c2106c0d98978dc768cae8e5d305caf3401f83853477d75cd6409806549e87aa8b0fa5
7
- data.tar.gz: 3352534d5a2aeb187db42d36c8adf621666612023b36a65fb904b85c0aa9925430f7c897f9b67ce824969c04730b547bfc578ae4c1f3b7da8f064b815e48c4d7
6
+ metadata.gz: 1e2541b872b918215dd73155ae55fc8d841a7da7fce77f6b984dc82187148dce0a4698ee2b8aaa561b659d1fd253f2077f614cb8d592edaa54a10a343ea8664a
7
+ data.tar.gz: c0bb9652d3a1cfe08485d6057f23db0e7c493fefe4bfd23be7796e825e1a0bc998b4b02e625c2475e87b247ffc49ffb229469c35adb3b770010bda4cb42c1220
data/.version CHANGED
@@ -1 +1 @@
1
- 1.16.1
1
+ 1.17.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.17.0
4
+
5
+ ### Fixed — `Resource.find` emitted a type string this namespace cannot resolve
6
+
7
+ `find` derived its `"klass"` with `String#capitalize` over Atlas's JSON key,
8
+ and `capitalize` is not the inverse of a Ruby class name once the type has two
9
+ words. Atlas answers `{"file_set": {...}}`, and `"file_set".capitalize` is
10
+ `"File_set"`. There is no `AtlasRb::File_set`.
11
+
12
+ The doc comment stated the convention as a promise — *"the resource type,
13
+ capitalized (e.g. `"Work"`)"* — and `"Work"` is the one shape where
14
+ capitalizing happens to work. Every other consumer read that as "`const_get`
15
+ this", so a FileSet NOID put a `NameError` in the caller's stack:
16
+
17
+ ```
18
+ uninitialized constant AtlasRb::File_set
19
+ ```
20
+
21
+ `find` now reports the class's own name, so a FileSet is `"FileSet"`.
22
+
23
+ **This changes one value on the wire**: `"File_set"` becomes `"FileSet"`. That
24
+ is the spelling Solr already carries as `internal_resource`, so a consumer fed
25
+ by both sources now sees one vocabulary instead of two.
26
+
27
+ ### Added — `Resource.class_for`, the supported type-string → class lookup
28
+
29
+ ```ruby
30
+ AtlasRb::Resource.class_for("file_set") # => AtlasRb::FileSet
31
+ AtlasRb::Resource.class_for("FileSet") # => AtlasRb::FileSet
32
+ AtlasRb::Resource.class_for("File_set") # => AtlasRb::FileSet
33
+ ```
34
+
35
+ Closing the loop on `find` used to mean reaching into this namespace by string
36
+ on a naming convention the gem never promised. `class_for` reads a stated,
37
+ closed set — `Resource::TYPE_MAP`, covering the eight `Resource` subclasses —
38
+ and accepts all three spellings the DRS stack produces for a type: Atlas's
39
+ wire key, Solr's `internal_resource`, and the pre-1.17.0 `"klass"` a caller
40
+ can still be holding.
41
+
42
+ The set is stated rather than derived because there is no rule to derive:
43
+ `Blob`'s `ROUTE` is `/files/`.
44
+
45
+ An unrecognized type raises `ArgumentError` — never `nil`, and never
46
+ `const_get`, which would resolve an arbitrary constant in this namespace. Same
47
+ argument `Middleware::RaiseOnReadError` settled for the read path: keep the
48
+ failure where the cause is.
49
+
50
+ ### Documented — what the generic resolver actually covers
51
+
52
+ Atlas answers `/resources/:id` for its Valkyrie-backed types only: `Work`,
53
+ `Collection`, `Community`, `FileSet`, `Blob`, `Delegate` and `Person`. A
54
+ `Compilation` is an ActiveRecord row in Atlas rather than a Valkyrie resource,
55
+ so the resolver never finds one, though `Compilation.find` does. `nil` from
56
+ `Resource.find` is therefore ambiguous: it means "no such id" **or** "that id
57
+ names a Compilation". `Resource.find`'s YARD and the README both say so now.
58
+
3
59
  ## 1.16.0
4
60
 
5
61
  ### Fixed — every read binding consults the HTTP status before it reads the body
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.16.1)
4
+ atlas_rb (1.17.0)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
data/README.md CHANGED
@@ -531,6 +531,42 @@ The two mutations raise the same way `reparent` does — `LinkedMemberError`
531
531
  on a structural `422` (carrying the envelope's `error` code as `#code`) and
532
532
  `ForbiddenError` on a `403` — instead of swallowing the envelope.
533
533
 
534
+ ### Resolving one id of unknown type (`Resource.find` and `Resource.class_for`)
535
+
536
+ When a NOID arrives as runtime data and you do not know its type, resolve
537
+ it generically and dispatch on the reported type:
538
+
539
+ ```ruby
540
+ found = AtlasRb::Resource.find("b8gtjvk")
541
+ found["klass"] # => "FileSet"
542
+ klass = AtlasRb::Resource.class_for(found["klass"]) # => AtlasRb::FileSet
543
+ klass.find(found["resource"]["id"])
544
+ ```
545
+
546
+ `class_for` is the supported way to turn a type string into a class. Do not
547
+ `const_get` into the `AtlasRb` namespace: the mapping is a stated, closed
548
+ set (`Resource::TYPE_MAP`), not a naming rule — `Blob`'s route is `/files/`,
549
+ and a wire key like `file_set` is not the class name.
550
+
551
+ It accepts every spelling the DRS stack produces for a type, because a
552
+ caller cannot tell which one it is holding:
553
+
554
+ | Spelling | Where it comes from |
555
+ |---------------|--------------------------------------------|
556
+ | `"file_set"` | Atlas's JSON wire key |
557
+ | `"FileSet"` | Solr's `internal_resource`, and `find`'s `"klass"` |
558
+ | `"File_set"` | a `"klass"` read from a gem before 1.17.0 |
559
+
560
+ An unknown type raises `ArgumentError` rather than returning `nil`, so a
561
+ type this gem does not model fails where the cause is.
562
+
563
+ **What the resolver covers.** Atlas answers `/resources/:id` for its
564
+ Valkyrie-backed types only: `Work`, `Collection`, `Community`, `FileSet`,
565
+ `Blob`, `Delegate` and `Person`. A `Compilation` is an ActiveRecord row in
566
+ Atlas rather than a Valkyrie resource, so the generic resolver never finds
567
+ one — use `AtlasRb::Compilation.find` for those. That makes `nil` ambiguous
568
+ here: it means "no such id" **or** "that id names a Compilation".
569
+
534
570
  ### Batch resolve (`Resource.find_many`)
535
571
 
536
572
  When you have a *set* of NOIDs and only need each one's title / klass /
@@ -25,6 +25,15 @@ module AtlasRb
25
25
  # method splits that into a normalized `{ "klass" => ..., "resource" => ... }`
26
26
  # pair so callers can dispatch on type.
27
27
  #
28
+ # ## What the resolver covers
29
+ #
30
+ # Atlas answers `/resources/:id` for its Valkyrie-backed types only:
31
+ # {Work}, {Collection}, {Community}, {FileSet}, {Blob}, {Delegate} and
32
+ # {Person}. A {Compilation} is an ActiveRecord row in Atlas rather than a
33
+ # Valkyrie resource, so the resolver never finds one — use
34
+ # {Compilation.find} for those. That makes `nil` ambiguous: it means "no
35
+ # such id" **or** "that id names a Compilation".
36
+ #
28
37
  # @param id [String] an Atlas resource ID of any type.
29
38
  # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
30
39
  # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
@@ -34,10 +43,14 @@ module AtlasRb
34
43
  # omitted.
35
44
  # @return [Hash{String => String, Hash}, nil] hash with two keys, or `nil`
36
45
  # when the id resolves to nothing (`404`):
37
- # - `"klass"` — the resource type, capitalized (e.g. `"Work"`).
46
+ # - `"klass"` — the resource type as its class name, e.g. `"Work"`,
47
+ # `"FileSet"`. That is the spelling Solr carries as
48
+ # `internal_resource`, and {Resource.class_for} turns it into the class.
38
49
  # - `"resource"` — the resource payload as a Hash.
39
50
  # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
40
51
  # auth/validation error envelope), carrying Atlas's status + body.
52
+ # @raise [ArgumentError] when Atlas names a type this gem defines no class
53
+ # for. See {Resource.class_for}.
41
54
  #
42
55
  # @example Polymorphic lookup
43
56
  # AtlasRb::Resource.find("abc123")
@@ -46,7 +59,10 @@ module AtlasRb
46
59
  result = fetch_resource('/resources/' + id, nuid: nuid, on_behalf_of: on_behalf_of)
47
60
  return nil if result.nil?
48
61
 
49
- AtlasRb::Mash.new("klass" => result.first[0].capitalize,
62
+ # The class's own name, never `capitalize` over the wire key: `capitalize`
63
+ # answers `"File_set"` for a `file_set`, which is not a constant in this
64
+ # namespace, and callers resolve this string to a class.
65
+ AtlasRb::Mash.new("klass" => class_for(result.first[0]).name.split("::").last,
50
66
  "resource" => result.first[1])
51
67
  end
52
68
 
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtlasRb
4
+ # Reopens {Resource} with the resource-type vocabulary. This lives in its own
5
+ # file, required after the eight subclasses, because the map holds the classes
6
+ # themselves and every one of them subclasses {Resource} — so none of them is
7
+ # defined yet while `resource.rb` is loading.
8
+ class Resource
9
+ # Every type the Atlas resolver can answer with, keyed by each spelling the
10
+ # DRS stack produces for it: Atlas's wire key (`"file_set"`), the Ruby class
11
+ # name Solr indexes as `internal_resource` (`"FileSet"`), and the
12
+ # capitalize-of-the-wire-key form (`"File_set"`) that a caller can still be
13
+ # holding from a value this gem emitted before {Resource.class_for} existed.
14
+ # A caller cannot tell which of the three it holds, so all three resolve.
15
+ #
16
+ # Stated rather than derived: {Blob}'s `ROUTE` is `/files/`, so nothing in
17
+ # the routes turns a type into its class either.
18
+ TYPE_MAP = {
19
+ "work" => Work, "Work" => Work,
20
+ "collection" => Collection, "Collection" => Collection,
21
+ "community" => Community, "Community" => Community,
22
+ "compilation" => Compilation, "Compilation" => Compilation,
23
+ "file_set" => FileSet, "FileSet" => FileSet, "File_set" => FileSet,
24
+ "blob" => Blob, "Blob" => Blob,
25
+ "delegate" => Delegate, "Delegate" => Delegate,
26
+ "person" => Person, "Person" => Person
27
+ }.freeze
28
+
29
+ # Resolve a resource-type string to the class that models it.
30
+ #
31
+ # Use this on any type that arrives as runtime data — {Resource.find}'s
32
+ # `"klass"`, a Solr `internal_resource` value, or Atlas's wire key — rather
33
+ # than reaching into this namespace with `const_get`. The set is closed and
34
+ # stated in {TYPE_MAP}; it is not a naming rule.
35
+ #
36
+ # An unrecognized type raises instead of resolving to `nil` or to whatever
37
+ # constant happens to bear that name. A caller holding a type this gem does
38
+ # not define has to hear about it here, where the cause is, rather than at
39
+ # the `NoMethodError` a few frames later.
40
+ #
41
+ # @param name [String, Symbol] a resource type in any of the three
42
+ # spellings {TYPE_MAP} accepts.
43
+ # @return [Class] the AtlasRb class for that type.
44
+ # @raise [ArgumentError] when the gem defines no class for that type.
45
+ #
46
+ # @example Dispatching on a type that arrives as data
47
+ # found = AtlasRb::Resource.find("b8gtjvk")
48
+ # AtlasRb::Resource.class_for(found["klass"]).find(found["resource"]["id"])
49
+ #
50
+ # @example Every spelling of one type
51
+ # AtlasRb::Resource.class_for("file_set") # => AtlasRb::FileSet
52
+ # AtlasRb::Resource.class_for("FileSet") # => AtlasRb::FileSet
53
+ # AtlasRb::Resource.class_for("File_set") # => AtlasRb::FileSet
54
+ def self.class_for(name)
55
+ TYPE_MAP.fetch(name.to_s) do
56
+ raise ArgumentError, "unknown Atlas resource type: #{name.inspect}"
57
+ end
58
+ end
59
+ end
60
+ end
data/lib/atlas_rb.rb CHANGED
@@ -33,6 +33,8 @@ require_relative "atlas_rb/blob"
33
33
  require_relative "atlas_rb/delegate"
34
34
  require_relative "atlas_rb/compilation"
35
35
  require_relative "atlas_rb/person"
36
+ # Must follow every Resource subclass above: it names them in a map.
37
+ require_relative "atlas_rb/resource_types"
36
38
  require_relative "atlas_rb/user"
37
39
  require_relative "atlas_rb/admin"
38
40
  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.16.1
4
+ version: 1.17.0
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-09-15 00:00:00.000000000 Z
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -180,6 +180,7 @@ files:
180
180
  - lib/atlas_rb/middleware/raise_on_stale_resource.rb
181
181
  - lib/atlas_rb/person.rb
182
182
  - lib/atlas_rb/resource.rb
183
+ - lib/atlas_rb/resource_types.rb
183
184
  - lib/atlas_rb/system.rb
184
185
  - lib/atlas_rb/system/token.rb
185
186
  - lib/atlas_rb/system/user.rb