ezid-client 1.6.0 → 1.9.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.
data/lib/ezid/metadata.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "hashie"
2
+ require_relative "metadata_transforms/datacite"
2
3
 
3
4
  module Ezid
4
5
  #
@@ -85,7 +86,12 @@ module Ezid
85
86
  end
86
87
 
87
88
  def replace(data)
88
- super coerce(data)
89
+ hsh = coerce(data)
90
+
91
+ # Perform additional profile transforms
92
+ MetadataTransformDatacite.inverse(hsh) if hsh["_profile"] == "datacite"
93
+
94
+ super hsh
89
95
  end
90
96
 
91
97
  # Output metadata in EZID ANVL format
@@ -94,6 +100,10 @@ module Ezid
94
100
  def to_anvl(include_readonly = true)
95
101
  hsh = to_h
96
102
  hsh.reject! { |k, v| READONLY.include?(k) } unless include_readonly
103
+
104
+ # Perform additional profile transforms
105
+ MetadataTransformDatacite.transform(hsh) if profile == "datacite"
106
+
97
107
  lines = hsh.map do |name, value|
98
108
  element = [escape(ESCAPE_NAMES_RE, name), escape(ESCAPE_VALUES_RE, value)]
99
109
  element.join(ANVL_SEPARATOR)
@@ -0,0 +1,72 @@
1
+ require "nokogiri"
2
+
3
+ module Ezid
4
+ class MetadataTransformDatacite
5
+
6
+ # Transforms the provided metadata hash into the appropriate format for datacite. Removes all "datacite.*" keys
7
+ # and transforms these to the appropriate datacite xml. The resultant xml is then added to a single "datacite" key.
8
+ def self.transform(hsh)
9
+ # Render the datacite xml
10
+ resource_opts = {
11
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
12
+ "xmlns" => "http://datacite.org/schema/kernel-4",
13
+ "xsi:schemaLocation" => "http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd"
14
+ }
15
+ xml_builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") { |builder|
16
+ builder.resource(resource_opts) {
17
+ builder.identifier(identifierType: hsh["datacite.identifiertype"] || "DOI") {
18
+ builder.text hsh["datacite.identifier"]
19
+ }
20
+ builder.creators {
21
+ builder.creator {
22
+ builder.creatorName hsh["datacite.creator"]
23
+ }
24
+ }
25
+ builder.titles {
26
+ builder.title hsh["datacite.title"]
27
+ }
28
+ builder.publisher hsh["datacite.publisher"]
29
+ builder.publicationYear hsh["datacite.publicationyear"]
30
+ builder.resourceType(resourceTypeGeneral: hsh["datacite.resourcetypegeneral"]) {
31
+ builder.text hsh["datacite.resourcetype"]
32
+ }
33
+ builder.descriptions {
34
+ builder.description(descriptionType: "Abstract") {
35
+ builder.text hsh["datacite.description"]
36
+ }
37
+ }
38
+ }
39
+ }
40
+ # Using this save option to prevent NG from rendering new lines and tabs
41
+ # between nodes. This to help with a cleaner anvl conversion. Similarly,
42
+ # the sub should just remove the new line after the xml header that NG
43
+ # adds, ex:
44
+ # <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<resource ...
45
+ xml = xml_builder
46
+ .to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
47
+ .sub("\n", "")
48
+
49
+
50
+ # Transform the hash
51
+ hsh.reject! { |k, v| k =~ /^datacite\./ }
52
+ hsh["datacite"] = xml
53
+ end
54
+
55
+ # Transforms the provided datacite metadata hash into the format appropriate for the Metadata class.
56
+ # Extracts appropriate fields from the datacite xml and creates the corresponding "datacite.*" keys
57
+ def self.inverse(hsh)
58
+ xml = Nokogiri::XML(hsh["datacite"])
59
+ xmlns = "http://datacite.org/schema/kernel-4"
60
+ hsh["datacite.identifier"] = xml.at_xpath("/ns:resource/ns:identifier/text()", ns: xmlns).to_s
61
+ hsh["datacite.identifiertype"] = xml.at_xpath("/ns:resource/ns:identifier/attribute::identifierType", ns: xmlns).to_s
62
+ hsh["datacite.creator"] = xml.at_xpath("/ns:resource/ns:creators/ns:creator/ns:creatorName/text()", ns: xmlns).to_s
63
+ hsh["datacite.title"] = xml.at_xpath("/ns:resource/ns:titles/ns:title/text()", ns: xmlns).to_s
64
+ hsh["datacite.publisher"] = xml.at_xpath("/ns:resource/ns:publisher/text()", ns: xmlns).to_s
65
+ hsh["datacite.publicationyear"] = xml.at_xpath("/ns:resource/ns:publicationYear/text()", ns: xmlns).to_s
66
+ hsh["datacite.resourcetype"] = xml.at_xpath("/ns:resource/ns:resourceType/text()", ns: xmlns).to_s
67
+ hsh["datacite.resourcetypegeneral"] = xml.at_xpath("/ns:resource/ns:resourceType/attribute::resourceTypeGeneral", ns: xmlns).to_s
68
+ hsh["datacite.description"] = xml.at_xpath("/ns:resource/ns:descriptions/ns:description/text()", ns: xmlns).to_s
69
+ hsh.delete("datacite")
70
+ end
71
+ end
72
+ end
@@ -1,9 +1,10 @@
1
- require "delegate"
2
- require "uri"
3
- require "net/http"
4
- require "forwardable"
1
+ require 'delegate'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require 'forwardable'
5
+ require 'date'
5
6
 
