atlas_rb 1.3.0 → 1.3.2

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: d65fea7329731bd0fa0a397a19f5764a05e88397c522baf28721e4a08282e2d3
4
- data.tar.gz: 7b2db70c9e1a7d9e8fad5194bcb045baeed562561b1c36a60c174e2c1fc5d0aa
3
+ metadata.gz: 54691ff87af06d611d63a27333682abfd284c67d6990d0c8400ea2b9b80adb92
4
+ data.tar.gz: 7d3119d56763694dad36c69ded4cbf975af62d6eb070baf36c0df02f422259f1
5
5
  SHA512:
6
- metadata.gz: 335c7d4bd02d6bdb6f297cf86857c042d515680fb8e36991d98a7a7bf4fdd1e52470f0dda4b40605f79d0618c7260a64f2125af2a3100a693c920b51ebc1c1f5
7
- data.tar.gz: ce1bc4b16ce482975af0785138574aa6b42fc2cc05ddfee777adbef2834b733bdb93df5390cfc4e9a5012af4b9483cf5676f7213edc8e79afc0fd886d2b84545
6
+ metadata.gz: 86dc095d2be7db4fdefe997cc6b33fabd0d3c1826804b0521fda48b06d17ec4cd3fde492a3926114965d4354823f55043ea7559d4ce82dcc06a288b7fa8afecd
7
+ data.tar.gz: c31c2bea0442ec082ede4a2328c6f7471eb02151021c6e08620b4fef5b254996ad424816493e8d955e202f5c5444e800393b85064dc9feb58a61cbaf126a9e08
data/.version CHANGED
@@ -1 +1 @@
1
- 1.3.0
1
+ 1.3.2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.2
4
+
5
+ ### Added — `AtlasRb::User` (read-only user directory)
6
+
7
+ A user-context binding for Atlas's user directory endpoints — recipient
8
+ typeahead and NUID → name resolution for any surface that today renders a
9
+ bare NUID (User Inbox sender display, Audit History chips, Rights history).
10
+
11
+ ```ruby
12
+ AtlasRb::User.search("jan", nuid: "000000002")
13
+ # => [{ "nuid" => "001234567", "name" => "Doe, Jane" }, ...]
14
+
15
+ AtlasRb::User.find_by_nuid("001234567")
16
+ # => { "nuid" => "001234567", "name" => "Doe, Jane" }
17
+
18
+ AtlasRb::User.resolve(["001234567", "007654321"])
19
+ # => one entry per resolvable NUID, ordered by name
20
+ ```
21
+
22
+ - `search` is the typeahead: case-insensitive match on name, prefix match
23
+ on NUID. Atlas caps the list at 10 and orders by name.
24
+ - `resolve` batch-resolves up to 100 NUIDs in one round-trip (an inbox
25
+ page of senders in one call). Unresolvable NUIDs are dropped — callers
26
+ index by `nuid`.
27
+ - `find_by_nuid` resolves a single NUID; returns `nil` on Atlas's 404
28
+ (unknown NUID, or one held by an excluded role — indistinguishable on
29
+ the wire by design).
30
+ - Minimal disclosure is enforced server-side: entries carry `nuid` +
31
+ `name` only, and `anonymous` / `guest` / `system` rows never appear.
32
+ - Deliberately **not** under `AtlasRb::System` — this is an acting-user
33
+ capability on the ordinary `ATLAS_TOKEN` + `User:` header pairing; the
34
+ `System` namespace stays reserved for system-token calls.
35
+
36
+ ## 1.3.1
37
+
38
+ ### Added — `AtlasRb::Resource.mods_versions` / `mods_version` (MODS version history)
39
+
40
+ Two bindings for Atlas's MODS version-history endpoints. `mods_versions`
41
+ lists the retained versions of a resource's descriptive metadata;
42
+ `mods_version` fetches the raw MODS XML as of a specific version — together
43
+ enough to drive a line-diff between any two MODS states.
44
+
45
+ ```ruby
46
+ history = AtlasRb::Resource.mods_versions("w-789")
47
+ history["versions"].first["version_id"] # => "v5" (newest)
48
+ history["versions"].first["actor_nuid"] # => "000000002"
49
+
50
+ old_xml = AtlasRb::Resource.mods_version("w-789", "v3")
51
+ new_xml = AtlasRb::Resource.mods_version("w-789", "v5")
52
+ ```
53
+
54
+ - `mods_versions` returns the full envelope (`resource_id` + a
55
+ reverse-chronological `versions` array) as an `AtlasRb::Mash`. Each
56
+ descriptor mirrors the audit-event shape (`version_id`, `created`,
57
+ `actor_nuid`, `on_behalf_of_nuid`, `source`, `note`); actor fields are
58
+ correlated from the audit log and may be `null`. Admin-gated server-side.
59
+ - `mods_version` returns the **raw XML body** (mirroring `Work.mods`). Only
60
+ XML is version-recoverable — the JSON access copy is overwritten in place
61
+ — so `kind:` is accepted for parity but XML is the only retained format.
62
+ - Version labels are opaque, sortable OCFL `vN` strings (a Blob's
63
+ preservation envelope occupies earlier versions, so the first MODS
64
+ version is typically `v3`). Treat them as identifiers to feed back into
65
+ `mods_version`, not as ordinals.
66
+ - Both are type-agnostic (Community / Collection / Work) and live on
67
+ `Resource` beside `history` / `permissions`. A resource with no MODS
68
+ returns `{ "versions" => [] }`.
69
+
3
70
  ## 1.3.0
