atlas_rb 0.0.81 → 0.0.83

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: 4041d1c545afb6a983fbedda8d91fd2f36abdc15480d551e0156e34b23828ec3
4
- data.tar.gz: 0317b9b4967fed169c969196d9fa6dc35a83b081a58bfb03ad8ff72239c73cfa
3
+ metadata.gz: 0a7a8531fb28a840a529a888d8d5b789ea9ba69aed95fa59d5899f4aea42afd8
4
+ data.tar.gz: 2f545fdddc0d22cdb3def311ed73abcb946524bffb0a3d55952e1cee9226eb10
5
5
  SHA512:
6
- metadata.gz: 76227a82a7a1d1afd5b99cbbf258f3376780fb43993008e513a85b898f0b9699b89c8512256bb7d10ded352de295669439ad09fdbf1fc672cf6c041e6dafdbe8
7
- data.tar.gz: 97222b89cf90ac7fcf8132c7a380baa569d6a8ce908b9bd041c36b2fb6bfb66cbcb9e81c018613316d6088bd967fc5c6f0201c6959440283981d3622faff8c7f
6
+ metadata.gz: 5fbc732c7f405ffe14c22169a38e8037eec302a4572f691ca41e2ebd8f5450aad5c33eab8d0b712dacf9295b62d364178ace4caa9af5d932c65f6f2ca04e9044
7
+ data.tar.gz: 4454c5562263f3401d9cf10764594b7acaf8d978f6264d6738d8f9324abe8b1e9542ce009559c59e3ee31ac36e2fee4a32222912efd43c6fdc06c798d8105a0e
data/.version CHANGED
@@ -1 +1 @@
1
- 0.0.81
1
+ 0.0.83
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --readme README.md
2
+ --markup markdown
3
+ --output-dir doc
4
+ --no-private
5
+ --protected
6
+ lib/**/*.rb
7
+ -
8
+ LICENSE.txt
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (0.0.81)
4
+ atlas_rb (0.0.83)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
@@ -40,6 +40,7 @@ GEM
40
40
  rspec-support (~> 3.13.0)
41
41
  rspec-support (3.13.3)
42
42
  uri (1.1.1)
43
+ yard (0.9.43)
43
44
 
44
45
  PLATFORMS
45
46
  x86_64-linux
@@ -48,6 +49,7 @@ DEPENDENCIES
48
49
  atlas_rb!
49
50
  rake (~> 13.0)
50
51
  rspec (~> 3.0)
52
+ yard (~> 0.9)
51
53
 
52
54
  BUNDLED WITH
53
55
  2.2.33
data/README.md CHANGED
@@ -1,39 +1,132 @@
1
- # AtlasRb
1
+ # atlas_rb
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/atlas_rb`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Ruby client for the **Atlas** API Northeastern University's institutional
4
+ digital repository.
4
5
 
5
- TODO: Delete this and the text above, and describe your gem
6
+ The gem wraps Atlas's REST endpoints in a small set of class-method-only
7
+ modules, one per resource type. There is no client object to instantiate;
8
+ calls are made directly on the resource class:
9
+
10
+ ```ruby
11
+ AtlasRb::Work.find("w-789")
12
+ ```
6
13
 
7
14
  ## Installation
8
15
 
9
- Add this line to your application's Gemfile:
16
+ Add to your Gemfile:
10
17
 
11
18
  ```ruby
12
- gem 'atlas_rb'
19
+ gem "atlas_rb"
13
20
  ```
14
21
 
15
- And then execute:
22
+ Then `bundle install`, or install standalone with `gem install atlas_rb`.
23
+
24
+ ## Configuration
16
25
 
17
- $ bundle install
26
+ Every request reads two environment variables:
18
27
 
19
- Or install it yourself as:
28
+ | Variable | Purpose |
29
+ |---------------|---------------------------------------------------------------|
30
+ | `ATLAS_URL` | Base URL of the Atlas API (e.g. `https://atlas.example.edu`). |
31
+ | `ATLAS_TOKEN` | Bearer token used in the `Authorization` header. |
20
32
 
21
- $ gem install atlas_rb
33
+ User-scoped calls (currently only `AtlasRb::Authentication`) additionally
34
+ accept an NUID — the Northeastern University ID — which is forwarded in a
35
+ `User: NUID <nuid>` header.
22
36
 
23
- ## Usage
37
+ ```ruby
38
+ ENV["ATLAS_URL"] = "https://atlas.example.edu"
39
+ ENV["ATLAS_TOKEN"] = "..."
40
+ ```
24
41
 
25
- TODO: Write usage instructions here
42
+ ## Resource hierarchy
26
43
 