6
- require_relative "../responses/response"
7
+ require_relative '../responses/response'
7
8
 
8
9
  module Ezid
9
10
  #
@@ -31,7 +32,7 @@ module Ezid
31
32
  end
32
33
 
33
34
  def short_name
34
- name.split("::").last.sub("Request", "")
35
+ name.split('::').last.sub('Request', '')
35
36
  end
36
37
  end
37
38
 
@@ -42,13 +43,24 @@ module Ezid
42
43
  def initialize(client, *args)
43
44
  @client = client
44
45
  super build_request
45
- set_content_type("text/plain", charset: "UTF-8")
46
+ set_content_type('text/plain', charset: 'UTF-8')
46
47
  end
47
48
 
48
49
  # Executes the request and returns the response
49
50
  # @return [Ezid::Response] the response
50
51
  def execute
51
- response_class.new(get_response_for_request)
52
+ retries = 0
53
+ begin
54
+ response_class.new(get_response_for_request)
55
+ rescue Net::HTTPServerException, UnexpectedResponseError => e
56
+ if retries < 2
57
+ sleep 15
58
+ retries += 1
59
+ retry
60
+ else
61
+ raise
62
+ end
63
+ end
52
64
  end
53
65
 
54
66
  # The request URI
@@ -91,6 +103,7 @@ module Ezid
91
103
 
92
104
  def get_response_for_request
93
105
  connection.start do |conn|
106
+ self['Accept'] = 'text/plain'
94
107
  add_authentication if authentication_required?
95
108
  add_metadata if has_metadata?
96
109
  conn.request(__getobj__)
@@ -113,7 +126,7 @@ module Ezid
113
126
  # Adds authentication data to the request
114
127
  def add_authentication
115
128
  if session.open?
116
- self["Cookie"] = session.cookie
129
+ self['Cookie'] = session.cookie
117
130
  else
118
131
  basic_auth(user, password)
119
132
  end
@@ -14,6 +14,19 @@ module Ezid
14
14
  # Error response status
15
15
  ERROR = "error".freeze
16
16
 
