atlas_rb 1.25.0 → 1.27.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 +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +73 -0
- data/Gemfile.lock +7 -3
- data/README.md +57 -23
- data/lib/atlas_rb/collection.rb +5 -0
- data/lib/atlas_rb/community.rb +5 -0
- data/lib/atlas_rb/compilation.rb +3 -0
- data/lib/atlas_rb/mash.rb +18 -0
- data/lib/atlas_rb/resource.rb +130 -18
- data/lib/atlas_rb/resource_writes.rb +53 -4
- data/lib/atlas_rb/work.rb +38 -0
- data/lib/atlas_rb.rb +3 -3
- metadata +2 -3
- data/lib/atlas_rb/resource_types.rb +0 -60
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40ea8cbef3fa493fefcf29c0dd15a4d7edf72efb58134f8d21050421517ef05f
|
|
4
|
+
data.tar.gz: 36a4eb18338f27da7ee744233e7b54fd6eb20faa76201e907f94703b8c6583e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d33812411e71cfa8bf0031fae4f81b735d598b75cb31f099e0c67a31d8dc0e4cc9c193524443f8dce983f87f96add5c9cc82c6de8c3d1d4211e69611254d4cc6
|
|
7
|
+
data.tar.gz: f287d655a86594da49f54caa8cb505277577b3b81284e313cf956ee6424fd09f3bdf0085ae51fae14420468c386626eba3c300be3f41e5762d57c65de5938213
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.27.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.27.0
|
|
4
|
+
|
|
5
|
+
### Removed — `Resource.class_for` and `Resource::TYPE_MAP`
|
|
6
|
+
|
|
7
|
+
Callers no longer turn a type string into a class themselves. Replace
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
AtlasRb::Resource.class_for(found.klass)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
with
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
found.resource_class
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The lookup no longer accepts `"File_set"`, the spelling `find` emitted
|
|
20
|
+
before 1.17.0, or `"compilation"` / `"Compilation"`, which no Atlas payload
|
|
21
|
+
reports: the resolver covers Valkyrie-backed types only. Each now raises
|
|
22
|
+
`ArgumentError`. `lib/atlas_rb/resource_types.rb` is gone.
|
|
23
|
+
|
|
24
|
+
### Added — `Mash#resource_class`
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
AtlasRb::Resource.find(id).resource_class # => AtlasRb::FileSet
|
|
28
|
+
AtlasRb::Resource.search("whaling").results.first.resource_class
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Every Atlas payload that names a type carries it as `"klass"`, so the method
|
|
32
|
+
lives on the shared `Mash` and covers `find`, `find_many`, `ancestors` nodes
|
|
33
|
+
and the Solr digests. `klass` itself is unchanged.
|
|
34
|
+
|
|
35
|
+
### Added — `Resource.unresolvable!`
|
|
36
|
+
|
|
37
|
+
Every `Resource` subclass now registers itself when it is defined, under its
|
|
38
|
+
class name and its snake-cased wire key. A subclass that Atlas never names in
|
|
39
|
+
a payload opts out by calling `unresolvable!` in its body. `Compilation` is
|
|
40
|
+
the only one.
|
|
41
|
+
|
|
42
|
+
### Added — `mods_editable?`
|
|
43
|
+
|
|
44
|
+
`true` for `Work`, `Collection` and `Community`, `false` for every other
|
|
45
|
+
type. It matches Atlas's `ResourcesController::TYPED_IVARS`, which Atlas
|
|
46
|
+
does not expose on the wire.
|
|
47
|
+
|
|
48
|
+
## 1.26.0
|
|
49
|
+
|
|
50
|
+
These bindings need Atlas 0.6.221 or later.
|
|
51
|
+
|
|
52
|
+
### Added — Darwin Core records
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
AtlasRb::Work.dwc(id, kind = "xml") # "xml", "json" or "html"; nil on 404
|
|
56
|
+
AtlasRb::Resource.put_dwc(id, xml_path, origin: nil)
|
|
57
|
+
AtlasRb::Resource.delete_dwc(id) # true; raises NotFoundError on 404
|
|
58
|
+
AtlasRb::Resource.dwc_versions(id)
|
|
59
|
+
AtlasRb::Resource.dwc_version(id, version_id)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A Work can hold one Simple Darwin Core record. `Work.dwc` returns the stored
|
|
63
|
+
XML by default, the display block for `"html"`, and the Work unwrapped from
|
|
64
|
+
`"work"` for `"json"`, with the terms under `"dwc"`. `put_dwc` replaces the
|
|
65
|
+
whole record and raises `ResourceError` on Atlas's `422` when the document
|
|
66
|
+
breaks a shape rule. `delete_dwc` withdraws the record without purging it, so
|
|
67
|
+
the next `put_dwc` restores it. The version calls mirror `mods_versions` and
|
|
68
|
+
`mods_version`.
|
|
69
|
+
|
|
70
|
+
### Documented — `metadata_formats` on the Work
|
|
71
|
+
|
|
72
|
+
`Work.find` results now carry `metadata_formats`, the additional records the
|
|
73
|
+
Work holds, such as `["dwc"]`. No code changed: the gem passes the whole Work
|
|
74
|
+
through.
|
|
75
|
+
|
|
3
76
|
## 1.25.0
|
|
4
77
|
|
|
5
78
|
These bindings need Atlas 0.6.214 or later.
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
atlas_rb (1.
|
|
4
|
+
atlas_rb (1.27.0)
|
|
5
5
|
faraday (~> 2.7)
|
|
6
6
|
faraday-follow_redirects (~> 0.3.0)
|
|
7
7
|
faraday-multipart (~> 1)
|
|
@@ -38,10 +38,13 @@ GEM
|
|
|
38
38
|
base64
|
|
39
39
|
logger (1.7.0)
|
|
40
40
|
multipart-post (2.4.1)
|
|
41
|
-
net-http (0.
|
|
41
|
+
net-http (0.10.0)
|
|
42
|
+
net-protocol (>= 0.3.0)
|
|
42
43
|
uri (>= 0.11.1)
|
|
43
44
|
net-http-persistent (4.0.8)
|
|
44
45
|
connection_pool (>= 2.2.4, < 4)
|
|
46
|
+
net-protocol (0.4.0)
|
|
47
|
+
timeout
|
|
45
48
|
rake (13.2.1)
|
|
46
49
|
rspec (3.13.0)
|
|
47
50
|
rspec-core (~> 3.13.0)
|
|
@@ -56,7 +59,8 @@ GEM
|
|
|
56
59
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
57
60
|
rspec-support (~> 3.13.0)
|
|
58
61
|
rspec-support (3.13.3)
|
|
59
|
-
|
|
62
|
+
timeout (0.6.1)
|
|
63
|
+
uri (1.1.2)
|
|
60
64
|
yard (0.9.43)
|
|
61
65
|
|
|
62
66
|
PLATFORMS
|
data/README.md
CHANGED
|
@@ -527,6 +527,35 @@ array (mirroring `Collection.children`); the two mutations return the list
|
|
|
527
527
|
Resolving those Collections' full contents is a Cerberus/Solr concern —
|
|
528
528
|
this gem never queries the index.
|
|
529
529
|
|
|
530
|
+
### Darwin Core records
|
|
531
|
+
|
|
532
|
+
A Work can hold one Simple Darwin Core record beside its MODS. The Work's
|
|
533
|
+
`metadata_formats` says whether it does, so check that before reading:
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
work = AtlasRb::Work.find("w-789")
|
|
537
|
+
if work["metadata_formats"].include?("dwc")
|
|
538
|
+
AtlasRb::Work.dwc("w-789", "html") # the display block
|
|
539
|
+
AtlasRb::Work.dwc("w-789", "json")["dwc"]["catalogNumber"] # => "S27880"
|
|
540
|
+
AtlasRb::Work.dwc("w-789") # the stored XML
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
AtlasRb::Resource.put_dwc("w-789", "/tmp/dwc.xml", origin: "xml_loader")
|
|
544
|
+
AtlasRb::Resource.delete_dwc("w-789") # withdraws; the next put_dwc restores
|
|
545
|
+
|
|
546
|
+
history = AtlasRb::Resource.dwc_versions("w-789")
|
|
547
|
+
AtlasRb::Resource.dwc_version("w-789", history["versions"].last["version_id"])
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
`Work.dwc` answers `nil` when the Work holds no record. `put_dwc` raises
|
|
551
|
+
`ResourceError` on a refused document; its body's `error` names the rule
|
|
552
|
+
(`malformed_xml`, `invalid_root`, `record_count` or `duplicate_term`). Atlas
|
|
553
|
+
checks shape only, so validate against `tdwg_dwc_simple.xsd` before you call.
|
|
554
|
+
`delete_dwc` purges nothing: Atlas keeps the document and its history.
|
|
555
|
+
|
|
556
|
+
The two version calls behave as `mods_versions` and `mods_version` do, under
|
|
557
|
+
the same admin gate.
|
|
558
|
+
|
|
530
559
|
The two mutations raise the same way `reparent` does — `LinkedMemberError`
|
|
531
560
|
on a structural `422` (carrying the envelope's `error` code as `#code`) and
|
|
532
561
|
`ForbiddenError` on a `403` — instead of swallowing the envelope.
|
|
@@ -540,6 +569,8 @@ write. The whole surface:
|
|
|
540
569
|
| Call | Endpoint |
|
|
541
570
|
|---|---|
|
|
542
571
|
| `Resource.put_mods(id, xml_path, origin:)` | `PUT /resources/{id}/mods` |
|
|
572
|
+
| `Resource.put_dwc(id, xml_path, origin:)` | `PUT /resources/{id}/dwc` (Works only) |
|
|
573
|
+
| `Resource.delete_dwc(id)` | `DELETE /resources/{id}/dwc` (Works only) |
|
|
543
574
|
| `Resource.set_permissions(id, acl)` | `PATCH /resources/{id}/permissions` |
|
|
544
575
|
| `Resource.set_thumbnails(id, thumbnail:, thumbnail_2x:, preview:)` | `PATCH /resources/{id}/thumbnails` |
|
|
545
576
|
| `Resource.reparent(id, parent_id)` | `PATCH /resources/{id}/parent` |
|
|
@@ -584,34 +615,37 @@ Writes that address something only one type has stay typed — `create`, the
|
|
|
584
615
|
Work-only lifecycle and derivative writes, `Collection.set_featured`, and the
|
|
585
616
|
FileSet, Blob and Compilation writes.
|
|
586
617
|
|
|
587
|
-
### Resolving one id of unknown type (`Resource.find` and `
|
|
618
|
+
### Resolving one id of unknown type (`Resource.find` and `resource_class`)
|
|
588
619
|
|
|
589
620
|
When a NOID arrives as runtime data and you do not know its type, resolve
|
|
590
621
|
it generically and dispatch on the reported type:
|
|
591
622
|
|
|
592
623
|
```ruby
|
|
593
624
|
found = AtlasRb::Resource.find("b8gtjvk")
|
|
594
|
-
found
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
`
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
625
|
+
found.klass # => "FileSet"
|
|
626
|
+
found.resource_class # => AtlasRb::FileSet
|
|
627
|
+
found.resource_class.find(found.resource.id)
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
`resource_class` is the supported way to get from a result to its class.
|
|
631
|
+
It works on every Atlas payload that carries a `"klass"`: `find`,
|
|
632
|
+
`find_many`, `ancestors` nodes, and the `search`, `descendant_works` and
|
|
633
|
+
`Compilation.contents` digests. Keep `klass` when the type has to be
|
|
634
|
+
serialized — a job argument, a log line — and use `resource_class` when
|
|
635
|
+
you act on it. Do not `const_get` into the `AtlasRb` namespace.
|
|
636
|
+
|
|
637
|
+
Every `Resource` subclass registers itself when it is defined, under its
|
|
638
|
+
class name and the key Atlas wraps its JSON in (`FileSet` → `"file_set"`).
|
|
639
|
+
`Compilation` opts out with `unresolvable!`, because the resolver never
|
|
640
|
+
returns one.
|
|
641
|
+
A row naming a type no class registers raises `ArgumentError` rather than
|
|
642
|
+
returning `nil`, so a type this gem does not model fails where the cause
|
|
643
|
+
is.
|
|
644
|
+
|
|
645
|
+
To ask whether a type has an editable MODS document, use
|
|
646
|
+
`mods_editable?`: it is true for `Work`, `Collection` and `Community` only.
|
|
647
|
+
`Resource.mods` answers for every type, so a successful read does not mean
|
|
648
|
+
a write will be accepted.
|
|
615
649
|
|
|
616
650
|
**What the resolver covers.** Atlas answers `/resources/:id` for its
|
|
617
651
|
Valkyrie-backed types only: `Work`, `Collection`, `Community`, `FileSet`,
|
|
@@ -651,7 +685,7 @@ result = AtlasRb::Resource.search("whaling logbook", type: "Work", per_page: 50)
|
|
|
651
685
|
result.pagination.total # => 12
|
|
652
686
|
hit = result.results.first
|
|
653
687
|
hit.title # => "Whaling logbook, 1851"
|
|
654
|
-
|
|
688
|
+
hit.resource_class.find(hit.noid)
|
|
655
689
|
```
|
|
656
690
|
|
|
657
691
|
It searches Works, Collections, Communities and People, most relevant first.
|
data/lib/atlas_rb/collection.rb
CHANGED
|
@@ -16,6 +16,11 @@ module AtlasRb
|
|
|
16
16
|
# @api private
|
|
17
17
|
ROUTE = "/collections/"
|
|
18
18
|
|
|
19
|
+
# Atlas accepts a MODS write for this type. See {Resource.mods_editable?}.
|
|
20
|
+
def self.mods_editable?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
# Fetch a single Collection by ID.
|
|
20
25
|
#
|
|
21
26
|
# @param id [String] the Collection ID.
|
data/lib/atlas_rb/community.rb
CHANGED
|
@@ -14,6 +14,11 @@ module AtlasRb
|
|
|
14
14
|
# @api private
|
|
15
15
|
ROUTE = "/communities/"
|
|
16
16
|
|
|
17
|
+
# Atlas accepts a MODS write for this type. See {Resource.mods_editable?}.
|
|
18
|
+
def self.mods_editable?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
# Fetch a single Community by ID.
|
|
18
23
|
#
|
|
19
24
|
# @param id [String] the Community ID.
|
data/lib/atlas_rb/compilation.rb
CHANGED
|
@@ -24,6 +24,9 @@ module AtlasRb
|
|
|
24
24
|
# Atlas REST endpoint prefix for this resource.
|
|
25
25
|
# @api private
|
|
26
26
|
ROUTE = "/compilations/"
|
|
27
|
+
# Atlas's resolver covers Valkyrie-backed types only, and a Compilation is
|
|
28
|
+
# an ActiveRecord row there, so no Atlas payload names one as its klass.
|
|
29
|
+
unresolvable!
|
|
27
30
|
|
|
28
31
|
# Fetch a single Compilation by ID.
|
|
29
32
|
#
|
data/lib/atlas_rb/mash.rb
CHANGED
|
@@ -12,5 +12,23 @@ module AtlasRb
|
|
|
12
12
|
# `type`, `id`).
|
|
13
13
|
class Mash < Hashie::Mash
|
|
14
14
|
disable_warnings
|
|
15
|
+
|
|
16
|
+
# The AtlasRb class named by this row's `"klass"`.
|
|
17
|
+
#
|
|
18
|
+
# Every Atlas payload that names a type carries it as `"klass"`, in the
|
|
19
|
+
# class-name spelling: {Resource.find}, {Resource.find_many}, `ancestors`
|
|
20
|
+
# nodes, and the Solr digests from {Resource.search},
|
|
21
|
+
# {Resource.descendant_works} and {Compilation.contents}. Nested hashes
|
|
22
|
+
# are Mashes too, so an `ancestors` node answers this as well.
|
|
23
|
+
#
|
|
24
|
+
# @return [Class] the AtlasRb resource class.
|
|
25
|
+
# @raise [ArgumentError] when the row has no `"klass"`, or names a type
|
|
26
|
+
# this gem does not register.
|
|
27
|
+
# @example Load a search hit as its full resource
|
|
28
|
+
# hit = AtlasRb::Resource.search("whaling").results.first
|
|
29
|
+
# hit.resource_class.find(hit.noid)
|
|
30
|
+
def resource_class
|
|
31
|
+
Resource.resolve_type(self["klass"])
|
|
32
|
+
end
|
|
15
33
|
end
|
|
16
34
|
end
|
data/lib/atlas_rb/resource.rb
CHANGED
|
@@ -18,6 +18,73 @@ module AtlasRb
|
|
|
18
18
|
class Resource
|
|
19
19
|
extend AtlasRb::FaradayHelper
|
|
20
20
|
|
|
21
|
+
# Every type the Atlas resolver can answer with, filled by {inherited} as
|
|
22
|
+
# each subclass loads and frozen in `atlas_rb.rb` once they all have. Keyed
|
|
23
|
+
# by both spellings Atlas produces: the wire key (`"file_set"`) and the
|
|
24
|
+
# class name Solr indexes as `internal_resource` (`"FileSet"`).
|
|
25
|
+
# @api private
|
|
26
|
+
TYPES = {}
|
|
27
|
+
|
|
28
|
+
# Registers every subclass as a type the resolver can answer with, unless
|
|
29
|
+
# it opts out with {unresolvable!}.
|
|
30
|
+
#
|
|
31
|
+
# The wire key is the class name in snake case, which is the root key
|
|
32
|
+
# Atlas wraps the type's JSON in (`json.file_set do`). That holds for every
|
|
33
|
+
# type today; a type whose wire name ever differs would need a way to say
|
|
34
|
+
# so.
|
|
35
|
+
#
|
|
36
|
+
# An anonymous class, or one defined after the gem has loaded, is not
|
|
37
|
+
# registered: the set of Atlas types is the gem's, and `TYPES` is frozen
|
|
38
|
+
# by then.
|
|
39
|
+
#
|
|
40
|
+
# @api private
|
|
41
|
+
# @return [void]
|
|
42
|
+
def self.inherited(subclass)
|
|
43
|
+
super
|
|
44
|
+
return if subclass.name.nil? || TYPES.frozen?
|
|
45
|
+
|
|
46
|
+
class_name = subclass.name.split("::").last
|
|
47
|
+
TYPES[class_name.gsub(/(?<!\A)([A-Z])/, '_\1').downcase] = subclass
|
|
48
|
+
TYPES[class_name] = subclass
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Removes this class from the types the resolver can answer with. For a
|
|
52
|
+
# subclass that Atlas never names in a payload.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def self.unresolvable!
|
|
56
|
+
TYPES.delete_if { |_, klass| klass == self }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The class registered for a type string, in either spelling.
|
|
60
|
+
#
|
|
61
|
+
# An unknown type raises rather than answering `nil`, so a caller holding a
|
|
62
|
+
# type this gem does not define hears about it here instead of at a
|
|
63
|
+
# `NoMethodError` a few frames later.
|
|
64
|
+
#
|
|
65
|
+
# @api private
|
|
66
|
+
# @param name [String, Symbol] a wire key or class name.
|
|
67
|
+
# @return [Class]
|
|
68
|
+
# @raise [ArgumentError] when no class is registered for that type.
|
|
69
|
+
def self.resolve_type(name)
|
|
70
|
+
TYPES.fetch(name.to_s) do
|
|
71
|
+
raise ArgumentError, "unknown Atlas resource type: #{name.inspect}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Whether Atlas accepts a MODS write for this type.
|
|
76
|
+
#
|
|
77
|
+
# {Resource.mods} is defined here on the base class, so every type answers
|
|
78
|
+
# the read — a FileSet's succeeds and returns nothing a curator can edit.
|
|
79
|
+
# Only {Work}, {Collection} and {Community} take a write, the same set as
|
|
80
|
+
# Atlas's `ResourcesController::TYPED_IVARS`. Atlas does not expose that
|
|
81
|
+
# set on the wire, so it is stated here.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def self.mods_editable?
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
21
88
|
# Resolve any Atlas resource by ID without knowing its type up front.
|
|
22
89
|
#
|
|
23
90
|
# The Atlas server returns a single-key JSON object whose key names the
|
|
@@ -45,12 +112,12 @@ module AtlasRb
|
|
|
45
112
|
# when the id resolves to nothing (`404`):
|
|
46
113
|
# - `"klass"` — the resource type as its class name, e.g. `"Work"`,
|
|
47
114
|
# `"FileSet"`. That is the spelling Solr carries as
|
|
48
|
-
# `internal_resource`, and {
|
|
115
|
+
# `internal_resource`, and {Mash#resource_class} turns it into the class.
|
|
49
116
|
# - `"resource"` — the resource payload as a Hash.
|
|
50
117
|
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
|
|
51
118
|
# auth/validation error envelope), carrying Atlas's status + body.
|
|
52
|
-
# @raise [ArgumentError] when Atlas names a type this gem
|
|
53
|
-
# for. See {Resource.
|
|
119
|
+
# @raise [ArgumentError] when Atlas names a type this gem registers no class
|
|
120
|
+
# for. See {Resource.inherited}.
|
|
54
121
|
#
|
|
55
122
|
# @example Polymorphic lookup
|
|
56
123
|
# AtlasRb::Resource.find("abc123")
|
|
@@ -62,7 +129,7 @@ module AtlasRb
|
|
|
62
129
|
# The class's own name, never `capitalize` over the wire key: `capitalize`
|
|
63
130
|
# answers `"File_set"` for a `file_set`, which is not a constant in this
|
|
64
131
|
# namespace, and callers resolve this string to a class.
|
|
65
|
-
AtlasRb::Mash.new("klass" =>
|
|
132
|
+
AtlasRb::Mash.new("klass" => resolve_type(result.first[0]).name.split("::").last,
|
|
66
133
|
"resource" => result.first[1])
|
|
67
134
|
end
|
|
68
135
|
|
|
@@ -167,8 +234,8 @@ module AtlasRb
|
|
|
167
234
|
# Each row is a digest read off Solr, not a resource: `{ "id", "noid",
|
|
168
235
|
# "klass", "title", "creators", "year", "thumbnail", "in_progress",
|
|
169
236
|
# "embargoed", "incomplete" }`. `klass` is one of the four type names
|
|
170
|
-
# above,
|
|
171
|
-
#
|
|
237
|
+
# above, so a caller loads the full resource with
|
|
238
|
+
# `hit.resource_class.find(hit.noid)`.
|
|
172
239
|
#
|
|
173
240
|
# @param query [String, nil] the search text. Omit or pass blank to browse
|
|
174
241
|
# everything the user may read, newest first.
|
|
@@ -189,7 +256,7 @@ module AtlasRb
|
|
|
189
256
|
# @example Find a Work by a word in its title, then load it
|
|
190
257
|
# result = AtlasRb::Resource.search("whaling logbook", type: "Work")
|
|
191
258
|
# hit = result.results.first
|
|
192
|
-
#
|
|
259
|
+
# hit.resource_class.find(hit.noid)
|
|
193
260
|
# @example Page through every match
|
|
194
261
|
# (1..AtlasRb::Resource.search("whaling").pagination.pages).flat_map do |page|
|
|
195
262
|
# AtlasRb::Resource.search("whaling", page: page).results
|
|
@@ -419,6 +486,47 @@ module AtlasRb
|
|
|
419
486
|
))
|
|
420
487
|
end
|
|
421
488
|
|
|
489
|
+
# List the retained versions of a Work's Darwin Core record.
|
|
490
|
+
#
|
|
491
|
+
# The same envelope as {mods_versions}, under the same admin gate, with
|
|
492
|
+
# attribution drawn from the Darwin Core edits only. A Work with no record,
|
|
493
|
+
# or an id that is not a Work, comes back as `{ "versions" => [] }`.
|
|
494
|
+
#
|
|
495
|
+
# @param id [String] the Work's NOID.
|
|
496
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
497
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
498
|
+
# header.
|
|
499
|
+
# @return [AtlasRb::Mash, nil] the envelope, with `"resource_id"` and a
|
|
500
|
+
# newest-first `"versions"` array; `nil` on a `404`.
|
|
501
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`,
|
|
502
|
+
# including the `403` a non-admin caller gets.
|
|
503
|
+
# @example
|
|
504
|
+
# AtlasRb::Resource.dwc_versions("w-789")["versions"].first["source"] # => "dwc"
|
|
505
|
+
def self.dwc_versions(id, nuid: nil, on_behalf_of: nil)
|
|
506
|
+
read_body(connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
507
|
+
.get('/resources/' + id + '/dwc/versions')) { |body| AtlasRb::Mash.new(body) }
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Fetch a Work's Darwin Core document as of a specific version.
|
|
511
|
+
#
|
|
512
|
+
# XML only, because Atlas overwrites the JSON access copy in place. Pass a
|
|
513
|
+
# `version_id` from {dwc_versions}.
|
|
514
|
+
#
|
|
515
|
+
# @param id [String] the Work's NOID.
|
|
516
|
+
# @param version_id [String] an OCFL version label, e.g. `"v3"`.
|
|
517
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
518
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
519
|
+
# header.
|
|
520
|
+
# @return [String, nil] the raw XML; `nil` on a `404` (unknown version, or
|
|
521
|
+
# no record ever written).
|
|
522
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`.
|
|
523
|
+
# @example
|
|
524
|
+
# AtlasRb::Resource.dwc_version("w-789", "v3")
|
|
525
|
+
def self.dwc_version(id, version_id, nuid: nil, on_behalf_of: nil)
|
|
526
|
+
read_raw(connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
527
|
+
.get('/resources/' + id + '/dwc/versions/' + version_id))
|
|
528
|
+
end
|
|
529
|
+
|
|
422
530
|
# Shared read path behind every typed `find`: perform a single-resource
|
|
423
531
|
# `GET` and hand the response to {AtlasRb::FaradayHelper#read_body}, which
|
|
424
532
|
# owns the status mapping (`404` → `nil`, `410` → the tombstone body, any
|
|
@@ -485,21 +593,25 @@ module AtlasRb
|
|
|
485
593
|
# @raise [AtlasRb::ResourceError] on any other non-2xx except `410`.
|
|
486
594
|
# @api private
|
|
487
595
|
def self.write_resource(resp)
|
|
596
|
+
guard_write(resp)
|
|
597
|
+
JSON.parse(resp.body)
|
|
598
|
+
end
|
|
599
|
+
private_class_method :write_resource
|
|
600
|
+
|
|
601
|
+
# The status half of {write_resource}, for a write that answers with no
|
|
602
|
+
# body, such as a `204`.
|
|
603
|
+
def self.guard_write(resp)
|
|
488
604
|
if resp.status == 404
|
|
489
605
|
raise AtlasRb::NotFoundError.new("#{request_target(resp)} → 404 (no such resource)", response: resp)
|
|
490
606
|
end
|
|
607
|
+
return if resp.success? || resp.status == 410
|
|
491
608
|
|
|
492
|
-
|
|
493
|
-
raise AtlasRb::ResourceError.new("#{request_target(resp)} → #{resp.status}: #{resp.body}", response: resp)
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
JSON.parse(resp.body)
|
|
609
|
+
raise AtlasRb::ResourceError.new("#{request_target(resp)} → #{resp.status}: #{resp.body}", response: resp)
|
|
497
610
|
end
|
|
498
|
-
private_class_method :
|
|
611
|
+
private_class_method :guard_write
|
|
499
612
|
|
|
500
|
-
# The multipart body behind {
|
|
501
|
-
#
|
|
502
|
-
# naming the surface that produced the edit.
|
|
613
|
+
# The multipart body behind {put_mods} and {put_dwc}: the XML document,
|
|
614
|
+
# plus the optional `origin` tag naming the surface that produced the edit.
|
|
503
615
|
#
|
|
504
616
|
# Atlas records `origin` verbatim on the audit event and never branches on
|
|
505
617
|
# it, so a host names its own surfaces (Cerberus sends `metadata_form`,
|
|
@@ -512,13 +624,13 @@ module AtlasRb
|
|
|
512
624
|
# @param origin [String, nil] the edit-origin tag, or `nil` to send none.
|
|
513
625
|
# @return [Hash] the multipart payload.
|
|
514
626
|
# @api private
|
|
515
|
-
def self.
|
|
627
|
+
def self.xml_upload_payload(xml_path, origin)
|
|
516
628
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
|
|
517
629
|
"application/xml",
|
|
518
630
|
File.basename(xml_path)) }
|
|
519
631
|
payload[:origin] = origin.to_s unless origin.nil? || origin.to_s.empty?
|
|
520
632
|
payload
|
|
521
633
|
end
|
|
522
|
-
private_class_method :
|
|
634
|
+
private_class_method :xml_upload_payload
|
|
523
635
|
end
|
|
524
636
|
end
|
|
@@ -5,9 +5,7 @@ module AtlasRb
|
|
|
5
5
|
# as a verb on the `/resources/{id}` sub-resource that already serves its
|
|
6
6
|
# `GET`, so a caller holding only a NOID never resolves the type first.
|
|
7
7
|
#
|
|
8
|
-
# Loaded after the subclasses because the typed writes delegate here
|
|
9
|
-
# because {Resource::TYPE_MAP} in `resource_types.rb` has the same ordering
|
|
10
|
-
# requirement.
|
|
8
|
+
# Loaded after the subclasses because the typed writes delegate here.
|
|
11
9
|
#
|
|
12
10
|
# **The names are deliberately not `update` and `metadata`.** Neither typed
|
|
13
11
|
# name says which document it writes, and `Resource.mods` / `.permissions`
|
|
@@ -52,10 +50,61 @@ module AtlasRb
|
|
|
52
50
|
def self.put_mods(id, xml_path, nuid: nil, on_behalf_of: nil, origin: nil)
|
|
53
51
|
unwrap(write_resource(
|
|
54
52
|
multipart(nuid, on_behalf_of: on_behalf_of)
|
|
55
|
-
.put('/resources/' + id + '/mods',
|
|
53
|
+
.put('/resources/' + id + '/mods', xml_upload_payload(xml_path, origin))
|
|
56
54
|
))
|
|
57
55
|
end
|
|
58
56
|
|
|
57
|
+
# Replace a Work's Darwin Core record.
|
|
58
|
+
#
|
|
59
|
+
# `PUT`: the document replaces the whole record. The first call creates it,
|
|
60
|
+
# and a call after {delete_dwc} restores it. Each call appends a version.
|
|
61
|
+
# Atlas checks the document's shape; validating it against
|
|
62
|
+
# `tdwg_dwc_simple.xsd` is the caller's job.
|
|
63
|
+
#
|
|
64
|
+
# @param id [String] the Work's NOID.
|
|
65
|
+
# @param xml_path [String] path to the Simple Darwin Core XML to upload.
|
|
66
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
67
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
68
|
+
# header.
|
|
69
|
+
# @param origin [String, nil] the editing surface to record on the audit
|
|
70
|
+
# event, e.g. `"xml_loader"`. Omitted from the body when nil.
|
|
71
|
+
# @return [AtlasRb::Mash] the Work's `id` and its terms under `"dwc"`.
|
|
72
|
+
# @raise [AtlasRb::NotFoundError] on `404` — no such id, or not a Work.
|
|
73
|
+
# @raise [AtlasRb::ResourceError] on any other non-2xx. A refused document
|
|
74
|
+
# is a `422` whose body names the rule in `error`: `malformed_xml`,
|
|
75
|
+
# `invalid_root`, `record_count` or `duplicate_term`.
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# AtlasRb::Resource.put_dwc("xsj3xmz", "/tmp/dwc.xml", origin: "xml_loader")
|
|
79
|
+
def self.put_dwc(id, xml_path, nuid: nil, on_behalf_of: nil, origin: nil)
|
|
80
|
+
unwrap(write_resource(
|
|
81
|
+
multipart(nuid, on_behalf_of: on_behalf_of)
|
|
82
|
+
.put('/resources/' + id + '/dwc', xml_upload_payload(xml_path, origin))
|
|
83
|
+
))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Withdraw a Work's Darwin Core record.
|
|
87
|
+
#
|
|
88
|
+
# Nothing is purged: Atlas keeps the document and its version history, and
|
|
89
|
+
# the next {put_dwc} restores the record. Until then {Work.dwc} answers
|
|
90
|
+
# `nil` and the Work's `metadata_formats` leaves out `"dwc"`.
|
|
91
|
+
#
|
|
92
|
+
# @param id [String] the Work's NOID.
|
|
93
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
94
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
95
|
+
# header.
|
|
96
|
+
# @return [true] when the record was withdrawn.
|
|
97
|
+
# @raise [AtlasRb::NotFoundError] on `404` — the Work holds no record, or
|
|
98
|
+
# the id is not a Work.
|
|
99
|
+
# @raise [AtlasRb::ResourceError] on any other non-2xx.
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# AtlasRb::Resource.delete_dwc("xsj3xmz")
|
|
103
|
+
def self.delete_dwc(id, nuid: nil, on_behalf_of: nil)
|
|
104
|
+
guard_write(connection({}, nuid, on_behalf_of: on_behalf_of).delete('/resources/' + id + '/dwc'))
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
59
108
|
# Adjust a resource's ACL.
|
|
60
109
|
#
|
|
61
110
|
# `PATCH`, and every key merges: a key you omit keeps its stored value.
|
data/lib/atlas_rb/work.rb
CHANGED
|
@@ -13,6 +13,11 @@ module AtlasRb
|
|
|
13
13
|
# @api private
|
|
14
14
|
ROUTE = "/works/"
|
|
15
15
|
|
|
16
|
+
# Atlas accepts a MODS write for this type. See {Resource.mods_editable?}.
|
|
17
|
+
def self.mods_editable?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
# Fetch a single Work by ID.
|
|
17
22
|
#
|
|
18
23
|
# @param id [String] the Work ID.
|
|
@@ -613,6 +618,39 @@ module AtlasRb
|
|
|
613
618
|
))
|
|
614
619
|
end
|
|
615
620
|
|
|
621
|
+
# Fetch the Work's Darwin Core record in the requested format.
|
|
622
|
+
#
|
|
623
|
+
# A Work holds a record only when its `metadata_formats` includes `"dwc"`,
|
|
624
|
+
# so check that before calling: a Work with no record answers `nil` here.
|
|
625
|
+
#
|
|
626
|
+
# @param id [String] the Work ID.
|
|
627
|
+
# @param kind [String] one of `"xml"` (default), `"json"` or `"html"`.
|
|
628
|
+
# `"xml"` is the stored Simple Darwin Core document, byte for byte, for a
|
|
629
|
+
# standalone download. `"html"` is the display block, the counterpart of
|
|
630
|
+
# `Work.mods(id, "html")`; each `<dt>` names its term in `data-term`.
|
|
631
|
+
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
632
|
+
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
633
|
+
# path it is ignored (identity lives in the token).
|
|
634
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
635
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
636
|
+
# omitted.
|
|
637
|
+
# @return [String, AtlasRb::Mash, nil] the raw body for `"xml"` and
|
|
638
|
+
# `"html"`; for `"json"`, the Work unwrapped from its `"work"` key, with
|
|
639
|
+
# the terms under `"dwc"`.
|
|
640
|
+
#
|
|
641
|
+
# `nil` when Atlas answers `404` — the Work holds no record (or it was
|
|
642
|
+
# withdrawn), or the id is not a Work.
|
|
643
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`,
|
|
644
|
+
# such as the `403` for a Work the caller may not read.
|
|
645
|
+
# @example
|
|
646
|
+
# AtlasRb::Work.dwc("w-789", "json")["dwc"]["catalogNumber"] # => "S27880"
|
|
647
|
+
def self.dwc(id, kind = 'xml', nuid: nil, on_behalf_of: nil)
|
|
648
|
+
body = read_raw(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + "/dwc.#{kind}"))
|
|
649
|
+
return body unless kind.to_s == 'json' && body
|
|
650
|
+
|
|
651
|
+
AtlasRb::Mash.new(JSON.parse(body))['work']
|
|
652
|
+
end
|
|
653
|
+
|
|
616
654
|
# List the Collections a Work is a *linked* member of.
|
|
617
655
|
#
|
|
618
656
|
# Wraps `GET /works/<id>/linked_members`. Linked membership is the DAG
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -33,9 +33,9 @@ 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
|
-
#
|
|
37
|
-
|
|
38
|
-
#
|
|
36
|
+
# Every resolvable type has registered itself by now.
|
|
37
|
+
AtlasRb::Resource::TYPES.freeze
|
|
38
|
+
# Must follow every Resource subclass above: the typed writes delegate here.
|
|
39
39
|
require_relative "atlas_rb/resource_writes"
|
|
40
40
|
require_relative "atlas_rb/user"
|
|
41
41
|
require_relative "atlas_rb/admin"
|
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.
|
|
4
|
+
version: 1.27.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-10-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -178,7 +178,6 @@ files:
|
|
|
178
178
|
- lib/atlas_rb/middleware/raise_on_stale_resource.rb
|
|
179
179
|
- lib/atlas_rb/person.rb
|
|
180
180
|
- lib/atlas_rb/resource.rb
|
|
181
|
-
- lib/atlas_rb/resource_types.rb
|
|
182
181
|
- lib/atlas_rb/resource_writes.rb
|
|
183
182
|
- lib/atlas_rb/system.rb
|
|
184
183
|
- lib/atlas_rb/system/token.rb
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|