4
71
 
5
72
  ### Added — `AtlasRb::Resource.find_many` (batch resolve by NOID)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.3.0)
4
+ atlas_rb (1.3.2)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
data/README.md CHANGED
@@ -245,6 +245,44 @@ mode-less session events. Atlas stamps `occurred_at` server-side.
245
245
  Authorization errors (`401` / `403`) surface as raw Faraday responses,
246
246
  matching `Resource.history`.
247
247
 
248
+ ### MODS version history
249
+
250
+ Every descriptive-metadata edit retains the prior MODS XML on the server.
251
+ `Resource.mods_versions` lists the retained versions; `Resource.mods_version`
252
+ fetches the raw MODS XML as of one of them — together enough to render a
253
+ line-diff between any two MODS states. Both are type-agnostic (pass any
254
+ Community, Collection, or Work ID).
255
+
256
+ ```ruby
257
+ history = AtlasRb::Resource.mods_versions("w-789")
258
+ history["resource_id"] # => "w-789"
259
+ history["versions"].first["version_id"] # => "v5" (newest first)
260
+ history["versions"].first["actor_nuid"] # => "000000002" (or nil)
261
+
262
+ # Fetch two versions and diff them:
263
+ old_xml = AtlasRb::Resource.mods_version("w-789", "v3")
264
+ new_xml = AtlasRb::Resource.mods_version("w-789", "v5")
265
+ ```
266
+
267
+ `mods_versions` returns the full envelope (`resource_id` + a
268
+ reverse-chronological `versions` array) as an `AtlasRb::Mash`; each
269
+ descriptor mirrors the audit-event shape (`version_id`, `created`,
270
+ `actor_nuid`, `on_behalf_of_nuid`, `source`, `note`), so the two streams
271
+ render with the same helpers. Actor attribution is correlated from the
272
+ audit log and may be `null`. The endpoint is admin-gated server-side
273
+ (it exposes edit attribution).
274
+
275
+ `mods_version` returns the **raw XML body** — like `Work.mods`, not a Mash.
276
+ Only XML is version-recoverable (the JSON access copy is overwritten in
277
+ place), so the server serves historical XML; `kind:` is accepted for parity
278
+ with `Work.mods` but XML is the only retained format.
279
+
280
+ Version labels are **opaque, sortable OCFL `vN` strings**, not a 1-based
281
+ counter — a Blob's preservation envelope occupies earlier versions, so the
282
+ first MODS version is typically `v3`. Treat them as identifiers to feed back
283
+ into `mods_version`. A resource with no MODS returns `{ "versions" => [] }`;
284
+ `401` / `403` surface as raw Faraday responses, matching `Resource.history`.
285
+
248
286
  ### Re-parenting
249
287
 
250
288
  `reparent` moves a resource to a new structural parent, binding Atlas's
@@ -166,5 +166,81 @@ module AtlasRb
166
166
  .get('/resources/' + id + '/history')&.body
167
167
  ))
168
168
  end