17
+ def initialize(http_response)
18
+ super
19
+
20
+ unless __getobj__.code =~ /2\d\d/
21
+ raise Error, "HTTP response error: %s %s" %
22
+ [ __getobj__.code, __getobj__.message ]
23
+ end
24
+
25
+ unless status_line =~ /^(#{SUCCESS}|#{ERROR}): /
26
+ raise UnexpectedResponseError, __getobj__.body
27
+ end
28
+ end
29
+
17
30
  # The response status -- "success" or "error"
18
31
  # @return [String] the status
19
32
  def status
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" xsi:schemaLocation="http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd"><identifier identifierType="DOI"></identifier><creators><creator><creatorName/></creator></creators><titles><title/></titles><publisher/><publicationYear/><resourceType resourceTypeGeneral=""></resourceType><descriptions><description descriptionType="Abstract"></description></descriptions></resource>
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" xsi:schemaLocation="http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd"><identifier identifierType="TestIdentifierType">TestIdentifier</identifier><creators><creator><creatorName>TestCreatorName</creatorName></creator></creators><titles><title>TestTitle</title></titles><publisher>TestPublisher</publisher><publicationYear>TestPublicationYear</publicationYear><resourceType resourceTypeGeneral="TestResourceTypeGeneral">TestResourceType</resourceType><descriptions><description descriptionType="Abstract">TestDescription</description></descriptions></resource>
@@ -1,18 +1,18 @@
1
1
  require 'tempfile'
2
2
 
3
3
  module Ezid
4
- RSpec.describe BatchDownload do
4
+ RSpec.describe BatchDownload, ezid: true do
5
5
 
6
6
  subject do
7
7
  a_week_ago = (Time.now - (7*24*60*60)).to_i
8
8
  described_class.new(:anvl, compression: "zip", permanence: "test", status: "public", createdAfter: a_week_ago)
9
9
  end
10
10
 
11
- its(:download_url) { is_expected.to match(/\Ahttp:\/\/ezid\.cdlib\.org\/download\/\w+\.zip\z/) }
12
-
13
11
  specify {
12
+ expect(subject.download_url).to match(/\Ahttps:\/\/ezid\.cdlib\.org\/download\/\w+\.zip\z/)
13
+ expect(subject.url).to match(/\Ahttps:\/\/ezid\.cdlib\.org\/download\/\w+\.zip\z/)
14
14
  Dir.mktmpdir do |tmpdir|
15
- expect(subject.download_file(path: tmpdir))
15
+ expect(subject.file(path: tmpdir))
16
16
  .to match(/\A#{tmpdir}\/\w+\.zip\z/)
17
17
  end
18
18
  }
@@ -1,7 +1,7 @@
1
1
  require "time"
2
2
 
3
3
  module Ezid
4
- RSpec.describe Client, integration: true do
4
+ RSpec.describe Client, ezid: true do
5
5
 
6
6
  shared_examples "an EZID client" do |client|
7
7
  it "should mint and modify" do
@@ -1,5 +1,5 @@
1
1
  module Ezid
2
- RSpec.describe Identifier, integration: true do
2
+ RSpec.describe Identifier, ezid: true do
3
3
 
4
4
  before {
5
5
  @identifier = described_class.mint(TEST_ARK_SHOULDER, target: "http://example.com")
@@ -38,7 +38,8 @@ module Ezid
38
38
  end
39
39
  describe "delete" do
40
40
  subject { described_class.mint(TEST_ARK_SHOULDER, status: "reserved") }
41
- it "deletes the identifier" do
41
+ # Getting 400 Bad Request response - DCS 3/22/21
42
+ xit "deletes the identifier" do
42
43
  subject.delete
43
44
  expect(subject).to be_deleted
44
45
  expect { described_class.find(subject.id) }.to raise_error(IdentifierNotFoundError)
data/spec/spec_helper.rb CHANGED
@@ -15,6 +15,8 @@
15
15
  #
16
16
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17
17
 
18
+ require "byebug"
19
+
18
20
  require "rspec/its"
19
21
 
20
22
  require "ezid/test_helper"
@@ -61,7 +63,7 @@ RSpec.configure do |config|
61
63
 
62
64
  # This setting enables warnings. It's recommended, but in some cases may
63
65
  # be too noisy due to issues in dependencies.
64
- config.warnings = true
66
+ config.warnings = false
65
67
 
66
68
  # Many RSpec users commonly either run the entire suite or an individual
67
69
  # file, and it's useful to allow more verbose output when running an
@@ -76,7 +78,7 @@ RSpec.configure do |config|
76
78
  # Print the 10 slowest examples and example groups at the
77
79
  # end of the spec run, to help surface which specs are running
78
80
  # particularly slow.
79
- config.profile_examples = 5
81
+ #config.profile_examples = 5
80
82
 
81
83
  # Run specs in random order to surface order dependencies. If you find an
82
84
  # order dependency and want to debug it, you can fix the order by providing
@@ -1,7 +1,7 @@
1
- require 'ezid/batch_enumerator'
1
+ require 'ezid/batch'
2
2
 
3
3
  module Ezid
4
- RSpec.describe BatchEnumerator do
4
+ RSpec.describe Batch do
5
5
 
6
6
  let(:batch_file) { File.expand_path("../../fixtures/anvl_batch.txt", __FILE__) }
7
7
 
@@ -1,7 +1,15 @@
1
1
  module Ezid
2
2
  RSpec.describe Client do
3
3
 
4
+ let(:http_response) { double }
5
+
6
+ before do
7
+ allow(http_response).to receive(:value) { nil }
8
+ allow(http_response).to receive(:code) { '200' }
9
+ end
10
+
4
11
  describe "initialization without a block" do
12
+ let(:http_response) { double }
5
13
  it "should not login" do
6
14
  expect_any_instance_of(described_class).not_to receive(:login)
7
15
  described_class.new
@@ -151,9 +159,33 @@ EOS
151
159
  end
152
160
 
153
161
  describe "error handling" do
154
- let(:http_response) { double(body: "error: bad request - no such identifier") }
155
- it "should raise an exception" do
156
- expect { subject.get_identifier_metadata("invalid") }.to raise_error(Error)
162
+ let(:stub_response) { GetIdentifierMetadataResponse.new(http_response) }
163
+ before do
164
+ allow(GetIdentifierMetadataRequest).to receive(:execute).with(subject, "invalid") { stub_response }
165
+ end
166
+
167
+ describe "HTTP error response" do
168
+ before do
169
+ allow(http_response).to receive(:code) { '500' }
170
+ allow(http_response).to receive(:message) { 'Internal Server Error' }
171
+ end
172
+ it "should raise an exception" do
173
+ expect { subject.get_identifier_metadata("invalid") }.to raise_error(Error)
174
+ end
175
+ end
176
+
177
+ describe "EZID API error response" do
178
+ let(:http_response) { double(body: "error: bad request - no such identifier") }
179
+ it "should raise an exception" do
180
+ expect { subject.get_identifier_metadata("invalid") }.to raise_error(Error)
181
+ end
182
+ end
183
+
184
+ describe "Unexpected response" do
185
+ let(:http_response) { double(body: "<html>\n<head><title>Ouch!</title></head>\n<body>Help!</body>\n</html>") }
186
+ it "should raise an exception" do
187
+ expect { subject.get_identifier_metadata("invalid") }.to raise_error(UnexpectedResponseError)
188
+ end
157
189
  end
158
190
  end
159
191
  end
@@ -1,6 +1,39 @@
1
1
  module Ezid
2
2
  RSpec.describe Identifier do
3
+
3
4
  describe "class methods" do
5
+
6
+ describe ".load" do
7
+ subject { described_class.load("ark:/99999/fk4086hs23", metadata) }
8
+ describe "with ANVL metadata" do
9
+ let(:metadata) do
10
+ <<-EOS
11
+ _updated: 1488227717
12
+ _target: http://example.com
13
+ _profile: erc
14
+ _ownergroup: apitest
15
+ _owner: apitest
16
+ _export: yes
17
+ _created: 1488227717
18
+ _status: public
19
+ EOS
20
+ end
21
+ its(:remote_metadata) {
22
+ is_expected.to eq({"_updated"=>"1488227717",
23
+ "_target"=>"http://example.com",
24
+ "_profile"=>"erc",
25
+ "_ownergroup"=>"apitest",
26
+ "_owner"=>"apitest",
27
+ "_export"=>"yes",
28
+ "_created"=>"1488227717",
29
+ "_status"=>"public"})
30
+ }
31
+ end
32
+ describe "with nil" do
33
+ let(:metadata) { nil }
34
+ its(:remote_metadata) { is_expected.to be_empty }
35
+ end
36
+ end
4
37
  describe ".create" do
5
38
  describe "with id and metadata args" do
6
39
  it "instantiates a new Identifier and saves it" do
@@ -63,6 +96,7 @@ module Ezid
63
96
  end
64
97
 
65
98
  describe "instance methods" do
99
+
66
100
  describe "#initialize" do
67
101
  before {
68
102
  allow(described_class).to receive(:defaults) { defaults }
@@ -129,7 +163,7 @@ module Ezid
129
163
  its(:client) { is_expected.to_not eq(client) }
130
164
  end
131
165
  end
132
- end
166
+ end # initialize
133
167
 
134
168
  describe "#update" do
135
169
  let(:metadata) { {"status" => "unavailable"} }
@@ -172,12 +206,23 @@ module Ezid
172
206
  end
173
207
 
174
208
  describe "#load_metadata" do
209
+ subject { described_class.new("id") }
175
210
  let(:metadata) { "_profile: erc" }
176
- before { allow(subject).to receive(:id) { "id" } }
177
211
  it "replaces the remote metadata with metadata from EZID" do
178
212
  expect(subject.client).to receive(:get_identifier_metadata).with("id") { double(id: "id", metadata: metadata) }
179
- expect(subject.remote_metadata).to receive(:replace).with(metadata)
180
213
  subject.load_metadata
214
+ expect(subject.remote_metadata).to eq({"_profile"=>"erc"})
215
+ expect(subject).to be_persisted
216
+ end
217
+ end
218
+
219
+ describe "#load_metadata!" do
220
+ subject { described_class.new("id") }
221
+ let(:metadata) { "_profile: erc" }
222
+ it "replaces the remote metadata with the provided metadata" do
223
+ subject.load_metadata!(metadata)
224
+ expect(subject.remote_metadata).to eq({"_profile"=>"erc"})
225
+ expect(subject).to be_persisted
181
226
  end
182
227
  end
183
228
 
@@ -323,23 +368,11 @@ module Ezid
323
368
  context "when the status is \"unavailable\"" do
324
369
  let(:status) { "#{Status::UNAVAILABLE} | whatever" }
325
370
  context "and no reason is given" do
326
- it "logs a warning" do
327
- pending "https://github.com/duke-libraries/ezid-client/issues/46"
328
- allow_message_expectations_on_nil
329
- expect(subject.logger).to receive(:warn)
330
- subject.unavailable!
331
- end
332
371
  it "does not change the status" do
333
372
  expect { subject.unavailable! }.not_to change(subject, :status)
334
373
  end
335
374
  end
336
375
  context "and a reason is given" do
337
- it "logs a warning" do
338
- pending "https://github.com/duke-libraries/ezid-client/issues/46"
339
- allow_message_expectations_on_nil
340
- expect(subject.logger).to receive(:warn)
341
- subject.unavailable!("because")
342
- end
343
376
  it "should change the status" do
344
377
  expect { subject.unavailable!("because") }.to change(subject, :status).from(status).to("#{Status::UNAVAILABLE} | because")
345
378
  end
@@ -382,5 +415,11 @@ module Ezid
382
415
  end
383
416
  end
384
417
  end
418
+
419
+ describe "#metadata" do
420
+ it "is frozen" do
421
+ expect { subject.metadata["foo"] = "bar" }.to raise_error(RuntimeError)
422
+ end
423
+ end
385
424
  end
386
425
  end