diff --git a/README.md b/README.md index 7d6ce70..d546135 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,17 @@ Or install it yourself as: [Mint an identifier on a shoulder](http://ezid.cdlib.org/doc/apidoc.html#operation-mint-identifier) +*Added in v1.4.0:* `Ezid::Identifier.mint` class method. + ``` ->> identifier = Ezid::Identifier.create(shoulder: "ark:/99999/fk4") -I, [2014-12-04T15:06:02.428445 #86655] INFO -- : EZID MintIdentifier -- success: ark:/99999/fk4rx9d523 -I, [2014-12-04T15:06:03.249793 #86655] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk4rx9d523 -=> # ->> identifier.id -=> "ark:/99999/fk4rx9d523" +>> identifier = Ezid::Identifier.mint("ark:/99999/fk4") +I, [2016-03-01T22:20:08.505323 #35148] INFO -- : EZID MintIdentifier -- success: ark:/99999/fk4tq65d6k +=> # >> identifier.status +I, [2016-03-01T22:20:22.323650 #35148] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk4tq65d6k => "public" >> identifier.target -=> "http://ezid.cdlib.org/id/ark:/99999/fk4rx9d523" +=> "http://ezid.cdlib.org/id/ark:/99999/fk4tq65d6k" ``` A default shoulder can be configured: @@ -59,19 +59,19 @@ end New identifiers will then be minted on the default shoulder when a shoulder is not specified: ``` ->> identifier = Ezid::Identifier.create +>> identifier = Ezid::Identifier.mint I, [2014-12-09T11:22:34.499860 #32279] INFO -- : EZID MintIdentifier -- success: ark:/99999/fk43f4wd4v -I, [2014-12-09T11:22:35.317181 #32279] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk43f4wd4v -=> # +=> # ``` [Create a specific identifier](http://ezid.cdlib.org/doc/apidoc.html#operation-create-identifier) +*Changed in v1.4.0:* `Ezid::Identifier.create` now expects the first argument to be the identifier (String) to create; the second optional argument is a hash of metadata elements. Passing the identifier in an `:id` hash option is deprecated and will be removed in v2.0. The `:shoulder` hash option is likewise deprecated; use `Ezid::Identifier.mint(shoulder, metadata)` instead. + ``` ->> identifier = Ezid::Identifier.create(id: "ark:/99999/fk4rx9d523/12345") +>> identifier = Ezid::Identifier.create("ark:/99999/fk4rx9d523/12345") I, [2014-12-09T11:21:42.077297 #32279] INFO -- : EZID CreateIdentifier -- success: ark:/99999/fk4rx9d523/12345 -I, [2014-12-09T11:21:42.808534 #32279] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk4rx9d523/12345 -=> # +=> # ``` **Retrieve** (Get Metadata) @@ -91,30 +91,34 @@ I, [2014-12-04T15:07:00.648676 #86655] INFO -- : EZID GetIdentifierMetadata -- => "http://example.com" >> identifier.save I, [2014-12-09T11:24:26.321801 #32279] INFO -- : EZID ModifyIdentifier -- success: ark:/99999/fk43f4wd4v -I, [2014-12-09T11:24:27.039288 #32279] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk43f4wd4v -=> # +=> # >> identifier.target +I, [2014-12-09T11:24:27.039288 #32279] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk43f4wd4v => "http://example.com" ``` +*Added in v1.4.0:* `Ezid::Identifier.modify(id, metadata)` class method. In support of more efficient updating of known identifiers, this method skips the GetIdentifierMetadata request used by `.find`. The operation will raise the `Ezid::IdentifierNotFoundError` if the EZID identifier does not exist. + **Delete** *Identifier status must be "reserved" to delete.* http://ezid.cdlib.org/doc/apidoc.html#operation-delete-identifier ``` ->> identifier = Ezid::Identifier.create(shoulder: "ark:/99999/fk4", status: "reserved") -I, [2014-12-04T15:12:39.976930 #86734] INFO -- : EZID MintIdentifier -- success: ark:/99999/fk4n58pc0r -I, [2014-12-04T15:12:40.693256 #86734] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk4n58pc0r -=> # +>> identifier = Ezid::Identifier.mint("ark:/99999/fk4", status: "reserved") +I, [2016-03-01T22:26:08.645858 #36701] INFO -- : EZID MintIdentifier -- success: ark:/99999/fk4pz5fm1b +=> # >> identifier.delete -I, [2014-12-04T15:12:48.853964 #86734] INFO -- : EZID DeleteIdentifier -- success: ark:/99999/fk4n58pc0r -=> # +I, [2016-03-01T22:26:14.829731 #36701] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk4pz5fm1b +I, [2016-03-01T22:26:15.711390 #36701] INFO -- : EZID DeleteIdentifier -- success: ark:/99999/fk4pz5fm1b +=> # ``` ## Batch Download See http://ezid.cdlib.org/doc/apidoc.html#parameters. Repeated values should be given as an array value for the parameter key. +*Added in v1.3.0:* `Ezid::BatchDownload` class. + ``` >> batch = Ezid::BatchDownload.new(:csv) => # @@ -161,33 +165,6 @@ Notes: Accessors are also implemented for the `crossref`, `datacite`, and `erc` elements as described in the EZID API documentation. -**Setting default metadata values** - -Default metadata values can be set: - -```ruby -Ezid::Client.configure do |config| - # set multiple defaults with a hash - config.identifier.defaults = {status: "reserved", profile: "dc"} - # or set individual elements - config.identifier.defaults[:status] = "reserved" - config.identifier.defaults[:profile] = "dc" -end -``` - -Then new identifiers will receive the defaults: - -``` ->> identifier = Ezid::Identifier.create(shoulder: "ark:/99999/fk4") -I, [2014-12-09T11:38:37.335136 #32279] INFO -- : EZID MintIdentifier -- success: ark:/99999/fk4zs2w500 -I, [2014-12-09T11:38:38.153546 #32279] INFO -- : EZID GetIdentifierMetadata -- success: ark:/99999/fk4zs2w500 -=> # ->> identifier.profile -=> "dc" ->> identifier.status -=> "reserved" -``` - ## Authentication Credentials can be provided in any -- or a combination -- of these ways: diff --git a/VERSION b/VERSION index f0bb29e..88c5fb8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0 +1.4.0 diff --git a/ezid-client.gemspec b/ezid-client.gemspec index 80168bd..cc54a10 100644 --- a/ezid-client.gemspec +++ b/ezid-client.gemspec @@ -24,4 +24,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.7" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", "~> 3.1" + spec.add_development_dependency "rspec-its" end diff --git a/lib/ezid/batch_download.rb b/lib/ezid/batch_download.rb index c640f16..cd1d43e 100644 --- a/lib/ezid/batch_download.rb +++ b/lib/ezid/batch_download.rb @@ -1,7 +1,6 @@ require "hashie" require "net/http" require "uri" -require_relative "reserved_metadata" module Ezid class BatchDownloadError < Error; end diff --git a/lib/ezid/client.rb b/lib/ezid/client.rb index 8b48061..2c353e4 100644 --- a/lib/ezid/client.rb +++ b/lib/ezid/client.rb @@ -6,7 +6,6 @@ require_relative "session" require_relative "metadata" require_relative "identifier" -require_relative "proxy_identifier" require_relative "batch_download" Dir[File.expand_path("../responses/*.rb", __FILE__)].each { |m| require m } diff --git a/lib/ezid/error.rb b/lib/ezid/error.rb index f5c9b22..a092af9 100644 --- a/lib/ezid/error.rb +++ b/lib/ezid/error.rb @@ -1,3 +1,11 @@ module Ezid class Error < ::RuntimeError; end + + # The requested identifier was not found + class IdentifierNotFoundError < Error; end + + # The requested action is not allowed + class NotAllowedError < Error; end + + class DeletionError < Error; end end diff --git a/lib/ezid/identifier.rb b/lib/ezid/identifier.rb index 75c1db1..791592e 100644 --- a/lib/ezid/identifier.rb +++ b/lib/ezid/identifier.rb @@ -6,53 +6,111 @@ module Ezid # class Identifier - attr_reader :client - attr_accessor :id, :shoulder, :metadata, :state - - private :state, :state=, :id= - - # Attributes to display on inspect - INSPECT_ATTRS = %w( id status target created ).freeze + attr_accessor :id, :shoulder, :persisted, :deleted + private :persisted=, :persisted, :deleted=, :deleted class << self attr_accessor :defaults # Creates or mints an identifier (depending on arguments) # @see #save + # @overload create(id, metadata=nil) + # Creates an identifier + # @param id [String] the identifier to create + # @param metadata [Hash] the metadata to set on the identifier + # @overload create(metadata=nil) + # Mints an identifier + # @deprecated Use {.mint} instead + # @param metadata [Hash] the metadata to set on the identifier + # @return [Ezid::Identifier] the new identifier + # @raise [Ezid::Error] + def create(*args) + raise ArgumentError, "`mint` receives 0-2 arguments." if args.size > 2 + if args.first.is_a?(Hash) + warn "[DEPRECATION] Sending a hash as the first argument to `create` is deprecated and will raise an exception in 2.0. Use `create(id, metadata)` or `mint(metadata)` instead. (called from #{caller.first})" + metadata = args.first + id = metadata.delete(:id) + else + id, metadata = args + end + if id.nil? + warn "[DEPRECATION] Calling `create` without an id will raise an exception in 2.0. Use `mint` instead. (called from #{caller.first})" + shoulder = metadata.delete(:shoulder) + mint(shoulder, metadata) + else + new(id, metadata) { |i| i.save } + end + end + + # Mints a new identifier + # @overload mint(shoulder, metadata=nil) + # @param shoulder [String] the EZID shoulder on which to mint + # @param metadata [Hash] the metadata to set on the identifier + # @overload mint(metadata=nil) + # @param metadata [Hash] the metadata to set on the identifier # @return [Ezid::Identifier] the new identifier # @raise [Ezid::Error] - def create(attrs = {}) - identifier = new(attrs) - identifier.save + def mint(*args) + raise ArgumentError, "`mint` receives 0-2 arguments." if args.size > 2 + metadata = args.last.is_a?(Hash) ? args.pop : nil + new(metadata) do |i| + i.shoulder = args.first + i.save + end + end + + # Modifies the metadata of an existing identifier. + # @param id [String] the EZID identifier + # @param metadata [Hash] the metadata to update on the identifier + # @return [Ezid::Identifier] the identifier + # @raise [Ezid::IdentifierNotFoundError] + def modify(id, metadata) + i = allocate + i.id = id + i.update_metadata(metadata) + i.modify! end # Retrieves an identifier + # @param id [String] the EZID identifier to find # @return [Ezid::Identifier] the identifier - # @raise [Ezid::Error] if the identifier does not exist in EZID + # @raise [Ezid::IdentifierNotFoundError] if the identifier does not exist in EZID def find(id) - identifier = new(id: id) - identifier.load_metadata + i = allocate + i.id = id + i.load_metadata end end self.defaults = {} - def initialize(args={}) - @client = args.delete(:client) || Client.new - @id = args.delete(:id) - @shoulder = args.delete(:shoulder) - @state = :new - self.metadata = Metadata.new args.delete(:metadata) - update_metadata self.class.defaults.merge(args) # deprecate? + def initialize(*args) + raise ArgumentError, "`new` receives 0-2 arguments." if args.size > 2 + data = args.last.is_a?(Hash) ? args.pop : nil + @id = args.first + apply_default_metadata + if data + if shoulder = data.delete(:shoulder) + warn "[DEPRECATION] The `:shoulder` hash option is deprecated and will raise an exception in 2.0. Use `Ezid::Identifier.mint(shoulder, metadata)` to mint an identifier. (called by #{caller.first})" + @shoulder = shoulder + end + if anvl = data.delete(:metadata) + update_metadata(anvl) + end + update_metadata(data) + end + yield self if block_given? end def inspect - attrs = if deleted? - "id=\"#{id}\" DELETED" - else - INSPECT_ATTRS.map { |attr| "#{attr}=#{send(attr).inspect}" }.join(", ") - end - "#<#{self.class.name} #{attrs}>" + id_val = if id.nil? + "NEW" + elsif deleted? + "#{id} [DELETED]" + else + id + end + "#<#{self.class.name} id=#{id_val}>" end def to_s @@ -60,11 +118,16 @@ def to_s end # Returns the identifier metadata - # @param load [Boolean] - flag to load the metadata from EZID if stale (default: `true`) # @return [Ezid::Metadata] the metadata - def metadata(load = true) - load_metadata if load && stale? - @metadata + def metadata(_=nil) + if !_.nil? + warn "[DEPRECATION] The parameter of `metadata` is deprecated and will be removed in 2.0. (called from #{caller.first})" + end + @metadata ||= Metadata.new + end + + def remote_metadata + @remote_metadata ||= Metadata.new end # Persist the identifer and/or metadata to EZID. @@ -77,27 +140,41 @@ def metadata(load = true) def save raise Error, "Cannot save a deleted identifier." if deleted? persist - reset + reset_metadata + self + end + + # Force a modification of the EZID identifier -- i.e., + # assumes previously persisted without confirmation. + # @return [Ezid::Identifier] the identifier + # @raise [Ezid::Error] if `id` is nil + # @raise [Ezid::IdentifierNotFoundError] if EZID identifier does not exist. + def modify! + raise Error, "Cannot modify an identifier without and id." if id.nil? + modify + persists! + reset_metadata + self end # Updates the metadata # @param attrs [Hash] the metadata # @return [Ezid::Identifier] the identifier def update_metadata(attrs={}) - attrs.each { |k, v| send("#{k}=", v) } + metadata.update(attrs) self end # Is the identifier persisted? # @return [Boolean] def persisted? - state == :persisted + !!persisted end # Has the identifier been deleted? # @return [Boolean] def deleted? - state == :deleted + !!deleted end # Updates the metadata and saves the identifier @@ -111,24 +188,26 @@ def update(data={}) # @deprecated Use {#load_metadata} instead. def reload - warn "[DEPRECATION] `reload` is deprecated and will be removed in version 2.0. Use `load_metadata` instead." + warn "[DEPRECATION] `reload` is deprecated and will be removed in version 2.0. Use `load_metadata` instead. (called from #{caller.first})" load_metadata end - # Loads the metadata from EZID (local changes will be lost!) + # Loads the metadata from EZID # @return [Ezid::Identifier] the identifier # @raise [Ezid::Error] def load_metadata response = client.get_identifier_metadata(id) - self.metadata = Metadata.new(response.metadata) - self.state = :persisted + # self.remote_metadata = Metadata.new(response.metadata) + remote_metadata.replace(response.metadata) + persists! self end # Empties the (local) metadata (changes will be lost!) # @return [Ezid::Identifier] the identifier def reset - clear_metadata + warn "[DEPRECATION] `reset` is deprecated and will be removed in 2.0. Use `reset_metadata` instead. (called from #{caller.first})" + reset_metadata self end @@ -139,8 +218,10 @@ def reset def delete raise Error, "Only persisted, reserved identifiers may be deleted: #{inspect}." unless deletable? client.delete_identifier(id) - self.state = :deleted - reset + reset_metadata + self.deleted = true + self.persisted = false + self end # Is the identifier reserved? @@ -190,22 +271,32 @@ def public! self.status = Status::PUBLIC end + def client + @client ||= Client.new + end + + def reset_metadata + metadata.clear unless metadata.empty? + remote_metadata.clear unless remote_metadata.empty? + end + protected - def method_missing(method, *args) - metadata.send(method, *args) + def method_missing(*args) + local_or_remote_metadata(*args) rescue NoMethodError super end private - def stale? - persisted? && metadata(false).empty? - end - - def clear_metadata - metadata(false).clear + def local_or_remote_metadata(*args) + value = metadata.send(*args) + if value.nil? && persisted? + load_metadata if remote_metadata.empty? + value = remote_metadata.send(*args) + end + value end def modify @@ -227,7 +318,15 @@ def create def persist persisted? ? modify : create_or_mint - self.state = :persisted + persists! + end + + def persists! + self.persisted = true + end + + def apply_default_metadata + update_metadata(self.class.defaults) end end diff --git a/lib/ezid/metadata.rb b/lib/ezid/metadata.rb index 0750423..0c65d60 100644 --- a/lib/ezid/metadata.rb +++ b/lib/ezid/metadata.rb @@ -1,5 +1,4 @@ require "hashie" -require_relative "reserved_metadata" module Ezid # @@ -8,7 +7,6 @@ module Ezid # @api private # class Metadata < Hashie::Mash - include ReservedMetadata # EZID metadata field/value separator ANVL_SEPARATOR = ": " @@ -30,13 +28,35 @@ class Metadata < Hashie::Mash # A line ending LINE_ENDING_RE = /\r?\n/ # @api private - RESERVED_ALIASES = %w( - coowners datacenter export owner ownergroup - profile shadowedby shadows status target - ).freeze - def initialize(data={}) - super coerce(data) + # + # EZID reserved metadata elements + # + # @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata + # + COOWNERS = "_coowners".freeze + CREATED = "_created".freeze + DATACENTER = "_datacenter".freeze + EXPORT = "_export".freeze + OWNER = "_owner".freeze + OWNERGROUP = "_ownergroup".freeze + PROFILE = "_profile".freeze + SHADOWEDBY = "_shadowedby".freeze + SHADOWS = "_shadows".freeze + STATUS = "_status".freeze + TARGET = "_target".freeze + UPDATED = "_updated".freeze + RESERVED = [ + COOWNERS, CREATED, DATACENTER, EXPORT, OWNER, OWNERGROUP, + PROFILE, SHADOWEDBY, SHADOWS, STATUS, TARGET, UPDATED + ].freeze + READONLY = [ + CREATED, DATACENTER, OWNER, OWNERGROUP, SHADOWEDBY, SHADOWS, UPDATED + ].freeze + + def initialize(data=nil) + super() + update(data) if data end def elements @@ -53,6 +73,14 @@ def updated to_time(_updated) end + def update(data) + super coerce(data) + end + + def replace(data) + super coerce(data) + end + # Output metadata in EZID ANVL format # @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies # @return [String] the ANVL output @@ -74,13 +102,13 @@ def to_s # Overrides Hashie::Mash def convert_key(key) - k = super - if RESERVED_ALIASES.include?(k) - "_#{k}" - elsif k =~ /\A(dc|datacite|erc)_/ - k.sub(/_/, ".") + converted = super + if RESERVED.include?("_#{converted}") + "_#{converted}" + elsif converted =~ /\A(dc|datacite|erc)_/ + converted.sub(/_/, ".") else - k + converted end end @@ -93,9 +121,7 @@ def to_time(value) # Coerce data into a Hash of elements def coerce(data) - data.to_h - rescue NoMethodError - coerce_string(data) + data.respond_to?(:to_h) ? data.to_h : coerce_string(data) end # Escape string for sending to EZID host diff --git a/lib/ezid/proxy_identifier.rb b/lib/ezid/proxy_identifier.rb index ba670a3..0786b0e 100644 --- a/lib/ezid/proxy_identifier.rb +++ b/lib/ezid/proxy_identifier.rb @@ -1,6 +1,8 @@ module Ezid class ProxyIdentifier + warn "[DEPRECATION] `Ezid::ProxyIdentifier` is deprecated and will be removed in v2.0. Use `Ezid::Identifier` instead." + attr_reader :id attr_accessor :__real diff --git a/lib/ezid/reserved_metadata.rb b/lib/ezid/reserved_metadata.rb deleted file mode 100644 index 0451f8f..0000000 --- a/lib/ezid/reserved_metadata.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Ezid - # - # EZID reserved metadata elements - # - # @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata - # - module ReservedMetadata - COOWNERS = "_coowners".freeze - CREATED = "_created".freeze - DATACENTER = "_datacenter".freeze - EXPORT = "_export".freeze - OWNER = "_owner".freeze - OWNERGROUP = "_ownergroup".freeze - PROFILE = "_profile".freeze - SHADOWEDBY = "_shadowedby".freeze - SHADOWS = "_shadows".freeze - STATUS = "_status".freeze - TARGET = "_target".freeze - UPDATED = "_updated".freeze - - # Read-only elements - READONLY = [ - CREATED, DATACENTER, OWNER, OWNERGROUP, SHADOWEDBY, SHADOWS, UPDATED - ].freeze - end -end diff --git a/lib/ezid/responses/response.rb b/lib/ezid/responses/response.rb index 3ac62e9..6c38e63 100644 --- a/lib/ezid/responses/response.rb +++ b/lib/ezid/responses/response.rb @@ -59,7 +59,7 @@ def success? # Returns an exception instance if there was an error # @return [Ezid::Error] the exception def exception - @exception ||= (error? && Error.new(message)) + error_class.new(message) if error? end # The URI path of the request @@ -68,5 +68,16 @@ def uri_path __getobj__.uri.path end + def error_class + case message + when /no such identifier/ + IdentifierNotFoundError + when /identifier status does not support deletion/ + DeletionError + else + Error + end + end + end end diff --git a/spec/integration/identifier_spec.rb b/spec/integration/identifier_spec.rb index 92c5238..7d3a8c0 100644 --- a/spec/integration/identifier_spec.rb +++ b/spec/integration/identifier_spec.rb @@ -1,51 +1,47 @@ module Ezid RSpec.describe Identifier do + before { + @identifier = described_class.mint(TEST_ARK_SHOULDER, target: "http://example.com") + } + describe "CRUD operations" do + describe "mint" do + subject { @identifier } + it { is_expected.to be_a(described_class) } + end describe "create" do - describe "with a shoulder" do - subject { described_class.create(shoulder: TEST_ARK_SHOULDER) } - it "should mint an identifier" do - expect(subject).to be_a(described_class) - expect(subject.id).to match(/#{TEST_ARK_SHOULDER}/) - end - end - describe "with an id" do - let(:minted) { described_class.create(shoulder: TEST_ARK_SHOULDER) } - subject { described_class.create(id: "#{minted}/123") } - it "should create the identifier" do - expect(subject).to be_a(described_class) - expect(subject.id).to eq("#{minted}/123") - end + subject { described_class.create("#{@identifier}/123") } + it "should create the identifier" do + expect(subject).to be_a(described_class) + expect(subject.id).to eq("#{@identifier}/123") end end - describe "retrieve" do - let(:minted) { described_class.create(shoulder: TEST_ARK_SHOULDER, target: "http://example.com") } - subject { described_class.find(minted.id) } - it "should instantiate the identifier" do + subject { described_class.find(@identifier.id) } + it "instantiates the identifier" do expect(subject).to be_a(described_class) - expect(subject.id).to eq(minted.id) + expect(subject.id).to eq(@identifier.id) expect(subject.target).to eq("http://example.com") end end - describe "update" do - subject { described_class.create(shoulder: TEST_ARK_SHOULDER, target: "http://google.com") } - before do - subject.target = "http://example.com" + specify { + subject.target = "http://google.com" subject.save - end - it "should update the metadata" do - expect(subject.target).to eq("http://example.com") - end + expect(subject.target).to eq("http://google.com") + } + specify { + subject.update(target: "http://www.microsoft.com") + expect(subject.target).to eq("http://www.microsoft.com") + } end - describe "delete" do - subject { described_class.create(shoulder: TEST_ARK_SHOULDER, status: "reserved") } - before { subject.delete } - it "should delete the identifier" do - expect { described_class.find(subject.id) }.to raise_error(Error) + subject { described_class.mint(TEST_ARK_SHOULDER, status: "reserved") } + it "deletes the identifier" do + subject.delete + expect(subject).to be_deleted + expect { described_class.find(subject.id) }.to raise_error(IdentifierNotFoundError) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4959c2c..65c4903 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,6 +15,8 @@ # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +require "rspec/its" + require "ezid/test_helper" ezid_test_mode! diff --git a/spec/unit/batch_download_spec.rb b/spec/unit/batch_download_spec.rb new file mode 100644 index 0000000..77e2532 --- /dev/null +++ b/spec/unit/batch_download_spec.rb @@ -0,0 +1,5 @@ +module Ezid + RSpec.describe BatchDownload do + # TODO + end +end diff --git a/spec/unit/identifier_spec.rb b/spec/unit/identifier_spec.rb index d4673f9..e30dd8c 100644 --- a/spec/unit/identifier_spec.rb +++ b/spec/unit/identifier_spec.rb @@ -1,283 +1,349 @@ module Ezid RSpec.describe Identifier do - - describe ".create" do - let(:attrs) { {shoulder: TEST_ARK_SHOULDER, profile: "dc", target: "http://example.com"} } - it "instantiates a new Identifier and saves it" do - expect(described_class).to receive(:new).with(attrs).and_call_original - expect_any_instance_of(described_class).to receive(:save) { double } - described_class.create(attrs) + describe "class methods" do + describe ".create" do + let(:args) { ["id", {profile: "dc", target: "http://example.com"}] } + it "instantiates a new Identifier and saves it" do + expect_any_instance_of(described_class).to receive(:save) { double(id: "id") } + described_class.create(*args) + end end - end - - describe ".find" do - it "instantiates a new identifier and loads the metadata" do - expect(described_class).to receive(:new).with(id: "id").and_call_original - expect_any_instance_of(described_class).to receive(:load_metadata) { double } - described_class.find("id") + describe ".mint" do + let(:attrs) { {profile: "dc", target: "http://example.com"} } + let(:args) { [TEST_ARK_SHOULDER, attrs] } + it "instantiates a new Identifier and saves it" do + expect_any_instance_of(described_class).to receive(:save) { double(id: "id") } + described_class.mint(*args) + end end - end - - describe ".defaults" do - before { @original_defaults = described_class.defaults } - after { described_class.defaults = @original_defaults } - it "can be set via client config" do - Client.config.identifier.defaults = {status: "reserved"} - expect(described_class.defaults).to eq({status: "reserved"}) + describe ".modify" do + let(:args) { ["id", {profile: "dc", target: "http://example.com"}] } + it "instantiates a new Indentifier and modifies it" do + expect_any_instance_of(described_class).not_to receive(:save) + expect_any_instance_of(described_class).to receive(:modify!) + described_class.modify(*args) + end + end + describe ".find" do + it "instantiates a new identifier and loads the metadata" do + expect_any_instance_of(described_class).to receive(:id=).with("id").and_call_original + expect_any_instance_of(described_class).to receive(:load_metadata) { + double(id: "id", metadata: nil) + } + described_class.find("id") + end + end + describe ".defaults" do + before { @original_defaults = described_class.defaults } + after { described_class.defaults = @original_defaults } + it "can be set via client config" do + Client.config.identifier.defaults = {status: "reserved"} + expect(described_class.defaults).to eq({status: "reserved"}) + end end end - describe "#initialize" do - describe "with metadata" do - describe "via the :metadata argument" do - subject { described_class.new(metadata: "_profile: dc\n_target: http://example.com") } - it "sets the metadata" do - expect(subject.profile).to eq("dc") - expect(subject.target).to eq("http://example.com") + describe "instance methods" do + describe "#initialize" do + before { + allow(described_class).to receive(:defaults) { defaults } + } + let(:defaults) { {} } + describe "with no arguments" do + its(:id) { is_expected.to be_nil } + describe "and no default metadata" do + its(:metadata) { is_expected.to be_empty } end - end - describe "via keyword arguments" do - subject { described_class.new(profile: "dc", target: "http://example.com") } - it "sets the metadata" do - expect(subject.profile).to eq("dc") - expect(subject.target).to eq("http://example.com") + describe "and with default metadata" do + let(:defaults) { {export: "no"} } + its(:metadata) { is_expected.to eq({"_export"=>"no"}) } end end - end - describe "default metadata" do - before do - allow(described_class).to receive(:defaults) { {profile: "dc", status: "reserved"} } + describe "with an id and no metadata" do + subject { described_class.new("id") } + its(:id) { is_expected.to eq("id") } + describe "and no default metadata" do + its(:metadata) { is_expected.to be_empty } + end + describe "and with default metadata" do + let(:defaults) { {export: "no"} } + its(:metadata) { is_expected.to eq({"_export"=>"no"}) } + end end - it "sets the default metadata" do - expect(subject.profile).to eq("dc") - expect(subject.status).to eq("reserved") + describe "with an id and metadata" do + subject { described_class.new("id", metadata: "_profile: dc\n_target: http://example.com", status: "reserved") } + its(:id) { is_expected.to eq("id") } + describe "and no default metadata" do + its(:metadata) { is_expected.to eq("_profile"=>"dc", "_target"=>"http://example.com", "_status"=>"reserved") } + end + describe "and with default metadata" do + let(:defaults) { {export: "no", status: "public"} } + its(:metadata) { is_expected.to eq("_profile"=>"dc", "_target"=>"http://example.com", "_status"=>"reserved", "_export"=>"no") } + end end - context "when explicit arguments override the defaults" do - subject { described_class.new(shoulder: TEST_ARK_SHOULDER, status: "public") } - it "overrides the defaults" do - expect(subject.profile).to eq("dc") - expect(subject.status).to eq("public") + describe "with only metadata" do + subject { described_class.new(metadata: "_profile: dc\n_target: http://example.com", status: "reserved") } + its(:id) { is_expected.to be_nil } + describe "and no default metadata" do + its(:metadata) { is_expected.to eq("_profile"=>"dc", "_target"=>"http://example.com", "_status"=>"reserved") } + end + describe "and with default metadata" do + let(:defaults) { {export: "no", status: "public"} } + its(:metadata) { is_expected.to eq("_profile"=>"dc", "_target"=>"http://example.com", "_status"=>"reserved", "_export"=>"no") } end end end - end - describe "#update" do - let(:metadata) { {"status" => "unavailable"} } - subject { described_class.new(id: "id") } - it "updates the metadata and saves" do - expect(subject).to receive(:update_metadata).with(metadata) - expect(subject).to receive(:save) { double } - subject.update(metadata) + describe "#update" do + let(:metadata) { {"status" => "unavailable"} } + subject { described_class.new(id: "id") } + it "updates the metadata and saves" do + expect(subject).to receive(:update_metadata).with(metadata) + expect(subject).to receive(:save) { double } + subject.update(metadata) + end end - end - describe "#update_metadata" do - it "updates the metadata" do - subject.update_metadata(:status => "public", _target: "localhost", "dc.creator" => "Me") - expect(subject.metadata.to_h).to eq({"_status"=>"public", "_target"=>"localhost", "dc.creator"=>"Me"}) + describe "#modify!" do + describe "when the Identifier has no id" do + specify { + expect { subject.modify! }.to raise_error(Error) + } + end + describe "when the Identifier has an id" do + specify { + subject.id = "id" + expect(subject).not_to receive(:save) + expect(subject).to receive(:modify) + subject.modify! + } + describe "when the identifier does not exist" do + specify { + subject.id = "id" + allow(subject.client).to receive(:modify_identifier).and_raise(IdentifierNotFoundError) + expect { subject.modify! }.to raise_error(IdentifierNotFoundError) + } + end + end end - end - describe "#load_metadata" do - let(:metadata) { "_profile: erc" } - before { allow(subject).to receive(:id) { "id" } } - it "initializes the metadata from EZID" do - expect(subject.client).to receive(:get_identifier_metadata).with("id") { double(id: "id", metadata: metadata) } - expect(Metadata).to receive(:new).with(metadata) - subject.load_metadata + describe "#update_metadata" do + it "updates the metadata" do + subject.update_metadata(:status => "public", _target: "localhost", "dc.creator" => "Me") + expect(subject.metadata.to_h).to eq({"_status"=>"public", "_target"=>"localhost", "dc.creator"=>"Me"}) + end end - end - describe "#reset" do - before { subject.metadata = Metadata.new(status: "public") } - it "clears the local metadata" do - expect { subject.reset }.to change { subject.metadata.empty? }.from(false).to(true) + describe "#load_metadata" do + let(:metadata) { "_profile: erc" } + before { allow(subject).to receive(:id) { "id" } } + it "replaces the remote metadata with metadata from EZID" do + expect(subject.client).to receive(:get_identifier_metadata).with("id") { double(id: "id", metadata: metadata) } + expect(subject.remote_metadata).to receive(:replace).with(metadata) + subject.load_metadata + end end - end - describe "#persisted?" do - describe "after initialization" do - it { is_expected.not_to be_persisted } - end - describe "when saving an unpersisted object" do - before { allow(subject).to receive(:create_or_mint) { nil } } - it "marks it as persisted" do - expect { subject.save }.to change(subject, :persisted?).from(false).to(true) + describe "#reset_metadata" do + before { + subject.status = "public" + subject.remote_metadata.profile = "dc" + } + it "clears the local metadata" do + expect { subject.reset_metadata } + .to change { subject.metadata.empty? } + .from(false).to(true) + end + it "clears the remote metadata" do + expect { subject.reset_metadata } + .to change { subject.remote_metadata.empty? } + .from(false).to(true) end end - describe "when saving a persisted object" do - before do - allow(subject).to receive(:persisted?) { true } - allow(subject).to receive(:modify) { nil } + + describe "#persisted?" do + describe "after initialization" do + it { is_expected.not_to be_persisted } end - it "does not change the persisted status" do - expect { subject.save }.not_to change(subject, :persisted?) + describe "when saving an unpersisted object" do + before { + allow(subject.client).to receive(:mint_identifier) { double(id: "id") } + subject.save + } + it { is_expected.to be_persisted } + end + describe "when saving a persisted object" do + before do + allow(subject).to receive(:persisted?) { true } + allow(subject).to receive(:modify) { nil } + end + it "does not change the persisted status" do + expect { subject.save }.not_to change(subject, :persisted?) + end end end - end - describe "#delete" do - context "when the identifier is reserved" do - subject { described_class.new(id: "id", status: Status::RESERVED) } - context "and is persisted" do - before { allow(subject).to receive(:persisted?) { true } } - it "deletes the identifier" do - expect(subject.client).to receive(:delete_identifier).with("id") { double(id: "id") } - subject.delete - expect(subject).to be_deleted + describe "#delete" do + context "when the identifier is reserved" do + subject { described_class.new("id", status: Status::RESERVED) } + context "and is persisted" do + before { allow(subject).to receive(:persisted?) { true } } + it "deletes the identifier" do + expect(subject.client).to receive(:delete_identifier).with("id") { double(id: "id") } + subject.delete + expect(subject).to be_deleted + end + end + context "and is not persisted" do + before { allow(subject).to receive(:persisted?) { false } } + it "raises an exception" do + expect { subject.delete }.to raise_error(Error) + end end end - context "and is not persisted" do - before { allow(subject).to receive(:persisted?) { false } } + context "when identifier is not reserved" do + subject { described_class.new(id: "id", status: Status::PUBLIC) } it "raises an exception" do expect { subject.delete }.to raise_error(Error) end end end - context "when identifier is not reserved" do - subject { described_class.new(id: "id", status: Status::PUBLIC) } - it "raises an exception" do - expect { subject.delete }.to raise_error(Error) - end - end - end - describe "#save" do - context "when the identifier is persisted" do - let(:metadata) { Metadata.new } - before do - allow(subject).to receive(:id) { "id" } - allow(subject).to receive(:persisted?) { true } - allow(subject).to receive(:metadata) { metadata } - end - it "modifies the identifier" do - expect(subject.client).to receive(:modify_identifier).with("id", metadata) { double(id: "id") } - subject.save - end - end - context "when the identifier is not persisted" do - before do - allow(subject).to receive(:persisted?) { false } - end - context "and `id' is present" do - before { allow(subject).to receive(:id) { "id" } } - it "creates the identifier" do - expect(subject.client).to receive(:create_identifier).with("id", subject.metadata) { double(id: "id") } + describe "#save" do + context "when the identifier is persisted" do + let(:metadata) { Metadata.new } + before do + allow(subject).to receive(:id) { "id" } + allow(subject).to receive(:persisted?) { true } + allow(subject).to receive(:metadata) { metadata } + end + it "modifies the identifier" do + expect(subject.client).to receive(:modify_identifier).with("id", metadata) { double(id: "id") } subject.save end end - context "and `id' is not present" do - context "and `shoulder' is present" do - before { allow(subject).to receive(:shoulder) { TEST_ARK_SHOULDER } } - it "mints the identifier" do - expect(subject.client).to receive(:mint_identifier).with(TEST_ARK_SHOULDER, subject.metadata) { double(id: "id") } + context "when the identifier is not persisted" do + before do + allow(subject).to receive(:persisted?) { false } + end + context "and `id' is present" do + before { allow(subject).to receive(:id) { "id" } } + it "creates the identifier" do + expect(subject.client).to receive(:create_identifier).with("id", subject.metadata) { double(id: "id") } subject.save end end - context "and `shoulder' is not present" do - before { allow(Client.config).to receive(:default_shoulder) { nil } } - it "raises an exception" do - expect { subject.save }.to raise_error(Error) + context "and `id' is not present" do + context "and `shoulder' is present" do + before { allow(subject).to receive(:shoulder) { TEST_ARK_SHOULDER } } + it "mints the identifier" do + expect(subject.client).to receive(:mint_identifier).with(TEST_ARK_SHOULDER, subject.metadata) { double(id: "id") } + subject.save + end + end + context "and `shoulder' is not present" do + before { allow(Client.config).to receive(:default_shoulder) { nil } } + it "raises an exception" do + expect { subject.save }.to raise_error(Error) + end end end end end - end - describe "boolean status methods" do - context "when the identifier is public" do - before { subject.public! } - it { is_expected.to be_public } - it { is_expected.not_to be_reserved } - it { is_expected.not_to be_unavailable } - end - context "when the identifier is reserved" do - before { subject.status = Status::RESERVED } - it { is_expected.not_to be_public } - it { is_expected.to be_reserved } - it { is_expected.not_to be_unavailable } - end - context "when the identifier is unavailable" do - context "and it has no reason" do - before { subject.unavailable! } - it { is_expected.not_to be_public } + describe "boolean status methods" do + context "when the identifier is public" do + before { subject.public! } + it { is_expected.to be_public } it { is_expected.not_to be_reserved } - it { is_expected.to be_unavailable } + it { is_expected.not_to be_unavailable } end - context "and it has a reason" do - before { subject.unavailable!("withdrawn") } + context "when the identifier is reserved" do + before { subject.status = Status::RESERVED } it { is_expected.not_to be_public } - it { is_expected.not_to be_reserved } - it { is_expected.to be_unavailable } + it { is_expected.to be_reserved } + it { is_expected.not_to be_unavailable } + end + context "when the identifier is unavailable" do + context "and it has no reason" do + before { subject.unavailable! } + it { is_expected.not_to be_public } + it { is_expected.not_to be_reserved } + it { is_expected.to be_unavailable } + end + context "and it has a reason" do + before { subject.unavailable!("withdrawn") } + it { is_expected.not_to be_public } + it { is_expected.not_to be_reserved } + it { is_expected.to be_unavailable } + end end end - end - describe "status-changing methods" do - subject { described_class.new(id: "id", status: status) } - describe "#unavailable!" do - context "when the status is \"unavailable\"" do - let(:status) { "#{Status::UNAVAILABLE} | whatever" } - context "and no reason is given" do - it "logs a warning" do - pending "https://github.com/duke-libraries/ezid-client/issues/46" - allow_message_expectations_on_nil - expect(subject.logger).to receive(:warn) - subject.unavailable! + describe "status-changing methods" do + subject { described_class.new(id: "id", status: status) } + describe "#unavailable!" do + context "when the status is \"unavailable\"" do + let(:status) { "#{Status::UNAVAILABLE} | whatever" } + context "and no reason is given" do + it "logs a warning" do + pending "https://github.com/duke-libraries/ezid-client/issues/46" + allow_message_expectations_on_nil + expect(subject.logger).to receive(:warn) + subject.unavailable! + end + it "does not change the status" do + expect { subject.unavailable! }.not_to change(subject, :status) + end end - it "does not change the status" do - expect { subject.unavailable! }.not_to change(subject, :status) + context "and a reason is given" do + it "logs a warning" do + pending "https://github.com/duke-libraries/ezid-client/issues/46" + allow_message_expectations_on_nil + expect(subject.logger).to receive(:warn) + subject.unavailable!("because") + end + it "should change the status" do + expect { subject.unavailable!("because") }.to change(subject, :status).from(status).to("#{Status::UNAVAILABLE} | because") + end end end - context "and a reason is given" do - it "logs a warning" do - pending "https://github.com/duke-libraries/ezid-client/issues/46" - allow_message_expectations_on_nil - expect(subject.logger).to receive(:warn) - subject.unavailable!("because") + context "when the status is \"reserved\"" do + let(:status) { Status::RESERVED } + context "and persisted" do + before { allow(subject).to receive(:persisted?) { true } } + it "raises an exception" do + expect { subject.unavailable! }.to raise_error(Error) + end end - it "should change the status" do - expect { subject.unavailable!("because") }.to change(subject, :status).from(status).to("#{Status::UNAVAILABLE} | because") + context "and not persisted" do + before { allow(subject).to receive(:persisted?) { false } } + it "changes the status" do + expect { subject.unavailable! }.to change(subject, :status).from(Status::RESERVED).to(Status::UNAVAILABLE) + end end end - end - context "when the status is \"reserved\"" do - let(:status) { Status::RESERVED } - context "and persisted" do - before { allow(subject).to receive(:persisted?) { true } } - it "raises an exception" do - expect { subject.unavailable! }.to raise_error(Error) + context "when the status is \"public\"" do + let(:status) { Status::PUBLIC } + context "and no reason is given" do + it "changes the status" do + expect { subject.unavailable! }.to change(subject, :status).from(Status::PUBLIC).to(Status::UNAVAILABLE) + end end - end - context "and not persisted" do - before { allow(subject).to receive(:persisted?) { false } } - it "changes the status" do - expect { subject.unavailable! }.to change(subject, :status).from(Status::RESERVED).to(Status::UNAVAILABLE) + context "and a reason is given" do + it "changes the status and appends the reason" do + expect { subject.unavailable!("withdrawn") }.to change(subject, :status).from(Status::PUBLIC).to("#{Status::UNAVAILABLE} | withdrawn") + end end end end - context "when the status is \"public\"" do - let(:status) { Status::PUBLIC } - context "and no reason is given" do - it "changes the status" do - expect { subject.unavailable! }.to change(subject, :status).from(Status::PUBLIC).to(Status::UNAVAILABLE) - end + describe "#public!" do + subject { described_class.new(id: "id", status: Status::UNAVAILABLE) } + it "changes the status" do + expect { subject.public! }.to change(subject, :status).from(Status::UNAVAILABLE).to(Status::PUBLIC) end - context "and a reason is given" do - it "changes the status and appends the reason" do - expect { subject.unavailable!("withdrawn") }.to change(subject, :status).from(Status::PUBLIC).to("#{Status::UNAVAILABLE} | withdrawn") - end - end - end - end - describe "#public!" do - subject { described_class.new(id: "id", status: Status::UNAVAILABLE) } - it "changes the status" do - expect { subject.public! }.to change(subject, :status).from(Status::UNAVAILABLE).to(Status::PUBLIC) end end end - end end