27
- ## Development
44
+ ```
45
+ Community → Collection → Work
46
+
47
+ FileSet
48
+
49
+ Blob
50
+ ```
51
+
52
+ | Class | Represents |
53
+ |------------------------|---------------------------------------------------------------------------|
54
+ | `AtlasRb::Community` | Top-level org unit; may nest sub-Communities. |
55
+ | `AtlasRb::Collection` | Holds Works; lives directly under a Community. |
56
+ | `AtlasRb::Work` | Bibliographic unit (article, thesis, dataset…); MODS metadata lives here. |
57
+ | `AtlasRb::FileSet` | Classified slot under a Work (e.g. `"primary"`, `"supplemental"`). |
58
+ | `AtlasRb::Blob` | The binary bytes; supports streaming downloads. |
59
+ | `AtlasRb::Authentication` | NUID → user record / group lookup. |
60
+ | `AtlasRb::Resource` | Generic resolver and permissions lookup. |
61
+ | `AtlasRb::Reset` | Test-only — wipes Atlas state via `GET /reset`. |
62
+
63
+ ### A note on `create` argument shapes
64
+
65
+ The CRUD-twin classes look the same but pass different parent IDs:
28
66
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
67
+ ```ruby
68
+ AtlasRb::Community.create(nil) # top-level community (parent_id: nil)
69
+ AtlasRb::Community.create("c-123") # sub-community of c-123
70
+ AtlasRb::Collection.create("c-123") # collection under community c-123
71
+ AtlasRb::Work.create("col-456") # work under collection col-456 (collection_id, not parent_id)
72
+ AtlasRb::FileSet.create("w-789", "primary") # file_set under work w-789, classification "primary"
73
+ AtlasRb::Blob.create("w-789", path, name) # blob under work w-789 with original filename preserved
74
+ ```
30
75
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
76
+ ## End-to-end example
77
+
78
+ ```ruby
79
+ require "atlas_rb"
32
80
 
33
- ## Contributing
81
+ ENV["ATLAS_URL"] = "https://atlas.example.edu"
82
+ ENV["ATLAS_TOKEN"] = "..."
83
+
84
+ # 1. Build the org structure (each create can optionally seed MODS metadata).
85
+ community = AtlasRb::Community.create(nil, "/tmp/community-mods.xml")
86
+ collection = AtlasRb::Collection.create(community["id"], "/tmp/coll-mods.xml")
87
+ work = AtlasRb::Work.create(collection["id"], "/tmp/work-mods.xml")
88
+
89
+ # 2. Upload a binary attached to the work, preserving the user-facing filename.
90
+ blob = AtlasRb::Blob.create(work["id"], "/tmp/upload.tmp", "thesis.pdf")
91
+
92
+ # 3. List everything attached to the work.
93
+ AtlasRb::Work.files(work["id"])
94
+
95
+ # 4. Stream the binary back without buffering it in memory.
96
+ File.open("out.pdf", "wb") do |f|
97
+ headers = AtlasRb::Blob.content(blob["id"]) { |chunk| f.write(chunk) }
98
+ puts headers["content-type"]
99
+ end
100
+
101
+ # 5. Look up the acting user and their groups.
102
+ AtlasRb::Authentication.login("001234567")
103
+ AtlasRb::Authentication.groups("001234567")
104
+ ```
105
+
106
+ ## Generated documentation
107
+
108
+ Full API reference, including `@param` / `@return` / `@example` for every
109
+ method, is generated with [YARD](https://yardoc.org/):
110
+
111
+ ```bash
112
+ bundle exec yard doc
113
+ open doc/index.html
114
+ ```
115
+
116
+ `yard stats --list-undoc` should report 100% coverage.
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ bin/setup # install dependencies
122
+ bin/console # IRB with atlas_rb loaded
123
+ bundle exec rspec # run tests
124
+ bundle exec rubocop # lint
125
+ ```
34
126
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/atlas_rb.
127
+ To cut a release, bump the version in `.version` (which `lib/atlas_rb/version.rb`
128
+ reads at load time) and run `bundle exec rake release`.
36
129
 
37
130
  ## License
38
131
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
132
+ MIT see [LICENSE.txt](LICENSE.txt).
@@ -1,15 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # User-facing identity lookups against the Atlas API.
5
+ #
6
+ # Unlike the resource classes, {Authentication} threads a real NUID into the
7
+ # `User` header via {FaradayHelper#connection}'s second positional argument.
8
+ # The Atlas server uses that NUID — combined with the bearer token from
9
+ # `ATLAS_TOKEN` — to resolve the acting user and their group memberships.
10
+ #
11
+ # No login round-trip happens here today; the bearer token is assumed to be
12
+ # already provisioned out-of-band. The commented-out code in this file
13
+ # reflects an older flow where a `/token` endpoint exchanged an NUID for a
14
+ # session token.
4
15
  class Authentication
5
16
  extend AtlasRb::FaradayHelper
6
17
 
18
+ # Look up the Atlas user record for an NUID.
19
+ #
20
+ # @param nuid [String] the user's Northeastern University ID.
21
+ # @return [Hash] the user record returned by `GET /user`, including at
22
+ # minimum `"id"`, `"name"`, and `"groups"`.
23
+ # @raise [JSON::ParserError] if the response body is not valid JSON
24
+ # (typically caused by an auth failure returning HTML).
25
+ #
26
+ # @example
27
+ # AtlasRb::Authentication.login("001234567")
28
+ # # => { "id" => 42, "name" => "Jane Doe", "groups" => [...] }
7
29
  def self.login(nuid)
8
30
  # JSON.parse(connection({ nuid: nuid }).post('/token')&.body)["token"]
9
31
  # need hash - id, name, token => ...
10
32
  JSON.parse(connection({}, nuid).get('/user')&.body)
11
33
  end
12
34
 
35
+ # Fetch only the group memberships for an NUID.
36
+ #
37
+ # Convenience wrapper around the same `GET /user` call as {.login}; useful
38
+ # when authorization checks only need group names.
39
+ #
40
+ # @param nuid [String] the user's Northeastern University ID.
41
+ # @return [Array<Hash>] the `"groups"` array from the user record.
42
+ # @raise [JSON::ParserError] if the response body is not valid JSON.
43
+ #
44
+ # @example
45
+ # AtlasRb::Authentication.groups("001234567")
46
+ # # => [{ "id" => 7, "name" => "Library Staff" }, ...]
13
47
  def self.groups(nuid)
14
48
  # user_details = login(nuid)
15
49
  # token = user_details[:token] ...
data/lib/atlas_rb/blob.rb CHANGED
@@ -1,13 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # The binary content backing a {FileSet} (or attached directly to a {Work}).
5
+ #
6
+ # Blobs are the bytes-on-disk layer of the hierarchy. Operations on this
7
+ # class deal with raw octet streams: uploading new content, replacing
8
+ # content on an existing Blob, and **streaming** downloads via a chunk
9
+ # handler so very large files don't have to be buffered in memory.
10
+ #
11
+ # See also: {Work}, {FileSet}.
4
12
  class Blob < Resource
13
+ # Atlas REST endpoint prefix for this resource.
14
+ # @api private
5
15
  ROUTE = "/files/"
6
16
 
17
+ # Fetch a single Blob's metadata record (not its bytes — see {.content}).
18
+ #
19
+ # @param id [String] the Blob ID.
20
+ # @return [Hash] the `"blob"` object, already unwrapped — typically
21
+ # includes `"id"`, `"original_filename"`, `"size"`, and a download URL.
22
+ #
23
+ # @example
24
+ # AtlasRb::Blob.find("b-321")
25
+ # # => { "id" => "b-321", "original_filename" => "scan.pdf", ... }
7
26
  def self.find(id)
8
27
  JSON.parse(connection({}).get(ROUTE + id)&.body)['blob']
9
28
  end
10
29
 
30
+ # Stream the Blob's binary content through a caller-supplied block.
31
+ #
32
+ # The body is **not** buffered — each chunk Faraday receives is yielded
33
+ # to `chunk_handler` immediately, making this safe for files larger than
34
+ # available memory. The first chunk's response headers are captured and
35
+ # returned so callers can inspect `Content-Type`, `Content-Length`, etc.
36
+ #
37
+ # @param id [String] the Blob ID.
38
+ # @yieldparam chunk [String] the next chunk of binary data.
39
+ # @return [Hash] the response headers from `GET /files/<id>/content`.
40
+ #
41
+ # @example Stream to disk
42
+ # File.open("/tmp/out.pdf", "wb") do |f|
43
+ # headers = AtlasRb::Blob.content("b-321") { |chunk| f.write(chunk) }
44
+ # puts headers["content-type"]
45
+ # end
46
+ def self.content(id, &chunk_handler)
47
+ headers = {}
48
+ connection({}).get("#{ROUTE}#{id}/content") do |req|
49
+ req.options.on_data = proc do |chunk, _bytes_received, env|
50
+ headers = env.response_headers if headers.empty? && env
51
+ chunk_handler.call(chunk)
52
+ end
53
+ end
54
+ headers
55
+ end
56
+
57
+ # Upload a new Blob attached to a Work.
58
+ #
59
+ # `original_filename` is preserved separately from the upload's
60
+ # `File.basename(blob_path)` because the on-disk path is often a temp
61
+ # file name (`RackMultipart...tmp`) — Atlas needs the user-facing name
62
+ # for download UX.
63
+ #
64
+ # @param id [String] the parent Work ID.
65
+ # @param blob_path [String] path to the binary file on disk to upload.
66
+ # @param original_filename [String] the user-facing filename Atlas
67
+ # should record (e.g. `"final_thesis.pdf"`).
68
+ # @return [Hash] the created `"blob"` payload, including its `"id"`.
69
+ #
70
+ # @example
71
+ # AtlasRb::Blob.create("w-789", "/tmp/upload.tmp", "final_thesis.pdf")
72
+ # # => { "id" => "b-321", "original_filename" => "final_thesis.pdf", ... }
11
73
  def self.create(id, blob_path, original_filename)
12
74
  payload = { work_id: id,
13
75
  original_filename: original_filename,
@@ -18,10 +80,29 @@ module AtlasRb
18
80
  JSON.parse(multipart({}).post(ROUTE, payload)&.body)['blob']
19
81
  end
20
82
 
83
+ # Delete a Blob (the bytes *and* the metadata record).
84
+ #
85
+ # @param id [String] the Blob ID.
86
+ # @return [Faraday::Response] the raw delete response.
87
+ #
88
+ # @example
89
+ # AtlasRb::Blob.destroy("b-321")
21
90
  def self.destroy(id)
22
91
  connection({}).delete(ROUTE + id)
23
92
  end
24
93
 
94
+ # Replace the bytes of an existing Blob in-place.
95
+ #
96
+ # The Blob ID is preserved; only the underlying content changes. The
97
+ # original filename is *not* updated by this call — use a new
98
+ # {.create} if you need a different `original_filename`.
99
+ #
100
+ # @param id [String] the Blob ID.
101
+ # @param blob_path [String] path to the replacement binary on disk.
102
+ # @return [Hash] the parsed JSON response from the patch.
103
+ #
104
+ # @example
105
+ # AtlasRb::Blob.update("b-321", "/tmp/revised.pdf")
25
106
  def self.update(id, blob_path)
26
107
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(blob_path),
27
108
  "application/octet-stream",
@@ -1,13 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # A grouping of {Work}s within a {Community}.
5
+ #
6
+ # Collections are the leaf containers in the organizational tree — they hold
7
+ # Works directly and cannot contain other Collections. Every Collection has
8
+ # exactly one parent Community.
9
+ #
10
+ # See also: {Community}, {Work}.
4
11
  class Collection < Resource
12
+ # Atlas REST endpoint prefix for this resource.
13
+ # @api private
5
14
  ROUTE = "/collections/"
6
15
 
16
+ # Fetch a single Collection by ID.
17
+ #
18
+ # @param id [String] the Collection ID.
19
+ # @return [Hash] the `"collection"` object, already unwrapped from the
20
+ # JSON response.
21
+ #
22
+ # @example
23
+ # AtlasRb::Collection.find("col-456")
24
+ # # => { "id" => "col-456", "title" => "Faculty Publications", ... }
7
25
  def self.find(id)
8
26
  JSON.parse(connection({}).get(ROUTE + id)&.body)["collection"]
9
27
  end
10
28
 
29
+ # Create a new Collection under an existing Community.
30
+ #
31
+ # **Note**: unlike {Community.create}, the `id` parameter here is the
32
+ # parent **Community** ID (not a parent Collection ID — Collections do
33
+ # not nest).
34
+ #
35
+ # @param id [String] the parent Community ID.
36
+ # @param xml_path [String, nil] optional path to a MODS XML file used to
37
+ # seed metadata. When given, the Collection is created and immediately
38
+ # patched with the metadata in the file.
39
+ # @return [Hash] the created Collection payload (post-update if
40
+ # `xml_path` was supplied).
41
+ #
42
+ # @example
43
+ # AtlasRb::Collection.create("c-123", "/tmp/collection-mods.xml")
11
44
  def self.create(id, xml_path = nil)
12
45
  result = JSON.parse(connection({ parent_id: id }).post(ROUTE)&.body)["collection"]
13
46
  return result unless xml_path.present?
@@ -16,14 +49,36 @@ module AtlasRb
16
49
  find(result["id"])
17
50
  end
18
51
 
52
+ # Delete a Collection.
53
+ #
54
+ # @param id [String] the Collection ID.
55
+ # @return [Faraday::Response] the raw delete response.
56
+ #
57
+ # @example
58
+ # AtlasRb::Collection.destroy("col-456")
19
59
  def self.destroy(id)
20
60
  connection({}).delete(ROUTE + id)
21
61
  end
22
62
 
63
+ # List the Works in a Collection.
64
+ #
65
+ # @param id [String] the Collection ID.
66
+ # @return [Hash] the child listing from `GET /collections/<id>/children`.
67
+ #
68
+ # @example
69
+ # AtlasRb::Collection.children("col-456")
23
70
  def self.children(id)
24
71
  JSON.parse(connection({}).get(ROUTE + id + '/children')&.body)
25
72
  end
26
73
 
74
+ # Replace a Collection's metadata by uploading a MODS XML document.
75
+ #
76
+ # @param id [String] the Collection ID.
77
+ # @param xml_path [String] path to a MODS XML file on disk.
78
+ # @return [Hash] the parsed JSON response from the patch.
79
+ #
80
+ # @example
81
+ # AtlasRb::Collection.update("col-456", "/tmp/collection-mods.xml")
27
82
  def self.update(id, xml_path)
28
83
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
29
84
  "application/xml",
@@ -31,10 +86,27 @@ module AtlasRb
31
86
  JSON.parse(multipart({}).patch(ROUTE + id, payload)&.body)
32
87
  end
33
88
 
89
+ # Patch individual metadata fields without uploading a full MODS document.
90
+ #
91
+ # @param id [String] the Collection ID.
92
+ # @param values [Hash] field-level metadata updates.
93
+ # @return [Hash] the parsed JSON response.
94
+ #
95
+ # @example
96
+ # AtlasRb::Collection.metadata("col-456", title: "Renamed Collection")
34
97
  def self.metadata(id, values)
35
98
  JSON.parse(connection({ metadata: values }).patch(ROUTE + id)&.body)
36
99
  end
37
100
 
101
+ # Fetch the Collection's MODS representation in the requested format.
102
+ #
103
+ # @param id [String] the Collection ID.
104
+ # @param kind [String, nil] one of `"json"` (default), `"html"`, or
105
+ # `"xml"`.
106
+ # @return [String] the raw response body in the requested format.
107
+ #
108
+ # @example
109
+ # AtlasRb::Collection.mods("col-456", "xml")
38
110
  def self.mods(id, kind = nil)
39
111
  # json default, html, xml
40
112
  connection({}).get(
@@ -1,13 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # A top-level grouping in the Atlas hierarchy.
5
+ #
6
+ # Communities are organizational containers — they hold {Collection}s and,
7
+ # optionally, sub-Communities. Most institutional structure (departments,
8
+ # programs, projects) is modeled as a tree of Communities with Collections
9
+ # at the leaves.
10
+ #
11
+ # See also: {Collection}, {Work}.
4
12
  class Community < Resource
13
+ # Atlas REST endpoint prefix for this resource.
14
+ # @api private
5
15
  ROUTE = "/communities/"
6
16
 
17
+ # Fetch a single Community by ID.
18
+ #
19
+ # @param id [String] the Community ID.
20
+ # @return [Hash] the `"community"` object from the JSON response,
21
+ # already unwrapped.
22
+ #
23
+ # @example
24
+ # AtlasRb::Community.find("c-123")
25
+ # # => { "id" => "c-123", "title" => "College of Engineering", ... }
7
26
  def self.find(id)
8
27
  JSON.parse(connection({}).get(ROUTE + id)&.body)["community"]
9
28
  end
10
29
 
30
+ # Create a new Community, optionally seeded with MODS metadata.
31
+ #
32
+ # Pass `id = nil` to create a top-level Community; pass a Community ID to
33
+ # nest the new Community beneath an existing one.
34
+ #
35
+ # @param id [String, nil] the parent Community ID, or `nil` for a
36
+ # top-level Community.
37
+ # @param xml_path [String, nil] optional path to a MODS XML file. When
38
+ # given, the Community is created and immediately patched with the
39
+ # metadata in the file; the returned Hash reflects the patched state.
40
+ # @return [Hash] the created Community payload (post-update if `xml_path`
41
+ # was supplied).
42
+ #
43
+ # @example Top-level community, no metadata
44
+ # AtlasRb::Community.create(nil)
45
+ #
46
+ # @example Sub-community seeded from MODS
47
+ # AtlasRb::Community.create("c-parent", "/tmp/dept-mods.xml")
11
48
  def self.create(id = nil, xml_path = nil)
12
49
  result = JSON.parse(connection({ parent_id: id }).post(ROUTE)&.body)["community"]
13
50
  return result unless xml_path.present?
@@ -16,14 +53,36 @@ module AtlasRb
16
53
  find(result["id"])
17
54
  end
18
55
 
56
+ # Delete a Community.
57
+ #
58
+ # @param id [String] the Community ID.
59
+ # @return [Faraday::Response] the raw delete response.
60
+ #
61
+ # @example
62
+ # AtlasRb::Community.destroy("c-123")
19
63
  def self.destroy(id)
20
64
  connection({}).delete(ROUTE + id)
21
65
  end
22
66
 
67
+ # List the immediate children (sub-Communities and Collections) of a Community.
68
+ #
69
+ # @param id [String] the parent Community ID.
70
+ # @return [Hash] the child listing from `GET /communities/<id>/children`.
71
+ #
72
+ # @example
73
+ # AtlasRb::Community.children("c-123")
23
74
  def self.children(id)
24
75
  JSON.parse(connection({}).get(ROUTE + id + '/children')&.body)
25
76
  end
26
77
 
78
+ # Replace a Community's metadata by uploading a MODS XML document.
79
+ #
80
+ # @param id [String] the Community ID.
81
+ # @param xml_path [String] path to a MODS XML file on disk.
82
+ # @return [Hash] the parsed JSON response from the patch.
83
+ #
84
+ # @example
85
+ # AtlasRb::Community.update("c-123", "/tmp/community-mods.xml")
27
86
  def self.update(id, xml_path)
28
87
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
29
88
  "application/xml",
@@ -31,10 +90,30 @@ module AtlasRb
31
90
  JSON.parse(multipart({}).patch(ROUTE + id, payload)&.body)
32
91
  end
33
92
 
93
+ # Patch individual metadata fields without uploading a full MODS document.
94
+ #
95
+ # @param id [String] the Community ID.
96
+ # @param values [Hash] field-level metadata updates (shape determined by
97
+ # the Atlas server, typically a mapping from MODS field name to value).
98
+ # @return [Hash] the parsed JSON response.
99
+ #
100
+ # @example
101
+ # AtlasRb::Community.metadata("c-123", title: "New Name")
34
102
  def self.metadata(id, values)
35
103
  JSON.parse(connection({ metadata: values }).patch(ROUTE + id)&.body)
36
104
  end
37
105
 
106
+ # Fetch the Community's MODS representation in the requested format.
107
+ #
108
+ # @param id [String] the Community ID.
109
+ # @param kind [String, nil] one of `"json"` (default when omitted),
110
+ # `"html"`, or `"xml"`. When `nil`, Atlas returns its default
111
+ # representation.
112
+ # @return [String] the raw response body (JSON, HTML, or XML serialized
113
+ # as a string).
114
+ #
115
+ # @example HTML rendering for display
116
+ # AtlasRb::Community.mods("c-123", "html")
38
117
  def self.mods(id, kind = nil)
39
118
  # json default, html, xml
40
119
  connection({}).get(
@@ -1,7 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # HTTP transport helpers shared by every resource class.
5
+ #
6
+ # Every Atlas request reads two environment variables:
7
+ #
8
+ # - `ATLAS_URL` — base URL of the Atlas API (e.g. `https://atlas.example.edu`).
9
+ # - `ATLAS_TOKEN` — bearer token used in the `Authorization` header.
10
+ #
11
+ # Most calls also identify the acting user via a `User: NUID <nuid>` header.
12
+ # Resource classes typically pass `nuid = nil` (anonymous / system context);
13
+ # {AtlasRb::Authentication} is the only place where a real NUID is currently
14
+ # threaded through.
15
+ #
16
+ # The module is mixed in via `extend`, so its methods become class methods on
17
+ # the host (e.g. `AtlasRb::Work.connection({})`).
4
18
  module FaradayHelper
19
+ # Build a JSON-content Faraday connection to the Atlas API.
20
+ #
21
+ # @param params [Hash] query-string / body params to attach to the request.
22
+ # Resource classes use this to pass things like `parent_id:`, `work_id:`,
23
+ # or `metadata:` without manually serializing.
24
+ # @param nuid [String, nil] optional Northeastern University ID to send in
25
+ # the `User` header. Defaults to `nil` (no NUID context).
26
+ # @return [Faraday::Connection] a connection that follows redirects and
27
+ # uses Faraday's default adapter.
28
+ #
29
+ # @example Fetching a community
30
+ # AtlasRb::Community.connection({}).get('/communities/abc123')
5
31
  def connection(params, nuid=nil)
6
32
  Faraday.new(
7
33
  url: ENV.fetch("ATLAS_URL", nil),
@@ -17,6 +43,24 @@ module AtlasRb
17
43
  end
18
44
  end
19
45
 
46
+ # Build a multipart Faraday connection used for binary and XML uploads.
47
+ #
48
+ # The same `ATLAS_URL` / `ATLAS_TOKEN` env vars apply. Unlike {#connection},
49
+ # the `Content-Type` is set automatically by the multipart middleware, and
50
+ # callers pass a payload hash whose values may include
51
+ # `Faraday::Multipart::FilePart` instances.
52
+ #
53
+ # @param nuid [String, nil] optional NUID for the `User` header.
54
+ # @return [Faraday::Connection] a multipart-aware connection.
55
+ #
56
+ # @example Posting a binary blob
57
+ # payload = {
58
+ # work_id: "w-123",
59
+ # binary: Faraday::Multipart::FilePart.new(File.open("scan.pdf"),
60
+ # "application/octet-stream",
61
+ # "scan.pdf")
62
+ # }
63
+ # AtlasRb::Blob.multipart({}).post('/files/', payload)
20
64
  def multipart(nuid=nil)
21
65
  Faraday.new(
22
66
  url: ENV.fetch("ATLAS_URL", nil),
@@ -1,21 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # An ordered, classified slot under a {Work} that holds a {Blob}.
5
+ #
6
+ # FileSets give a Work multiple distinct files (e.g. a primary PDF, a
7
+ # supplemental dataset, a thumbnail) and tag each with a `classification`
8
+ # so the UI knows how to display it. The actual binary content lives on
9
+ # the associated {Blob}.
10
+ #
11
+ # See also: {Work}, {Blob}.
4
12
  class FileSet < Resource
13
+ # Atlas REST endpoint prefix for this resource.
14
+ # @api private
5
15
  ROUTE = "/file_sets/"
6
16
 
17
+ # Fetch a single FileSet by ID.
18
+ #
19
+ # @param id [String] the FileSet ID.
20
+ # @return [Hash] the `"file_set"` object, already unwrapped.
21
+ #
22
+ # @example
23
+ # AtlasRb::FileSet.find("fs-001")
7
24
  def self.find(id)
8
25
  JSON.parse(connection({}).get(ROUTE + id)&.body)["file_set"]
9
26
  end
10
27
 
28
+ # Create a new FileSet under a Work.
29
+ #
30
+ # @param id [String] the parent Work ID.
31
+ # @param classification [String] role tag for the FileSet — e.g.
32
+ # `"primary"`, `"supplemental"`, `"thumbnail"`. The exact set is
33
+ # defined by the Atlas server.
34
+ # @return [Hash] the created `"file_set"` payload, including its `"id"`
35
+ # which can then be passed to {.update} to attach a binary.
36
+ #
37
+ # @example
38
+ # fs = AtlasRb::FileSet.create("w-789", "primary")
39
+ # AtlasRb::FileSet.update(fs["id"], "/tmp/article.pdf")
11
40
  def self.create(id, classification)
12
41
  JSON.parse(connection({ work_id: id, classification: classification }).post(ROUTE)&.body)["file_set"]
13
42
  end
14
43
 
44
+ # Delete a FileSet.
45
+ #
46
+ # @param id [String] the FileSet ID.
47
+ # @return [Faraday::Response] the raw delete response.
48
+ #
49
+ # @example
50
+ # AtlasRb::FileSet.destroy("fs-001")
15
51
  def self.destroy(id)
16
52
  connection({}).delete(ROUTE + id)
17
53
  end
18
54
 
55
+ # Attach (or replace) the binary content backing this FileSet.
56
+ #
57
+ # The body is uploaded as `application/octet-stream` regardless of the
58
+ # file's true type — Atlas inspects the content server-side. To upload
59
+ # a binary blob *plus* an original filename, use {Blob.create} directly
60
+ # against the underlying `/files/` endpoint.
61
+ #
62
+ # @param id [String] the FileSet ID.
63
+ # @param blob_path [String] path to the binary file on disk.
64
+ # @return [Hash] the parsed JSON response from the patch.
65
+ #
66
+ # @example
67
+ # AtlasRb::FileSet.update("fs-001", "/tmp/article.pdf")
19
68
  def self.update(id, blob_path)
20
69
  # Need to figure out blob vs XML
21
70
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(blob_path),
@@ -1,15 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # Abstract base for every Atlas resource type.
5
+ #
6
+ # Subclasses define a `ROUTE` constant (e.g. `"/communities/"`) and override
7
+ # whichever of `find / create / destroy / update / metadata / mods` apply.
8
+ # The {Resource} class itself ships three endpoints that are not
9
+ # type-specific: a generic resolver, an XML preview helper, and a
10
+ # permissions lookup.
11
+ #
12
+ # The Atlas resource hierarchy is:
13
+ #
14
+ # {Community} → {Collection} → {Work} → {FileSet} → {Blob}
15
+ #
16
+ # Subclasses extend {FaradayHelper} so that `connection(...)` and
17
+ # `multipart(...)` are available as class methods.
4
18
  class Resource
5
19
  extend AtlasRb::FaradayHelper
6
20
 
21
+ # Resolve any Atlas resource by ID without knowing its type up front.
22
+ #
23
+ # The Atlas server returns a single-key JSON object whose key names the
24
+ # resource type (`"community"`, `"collection"`, `"work"`, etc.); this
25
+ # method splits that into a normalized `{ "klass" => ..., "resource" => ... }`
26
+ # pair so callers can dispatch on type.
27
+ #
28
+ # @param id [String] an Atlas resource ID of any type.
29
+ # @return [Hash{String => String, Hash}] hash with two keys:
30
+ # - `"klass"` — the resource type, capitalized (e.g. `"Work"`).
31
+ # - `"resource"` — the resource payload as a Hash.
32
+ #
33
+ # @example Polymorphic lookup
34
+ # AtlasRb::Resource.find("abc123")
35
+ # # => { "klass" => "Work", "resource" => { "id" => "abc123", "title" => "..." } }
7
36
  def self.find(id)
8
37
  result = JSON.parse(connection({}).get('/resources/' + id)&.body)
9
38
  { "klass" => result.first[0].capitalize,
10
39
  "resource" => result.first[1] }
11
40
  end
12
41
 
42
+ # Validate a MODS XML document against Atlas's schema *without* persisting it.
43
+ #
44
+ # Useful for surfacing validation errors in UIs before the user commits.
45
+ #
46
+ # @param xml_path [String] path to a MODS XML file on disk.
47
+ # @return [String] the raw response body from `POST /resources/preview`
48
+ # — typically a JSON or XML error report.
49
+ #
50
+ # @example
51
+ # AtlasRb::Resource.preview("/tmp/draft-mods.xml")
13
52
  def self.preview(xml_path)
14
53
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
15
54
  "application/xml",
@@ -17,6 +56,15 @@ module AtlasRb
17
56
  multipart({}).post('/resources/preview', payload)&.body
18
57
  end
19
58
 
59
+ # Fetch the access-control entries for a resource.
60
+ #
61
+ # @param id [String] an Atlas resource ID.
62
+ # @return [Hash] the `"resource"` payload from `GET /resources/<id>/permissions`,
63
+ # typically containing read/write/admin grant lists.
64
+ #
65
+ # @example
66
+ # AtlasRb::Resource.permissions("abc123")
67
+ # # => { "id" => "abc123", "read" => [...], "write" => [...] }
20
68
  def self.permissions(id)
21
69
  result = JSON.parse(connection({}).get('/resources/' + id + '/permissions')&.body)["resource"]
22
70
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # Current gem version, read from the `.version` file at the repo root at load time.
4
5
  VERSION = File.read(".version").strip
5
6
  end
data/lib/atlas_rb/work.rb CHANGED
@@ -1,13 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtlasRb
4
+ # The bibliographic unit in Atlas — an article, thesis, dataset, image, etc.
5
+ #
6
+ # A Work belongs to exactly one {Collection} and aggregates one or more
7
+ # {FileSet}s, each of which holds binary content via a {Blob}. MODS metadata
8
+ # is attached at the Work level.
9
+ #
10
+ # See also: {Collection}, {FileSet}, {Blob}.
4
11
  class Work < Resource
12
+ # Atlas REST endpoint prefix for this resource.
13
+ # @api private
5
14
  ROUTE = "/works/"
6
15
 
16
+ # Fetch a single Work by ID.
17
+ #
18
+ # @param id [String] the Work ID.
19
+ # @return [Hash] the `"work"` object, already unwrapped from the JSON
20
+ # response.
21
+ #
22
+ # @example
23
+ # AtlasRb::Work.find("w-789")
24
+ # # => { "id" => "w-789", "title" => "An Article", ... }
7
25
  def self.find(id)
8
26
  JSON.parse(connection({}).get(ROUTE + id)&.body)["work"]
9
27
  end
10
28
 
29
+ # Create a new Work in an existing Collection.
30
+ #
31
+ # **Note**: unlike {Community.create} and {Collection.create}, the `id`
32
+ # parameter here is the parent **Collection** ID. The underlying request
33
+ # uses the `collection_id` query param rather than `parent_id`.
34
+ #
35
+ # @param id [String] the parent Collection ID.
36
+ # @param xml_path [String, nil] optional path to a MODS XML file. When
37
+ # given, the Work is created and immediately patched with the metadata
38
+ # in the file.
39
+ # @return [Hash] the created Work payload (post-update if `xml_path` was
40
+ # supplied).
41
+ #
42
+ # @example Empty work, metadata to be added later
43
+ # AtlasRb::Work.create("col-456")
44
+ #
45
+ # @example Work seeded from MODS
46
+ # AtlasRb::Work.create("col-456", "/tmp/work-mods.xml")
11
47
  def self.create(id, xml_path = nil)
12
48
  result = JSON.parse(connection({ collection_id: id }).post(ROUTE)&.body)["work"]
13
49
  return result unless xml_path.present?
@@ -16,10 +52,25 @@ module AtlasRb
16
52
  find(result["id"])
17
53
  end
18
54
 
55
+ # Delete a Work.
56
+ #
57
+ # @param id [String] the Work ID.
58
+ # @return [Faraday::Response] the raw delete response.
59
+ #
60
+ # @example
61
+ # AtlasRb::Work.destroy("w-789")
19
62
  def self.destroy(id)
20
63
  connection({}).delete(ROUTE + id)
21
64
  end
22
65
 
66
+ # Replace a Work's metadata by uploading a MODS XML document.
67
+ #
68
+ # @param id [String] the Work ID.
69
+ # @param xml_path [String] path to a MODS XML file on disk.
70
+ # @return [Hash] the parsed JSON response from the patch.
71
+ #
72
+ # @example
73
+ # AtlasRb::Work.update("w-789", "/tmp/work-mods.xml")
23
74
  def self.update(id, xml_path)
24
75
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
25
76
  "application/xml",
@@ -27,14 +78,41 @@ module AtlasRb
27
78
  JSON.parse(multipart({}).patch(ROUTE + id, payload)&.body)
28
79
  end
29
80
 
81
+ # Patch individual metadata fields without uploading a full MODS document.
82
+ #
83
+ # @param id [String] the Work ID.
84
+ # @param values [Hash] field-level metadata updates.
85
+ # @return [Hash] the parsed JSON response.
86
+ #
87
+ # @example
88
+ # AtlasRb::Work.metadata("w-789", title: "Revised Title")
30
89
  def self.metadata(id, values)
31
90
  JSON.parse(connection({ metadata: values }).patch(ROUTE + id)&.body)
32
91
  end
33
92
 
93
+ # List the {FileSet}s and {Blob}s attached to a Work.
94
+ #
95
+ # Useful for building download UIs — the response includes enough to
96
+ # render each file's display name, size, and download URL.
97
+ #
98
+ # @param id [String] the Work ID.
99
+ # @return [Hash] the listing from `GET /works/<id>/files`.
100
+ #
101
+ # @example
102
+ # AtlasRb::Work.files("w-789")
34
103
  def self.files(id)
35
104
  JSON.parse(connection({}).get(ROUTE + id + '/files')&.body)
36
105
  end
37
106
 
107
+ # Fetch the Work's MODS representation in the requested format.
108
+ #
109
+ # @param id [String] the Work ID.
110
+ # @param kind [String, nil] one of `"json"` (default), `"html"`, or
111
+ # `"xml"`.
112
+ # @return [String] the raw response body in the requested format.
113
+ #
114
+ # @example
115
+ # AtlasRb::Work.mods("w-789", "html")
38
116
  def self.mods(id, kind = nil)
39
117
  # json default, html, xml
40
118
  connection({}).get(
data/lib/atlas_rb.rb CHANGED
@@ -13,13 +13,73 @@ require_relative "atlas_rb/work"
13
13
  require_relative "atlas_rb/file_set"
14
14
  require_relative "atlas_rb/blob"
15
15
 
16
+ # Ruby client for the Atlas API — Northeastern University's institutional
17
+ # digital repository (the successor to Cerberus).
18
+ #
19
+ # ## Configuration
20
+ #
21
+ # Two environment variables drive every request:
22
+ #
23
+ # - `ATLAS_URL` — base URL of the Atlas API (e.g. `https://atlas.example.edu`).
24
+ # - `ATLAS_TOKEN` — bearer token sent in the `Authorization` header.
25
+ #
26
+ # {AtlasRb::Authentication} additionally accepts an NUID (Northeastern
27
+ # University ID) which is forwarded in a `User: NUID <nuid>` header so the
28
+ # server can resolve the acting user.
29
+ #
30
+ # ## Resource hierarchy
31
+ #
32
+ # {AtlasRb::Community} → {AtlasRb::Collection} → {AtlasRb::Work}
33
+ # ↓
34
+ # {AtlasRb::FileSet}
35
+ # ↓
36
+ # {AtlasRb::Blob}
37
+ #
38
+ # - **Community** — top-level org unit; may nest sub-Communities.
39
+ # - **Collection** — holds Works; lives directly under a Community.
40
+ # - **Work** — the bibliographic unit (article, thesis, dataset…); MODS
41
+ # metadata is attached here.
42
+ # - **FileSet** — classified slot under a Work that owns one Blob.
43
+ # - **Blob** — the binary bytes themselves; supports streaming downloads
44
+ # via {AtlasRb::Blob.content}.
45
+ #
46
+ # ## Quick start
47
+ #
48
+ # @example End-to-end: create a Work and attach a file
49
+ # ENV["ATLAS_URL"] = "https://atlas.example.edu"
50
+ # ENV["ATLAS_TOKEN"] = "..."
51
+ #
52
+ # community = AtlasRb::Community.create(nil, "/tmp/community-mods.xml")
53
+ # collection = AtlasRb::Collection.create(community["id"], "/tmp/coll-mods.xml")
54
+ # work = AtlasRb::Work.create(collection["id"], "/tmp/work-mods.xml")
55
+ # blob = AtlasRb::Blob.create(work["id"],
56
+ # "/tmp/upload.tmp",
57
+ # "thesis.pdf")
58
+ #
59
+ # @example Streaming a download
60
+ # File.open("out.pdf", "wb") do |f|
61
+ # AtlasRb::Blob.content(blob["id"]) { |chunk| f.write(chunk) }
62
+ # end
16
63
  module AtlasRb
64
+ # Generic error raised by future code paths; not currently used by any
65
+ # resource class. Atlas errors today surface as raw `Faraday::Response`
66
+ # objects or `JSON::ParserError`s on malformed bodies.
17
67
  class Error < StandardError; end
18
- # Your code goes here...
19
68
 
69
+ # Test-environment helper that wipes Atlas state via `GET /reset`.
70
+ #
71
+ # **Do not call against production.** This exists so RSpec suites running
72
+ # against a disposable Atlas instance can return to a clean baseline
73
+ # between examples.
20
74
  class Reset
21
75
  extend AtlasRb::FaradayHelper
22
76
 
77
+ # Reset the connected Atlas instance to a clean state.
78
+ #
79
+ # @return [String, nil] the raw response body from `GET /reset`.
80
+ #
81
+ # @example
82
+ # AtlasRb::Reset.clean
23
83
  def self.clean
24
84
  connection({}).get("/reset")&.body
25
85
  end
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: 0.0.81
4
+ version: 0.0.83
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-04-22 00:00:00.000000000 Z
11
+ date: 2026-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.9'
69
83
  description:
70
84
  email:
71
85
  - david.g.cliff@gmail.com
@@ -77,6 +91,7 @@ files:
77
91
  - ".rspec"
78
92
  - ".rubocop.yml"
79
93
  - ".version"
94
+ - ".yardopts"
80
95
  - Gemfile
81
96
  - Gemfile.lock
82
97
  - LICENSE.txt