169
+
170
+ # List the retained MODS versions for a resource.
171
+ #
172
+ # Wraps Atlas's `GET /resources/<id>/mods/versions`, which returns the
173
+ # full envelope — `resource_id` plus a reverse-chronological `versions`
174
+ # array — as an `AtlasRb::Mash`. Each version descriptor mirrors the
175
+ # audit-event shape (`version_id`, `created`, `actor_nuid`,
176
+ # `on_behalf_of_nuid`, `source`, `note`) so the two streams render with
177
+ # the same helpers; actor fields are correlated from the audit log and
178
+ # may be `null` when a version has no matching edit event.
179
+ #
180
+ # Type-agnostic: pass any Modsable resource ID (Community, Collection,
181
+ # Work). A resource with no MODS comes back as `{ "versions" => [] }`.
182
+ #
183
+ # Version labels are opaque, sortable OCFL `vN` strings — not a 1-based
184
+ # counter — so treat them as identifiers to feed back into
185
+ # {mods_version}, not as ordinals. The server admin-gates this endpoint
186
+ # (it exposes edit attribution); `401` / `403` surface as raw Faraday
187
+ # responses, matching {history}.
188
+ #
189
+ # @param id [String] an Atlas resource ID.
190
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
191
+ # `User:` header. Required for cerberus-token requests; legacy bearer
192
+ # tokens still resolve without it.
193
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
194
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
195
+ # omitted.
196
+ # @return [AtlasRb::Mash] the parsed envelope, with `"resource_id"` and a
197
+ # `"versions"` array (reverse chronological; possibly empty).
198
+ #
199
+ # @example
200
+ # history = AtlasRb::Resource.mods_versions("w-789")
201
+ # history["versions"].first["version_id"] # => "v5"
202
+ # history["versions"].first["actor_nuid"] # => "000000002"
203
+ def self.mods_versions(id, nuid: nil, on_behalf_of: nil)
204
+ AtlasRb::Mash.new(JSON.parse(
205
+ connection({}, nuid, on_behalf_of: on_behalf_of)
206
+ .get('/resources/' + id + '/mods/versions')&.body
207
+ ))
208
+ end
209
+
210
+ # Fetch the MODS document as of a specific version.
211
+ #
212
+ # Wraps Atlas's `GET /resources/<id>/mods/versions/<version_id>` and
213
+ # returns the **raw response body** (not parsed) — mirroring
214
+ # {Work.mods}. Pass a `version_id` obtained from {mods_versions} (an
215
+ # opaque OCFL `vN` label).
216
+ #
217
+ # Only XML is version-recoverable: the JSON access copy is overwritten in
218
+ # place, so the server serves historical XML (the default). `kind:` is
219
+ # accepted for parity with {Work.mods} but XML is currently the only
220
+ # supported format. An unknown version yields a `404` (raw Faraday
221
+ # response).
222
+ #
223
+ # @param id [String] an Atlas resource ID.
224
+ # @param version_id [String] an OCFL version label from {mods_versions},
225
+ # e.g. `"v3"`.
226
+ # @param kind [String, nil] response format extension. Omit (or pass
227
+ # `"xml"`) for the historical XML — the only format the server retains.
228
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
229
+ # `User:` header. Required for cerberus-token requests; legacy bearer
230
+ # tokens still resolve without it.
231
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
232
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
233
+ # omitted.
234
+ # @return [String] the raw MODS XML body for that version.
235
+ #
236
+ # @example Diff two MODS versions
237
+ # old_xml = AtlasRb::Resource.mods_version("w-789", "v3")
238
+ # new_xml = AtlasRb::Resource.mods_version("w-789", "v5")
239
+ def self.mods_version(id, version_id, kind: nil, nuid: nil, on_behalf_of: nil)
240
+ connection({}, nuid, on_behalf_of: on_behalf_of).get(
241
+ '/resources/' + id + '/mods/versions/' + version_id +
242
+ (kind.present? ? ".#{kind}" : '')
243
+ )&.body
244
+ end
169
245
  end
