atlas_rb 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2274a10469db90f642ca79a877ab37f9db69c294c55737a28824885249ef6220
4
- data.tar.gz: c16a9865aa30a521823c17587e80179e681fab4a9c3747957ab0b943e8591548
3
+ metadata.gz: 74f74d6af596e2f817ba6991018efd21cb158f897efa39ffc572367a27a3c968
4
+ data.tar.gz: 26b52d5a67e30cac921695e31aba127338fe77095218a1277f6c04049c1dde5e
5
5
  SHA512:
6
- metadata.gz: fbf77b86e3b695401be85f2f913d4c71820ae2e1c2078884d6b13038d46cca749aaafaf7ef588e9f742db01e8b53b5375734893572430a9ccc7adbfed5444e50
7
- data.tar.gz: baf502af151908b4da14e4e51597d60f7c88b90d7659deac2731bd44dc317376dce11bbb8e2c2e9f3d7fbfc33adc0e8b6572124769651e7917bfb51eaaac76b5
6
+ metadata.gz: 692f82797f41797a10b06b1c6fb79624a03437711c1781afd3e7a0d1f8711eb5964c42197dd6b411e4c1b0df84115a609c7f26091d736f02faacb72a40dc22f3
7
+ data.tar.gz: fffef87ace94444d2f9b82f5347ad342e8a908f05541fd4c4b4feced02b63cc6ecc5eff57ca2c0824439fb5c133fa3a4389655c157e8809aafc472236fc9eb60
data/.version CHANGED
@@ -1 +1 @@
1
- 1.25.0
1
+ 1.26.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.26.0
4
+
5
+ These bindings need Atlas 0.6.221 or later.
6
+
7
+ ### Added — Darwin Core records
8
+
9
+ ```ruby
10
+ AtlasRb::Work.dwc(id, kind = "xml") # "xml", "json" or "html"; nil on 404
11
+ AtlasRb::Resource.put_dwc(id, xml_path, origin: nil)
12
+ AtlasRb::Resource.delete_dwc(id) # true; raises NotFoundError on 404
13
+ AtlasRb::Resource.dwc_versions(id)
14
+ AtlasRb::Resource.dwc_version(id, version_id)
15
+ ```
16
+
17
+ A Work can hold one Simple Darwin Core record. `Work.dwc` returns the stored
18
+ XML by default, the display block for `"html"`, and the Work unwrapped from
19
+ `"work"` for `"json"`, with the terms under `"dwc"`. `put_dwc` replaces the
20
+ whole record and raises `ResourceError` on Atlas's `422` when the document
21
+ breaks a shape rule. `delete_dwc` withdraws the record without purging it, so
22
+ the next `put_dwc` restores it. The version calls mirror `mods_versions` and
23
+ `mods_version`.
24
+
25
+ ### Documented — `metadata_formats` on the Work
26
+
27
+ `Work.find` results now carry `metadata_formats`, the additional records the
28
+ Work holds, such as `["dwc"]`. No code changed: the gem passes the whole Work
29
+ through.
30
+
3
31
  ## 1.25.0
4
32
 
5
33
  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.25.0)