170
246
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtlasRb
4
+ # Read-only user directory lookups — typeahead search and NUID → name
5
+ # resolution.
6
+ #
7
+ # This is a **user-context** binding: calls authenticate as the acting
8
+ # user via the standard `ATLAS_TOKEN` + `User:` header pairing, like every
9
+ # other top-level class. It is deliberately *not* part of
10
+ # {AtlasRb::System} — that namespace is structurally reserved for
11
+ # system-token calls ({System::User.find_or_create}), and directory
12
+ # lookups are an ordinary logged-in-user capability.
13
+ #
14
+ # Atlas enforces minimal disclosure: every entry carries `nuid` + `name`
15
+ # only (no email, role, or groups), and rows with role `anonymous`,
16
+ # `guest`, or `system` are never returned. Per the layering principle the
17
+ # gem adds nothing on top — no caching, no name parsing, no result
18
+ # shaping; presentation belongs to the host application.
19
+ class User
20
+ extend AtlasRb::FaradayHelper
21
+
22
+ # Atlas REST endpoint prefix for the user directory.
23
+ # @api private
24
+ ROUTE = "/users"
25
+
26
+ # Typeahead search of the user directory.
27
+ #
28
+ # Case-insensitive match on name, prefix match on NUID (so typing a
29
+ # known NUID works too). Atlas caps the result (10 entries) and orders
30
+ # it by name; a blank query resolves to an empty list.
31
+ #
32
+ # @param query [String] name fragment or NUID prefix to match.
33
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
34
+ # `User:` header. Required for cerberus-token requests; legacy bearer
35
+ # tokens still resolve without it.
36
+ # @return [Array<AtlasRb::Mash>] matching directory entries, each
37
+ # carrying `nuid` and `name`.
38
+ #
39
+ # @example Recipient typeahead
40
+ # AtlasRb::User.search("jan", nuid: "000000002")
41
+ # # => [{ "nuid" => "001234567", "name" => "Doe, Jane" }, ...]
42
+ def self.search(query, nuid: nil)
43
+ JSON.parse(
44
+ connection({ q: query }, nuid).get(ROUTE)&.body
45
+ ).map { |entry| AtlasRb::Mash.new(entry) }
46
+ end
47
+
48
+ # Resolve a single NUID to a directory entry.
49
+ #
50
+ # @param target_nuid [String] the NUID being looked up — the *subject*
51
+ # of the call, distinct from the acting `nuid:` kwarg.
52
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
53
+ # `User:` header. Required for cerberus-token requests; legacy bearer
54
+ # tokens still resolve without it.
55
+ # @return [AtlasRb::Mash, nil] the `nuid` + `name` entry, or `nil` when
56
+ # Atlas reports the NUID as absent (unknown, or held by an excluded
57
+ # role — the two are indistinguishable on the wire by design).
58
+ #
59
+ # @example Sender-name display
60
+ # AtlasRb::User.find_by_nuid("001234567")
61
+ # # => { "nuid" => "001234567", "name" => "Doe, Jane" }
62
+ def self.find_by_nuid(target_nuid, nuid: nil)
63
+ response = connection({}, nuid).get("#{ROUTE}/by_nuid/#{target_nuid}")
64
+ return nil if response.status == 404
65
+
66
+ AtlasRb::Mash.new(JSON.parse(response.body))
67
+ end
68
+
69
+ # Batch-resolve a set of NUIDs to directory entries in one call.
70
+ #
71
+ # Same response shape as {.search}. Unresolvable NUIDs (unknown or
72
+ # excluded-role) are dropped, so the result may be shorter than the
73
+ # input — callers index by `nuid`. Atlas caps the batch at 100.
74
+ #
75
+ # @param nuids [Array<String>, String] the NUIDs to resolve.
76
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
77
+ # `User:` header. Required for cerberus-token requests; legacy bearer
78
+ # tokens still resolve without it.
79
+ # @return [Array<AtlasRb::Mash>] resolved entries, each carrying `nuid`
80
+ # and `name`, ordered by name.
81
+ #
82
+ # @example Resolve an inbox page of senders in one round-trip
83
+ # senders = AtlasRb::User.resolve(["001234567", "007654321"])
84
+ # by_nuid = senders.index_by { |entry| entry["nuid"] }
85
+ def self.resolve(nuids, nuid: nil)
86
+ JSON.parse(
87
+ connection({ nuids: Array(nuids).join(",") }, nuid).get(ROUTE)&.body
88
+ ).map { |entry| AtlasRb::Mash.new(entry) }
89
+ end
90
+ end
91
+ end
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/user"
21
22
  require_relative "atlas_rb/admin"
22
23
  require_relative "atlas_rb/admin/work"
23
24
  require_relative "atlas_rb/admin/collection"
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.0
4
+ version: 1.3.2
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-06 00:00:00.000000000 Z
11
+ date: 2026-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -134,6 +134,7 @@ files:
134
134
  - lib/atlas_rb/middleware/raise_on_stale_resource.rb
135
135
  - lib/atlas_rb/resource.rb
136
136
  - lib/atlas_rb/system/user.rb
137
+ - lib/atlas_rb/user.rb
137
138
  - lib/atlas_rb/version.rb
138
139
  - lib/atlas_rb/work.rb
139
140
  - sig/atlas_rb.rbs