4
+ atlas_rb (1.26.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.9.1)
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
- uri (1.1.1)
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` |
@@ -419,6 +419,47 @@ module AtlasRb
419
419
  ))
420
420
  end
421
421
 
422
+ # List the retained versions of a Work's Darwin Core record.
423
+ #
424
+ # The same envelope as {mods_versions}, under the same admin gate, with
425
+ # attribution drawn from the Darwin Core edits only. A Work with no record,
426
+ # or an id that is not a Work, comes back as `{ "versions" => [] }`.
427
+ #
428
+ # @param id [String] the Work's NOID.
429
+ # @param nuid [String, nil] optional acting user's NUID.
430
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
431
+ # header.
432
+ # @return [AtlasRb::Mash, nil] the envelope, with `"resource_id"` and a
433
+ # newest-first `"versions"` array; `nil` on a `404`.
434
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`,
435
+ # including the `403` a non-admin caller gets.
436
+ # @example
437
+ # AtlasRb::Resource.dwc_versions("w-789")["versions"].first["source"] # => "dwc"
438
+ def self.dwc_versions(id, nuid: nil, on_behalf_of: nil)
439
+ read_body(connection({}, nuid, on_behalf_of: on_behalf_of)
440
+ .get('/resources/' + id + '/dwc/versions')) { |body| AtlasRb::Mash.new(body) }
441
+ end
442
+
443
+ # Fetch a Work's Darwin Core document as of a specific version.
444
+ #
445
+ # XML only, because Atlas overwrites the JSON access copy in place. Pass a
446
+ # `version_id` from {dwc_versions}.
447
+ #
448
+ # @param id [String] the Work's NOID.
449
+ # @param version_id [String] an OCFL version label, e.g. `"v3"`.
450
+ # @param nuid [String, nil] optional acting user's NUID.
451
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
452
+ # header.
453
+ # @return [String, nil] the raw XML; `nil` on a `404` (unknown version, or
454
+ # no record ever written).
455
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`.
456
+ # @example
457
+ # AtlasRb::Resource.dwc_version("w-789", "v3")
458
+ def self.dwc_version(id, version_id, nuid: nil, on_behalf_of: nil)
459
+ read_raw(connection({}, nuid, on_behalf_of: on_behalf_of)
460
+ .get('/resources/' + id + '/dwc/versions/' + version_id))
461
+ end
462
+
422
463
  # Shared read path behind every typed `find`: perform a single-resource
423
464
  # `GET` and hand the response to {AtlasRb::FaradayHelper#read_body}, which
424
465
  # owns the status mapping (`404` → `nil`, `410` → the tombstone body, any
@@ -485,21 +526,25 @@ module AtlasRb
485
526
  # @raise [AtlasRb::ResourceError] on any other non-2xx except `410`.
486
527
  # @api private
487
528
  def self.write_resource(resp)
529
+ guard_write(resp)
530
+ JSON.parse(resp.body)
531
+ end
532
+ private_class_method :write_resource
533
+
534
+ # The status half of {write_resource}, for a write that answers with no
535
+ # body, such as a `204`.
536
+ def self.guard_write(resp)
488
537
  if resp.status == 404
489
538
  raise AtlasRb::NotFoundError.new("#{request_target(resp)} → 404 (no such resource)", response: resp)
490
539
  end
540
+ return if resp.success? || resp.status == 410
491
541
 
492
- unless resp.success? || resp.status == 410
493
- raise AtlasRb::ResourceError.new("#{request_target(resp)} → #{resp.status}: #{resp.body}", response: resp)
494
- end
495
-
496
- JSON.parse(resp.body)
542
+ raise AtlasRb::ResourceError.new("#{request_target(resp)} → #{resp.status}: #{resp.body}", response: resp)
497
543
  end
498
- private_class_method :write_resource
544
+ private_class_method :guard_write
499
545
 
500
- # The multipart body behind {Work.update} / {Collection.update} /
501
- # {Community.update}: the MODS document, plus the optional `origin` tag
502
- # naming the surface that produced the edit.
546
+ # The multipart body behind {put_mods} and {put_dwc}: the XML document,
547
+ # plus the optional `origin` tag naming the surface that produced the edit.
503
548
  #
504
549
  # Atlas records `origin` verbatim on the audit event and never branches on
505
550
  # it, so a host names its own surfaces (Cerberus sends `metadata_form`,
@@ -512,13 +557,13 @@ module AtlasRb
512
557
  # @param origin [String, nil] the edit-origin tag, or `nil` to send none.
513
558
  # @return [Hash] the multipart payload.
514
559
  # @api private
515
- def self.mods_upload_payload(xml_path, origin)
560
+ def self.xml_upload_payload(xml_path, origin)
516
561
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
517
562
  "application/xml",
518
563
  File.basename(xml_path)) }
519
564
  payload[:origin] = origin.to_s unless origin.nil? || origin.to_s.empty?
520
565
  payload
521
566
  end
522
- private_class_method :mods_upload_payload
567
+ private_class_method :xml_upload_payload
523
568
  end
524
569
  end
@@ -52,10 +52,61 @@ module AtlasRb
52
52
  def self.put_mods(id, xml_path, nuid: nil, on_behalf_of: nil, origin: nil)
53
53
  unwrap(write_resource(
54
54
  multipart(nuid, on_behalf_of: on_behalf_of)
55
- .put('/resources/' + id + '/mods', mods_upload_payload(xml_path, origin))
55
+ .put('/resources/' + id + '/mods', xml_upload_payload(xml_path, origin))
56
56
  ))
57
57
  end
58
58
 
59
+ # Replace a Work's Darwin Core record.
60
+ #
61
+ # `PUT`: the document replaces the whole record. The first call creates it,
62
+ # and a call after {delete_dwc} restores it. Each call appends a version.
63
+ # Atlas checks the document's shape; validating it against
64
+ # `tdwg_dwc_simple.xsd` is the caller's job.
65
+ #
66
+ # @param id [String] the Work's NOID.
67
+ # @param xml_path [String] path to the Simple Darwin Core XML to upload.
68
+ # @param nuid [String, nil] optional acting user's NUID.
69
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
70
+ # header.
71
+ # @param origin [String, nil] the editing surface to record on the audit
72
+ # event, e.g. `"xml_loader"`. Omitted from the body when nil.
73
+ # @return [AtlasRb::Mash] the Work's `id` and its terms under `"dwc"`.
74
+ # @raise [AtlasRb::NotFoundError] on `404` — no such id, or not a Work.
75
+ # @raise [AtlasRb::ResourceError] on any other non-2xx. A refused document
76
+ # is a `422` whose body names the rule in `error`: `malformed_xml`,
77
+ # `invalid_root`, `record_count` or `duplicate_term`.
78
+ #
79
+ # @example
80
+ # AtlasRb::Resource.put_dwc("xsj3xmz", "/tmp/dwc.xml", origin: "xml_loader")
81
+ def self.put_dwc(id, xml_path, nuid: nil, on_behalf_of: nil, origin: nil)
82
+ unwrap(write_resource(
83
+ multipart(nuid, on_behalf_of: on_behalf_of)
84
+ .put('/resources/' + id + '/dwc', xml_upload_payload(xml_path, origin))
85
+ ))
86
+ end
87
+
88
+ # Withdraw a Work's Darwin Core record.
89
+ #
90
+ # Nothing is purged: Atlas keeps the document and its version history, and
91
+ # the next {put_dwc} restores the record. Until then {Work.dwc} answers
92
+ # `nil` and the Work's `metadata_formats` leaves out `"dwc"`.
93
+ #
94
+ # @param id [String] the Work's NOID.
95
+ # @param nuid [String, nil] optional acting user's NUID.
96
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
97
+ # header.
98
+ # @return [true] when the record was withdrawn.
99
+ # @raise [AtlasRb::NotFoundError] on `404` — the Work holds no record, or
100
+ # the id is not a Work.
101
+ # @raise [AtlasRb::ResourceError] on any other non-2xx.
102
+ #
103
+ # @example
104
+ # AtlasRb::Resource.delete_dwc("xsj3xmz")
105
+ def self.delete_dwc(id, nuid: nil, on_behalf_of: nil)
106
+ guard_write(connection({}, nuid, on_behalf_of: on_behalf_of).delete('/resources/' + id + '/dwc'))
107
+ true
108
+ end
109
+
59
110
  # Adjust a resource's ACL.
60
111
  #
61
112
  # `PATCH`, and every key merges: a key you omit keeps its stored value.
data/lib/atlas_rb/work.rb CHANGED
@@ -613,6 +613,39 @@ module AtlasRb
613
613
  ))
614
614
  end
615
615
 
616
+ # Fetch the Work's Darwin Core record in the requested format.
617
+ #
618
+ # A Work holds a record only when its `metadata_formats` includes `"dwc"`,
619
+ # so check that before calling: a Work with no record answers `nil` here.
620
+ #
621
+ # @param id [String] the Work ID.
622
+ # @param kind [String] one of `"xml"` (default), `"json"` or `"html"`.
623
+ # `"xml"` is the stored Simple Darwin Core document, byte for byte, for a
624
+ # standalone download. `"html"` is the display block, the counterpart of
625
+ # `Work.mods(id, "html")`; each `<dt>` names its term in `data-term`.
626
+ # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
627
+ # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
628
+ # path it is ignored (identity lives in the token).
629
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
630
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
631
+ # omitted.
632
+ # @return [String, AtlasRb::Mash, nil] the raw body for `"xml"` and
633
+ # `"html"`; for `"json"`, the Work unwrapped from its `"work"` key, with
634
+ # the terms under `"dwc"`.
635
+ #
636
+ # `nil` when Atlas answers `404` — the Work holds no record (or it was
637
+ # withdrawn), or the id is not a Work.
638
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`,
639
+ # such as the `403` for a Work the caller may not read.
640
+ # @example
641
+ # AtlasRb::Work.dwc("w-789", "json")["dwc"]["catalogNumber"] # => "S27880"
642
+ def self.dwc(id, kind = 'xml', nuid: nil, on_behalf_of: nil)
643
+ body = read_raw(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + "/dwc.#{kind}"))
644
+ return body unless kind.to_s == 'json' && body
645
+
646
+ AtlasRb::Mash.new(JSON.parse(body))['work']
647
+ end
648
+
616
649
  # List the Collections a Work is a *linked* member of.
617
650
  #
618
651
  # Wraps `GET /works/<id>/linked_members`. Linked membership is the DAG
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.25.0
4
+ version: 1.26.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-01 00:00:00.000000000 Z
11
+ date: 2026-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday