diff --git a/lib/ezid/identifier.rb b/lib/ezid/identifier.rb index 6249f0a..62c1c3c 100644 --- a/lib/ezid/identifier.rb +++ b/lib/ezid/identifier.rb @@ -2,14 +2,14 @@ module Ezid # # Represents an EZID identifier as a resource. # - # Ezid::Identifier delegates access to registered metadata elements through #method_missing. - # # @api public # class Identifier - attr_reader :id, :client - attr_accessor :shoulder, :metadata + 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 ) @@ -46,15 +46,16 @@ def initialize(args={}) @client = args.delete(:client) || Client.new @id = args.delete(:id) @shoulder = args.delete(:shoulder) - @deleted = false - init_metadata(args) + @state = :new + self.metadata = Metadata.new args.delete(:metadata) + update_metadata self.class.defaults.merge(args) # deprecate? end def inspect attrs = if deleted? "id=\"#{id}\" DELETED" else - INSPECT_ATTRS.map { |attr| "#{attr}=\"#{send(attr)}\"" }.join(" ") + INSPECT_ATTRS.map { |attr| "#{attr}=#{send(attr).inspect}" }.join(", ") end "#<#{self.class.name} #{attrs}>" end @@ -63,6 +64,14 @@ def to_s id end + # Returns the identifier metadata + # @param refresh [Boolean] - flag to refresh the metadata from EZID if stale (default: `true`) + # @return [Ezid::Metadata] the metadata + def metadata(refresh = true) + refresh_metadata if refresh && stale? + @metadata + end + # Persist the identifer and/or metadata to EZID. # If the identifier is already persisted, this is an update operation; # Otherwise, create (if it has an id) or mint (if it has a shoulder) @@ -72,8 +81,8 @@ def to_s # with an error status. def save raise Error, "Cannot save a deleted identifier." if deleted? - persisted? ? modify : create_or_mint - reload + persist + reset end # Updates the metadata @@ -87,14 +96,13 @@ def update_metadata(attrs={}) # Is the identifier persisted? # @return [Boolean] def persisted? - return false if deleted? - !!(id && created) + state == :persisted end # Has the identifier been deleted? # @return [Boolean] def deleted? - @deleted + state == :deleted end # Updates the metadata and saves the identifier @@ -128,7 +136,7 @@ def reset def delete raise Error, "Only persisted, reserved identifiers may be deleted: #{inspect}." unless deletable? client.delete_identifier(id) - @deleted = true + self.state = :deleted reset end @@ -174,44 +182,49 @@ def public! protected - def method_missing(method, *args) - metadata.send(method, *args) - rescue NoMethodError - super - end + def method_missing(method, *args) + metadata.send(method, *args) + rescue NoMethodError + super + end private - def refresh_metadata - response = client.get_identifier_metadata(id) - @metadata = Metadata.new(response.metadata) - end + def stale? + persisted? && metadata(false).empty? + end - def clear_metadata - @metadata.clear - end + def refresh_metadata + response = client.get_identifier_metadata(id) + self.metadata = Metadata.new response.metadata + self.state = :persisted + end - def modify - client.modify_identifier(id, metadata) - end + def clear_metadata + metadata(false).clear + end - def create_or_mint - id ? create : mint - end + def modify + client.modify_identifier(id, metadata) + end - def mint - response = client.mint_identifier(shoulder, metadata) - @id = response.id - end + def create_or_mint + id ? create : mint + end - def create - client.create_identifier(id, metadata) - end + def mint + response = client.mint_identifier(shoulder, metadata) + self.id = response.id + end - def init_metadata(args) - @metadata = Metadata.new(args.delete(:metadata)) - update_metadata(self.class.defaults.merge(args)) - end + def create + client.create_identifier(id, metadata) + end + + def persist + persisted? ? modify : create_or_mint + self.state = :persisted + end end end diff --git a/lib/ezid/metadata.rb b/lib/ezid/metadata.rb index d3a1330..d7b63da 100644 --- a/lib/ezid/metadata.rb +++ b/lib/ezid/metadata.rb @@ -1,4 +1,4 @@ -require "delegate" +require "forwardable" module Ezid # @@ -6,37 +6,49 @@ module Ezid # # @api private # - class Metadata < SimpleDelegator + class Metadata + extend Forwardable + + attr_reader :elements + + def_delegators :elements, :[], :[]=, :each, :clear, :to_h, :empty? class << self - def metadata_reader(method, alias_as=nil) - define_method method do - self[method.to_s] + def metadata_reader(element, alias_as=nil) + define_method element do + get(element) end if alias_as - alias_method alias_as, method + alias_method alias_as, element end end - def metadata_writer(method, alias_as=nil) - define_method "#{method}=" do |value| - self[method.to_s] = value + def metadata_writer(element, alias_as=nil) + define_method "#{element}=" do |value| + set(element, value) end if alias_as - alias_method "#{alias_as}=".to_sym, "#{method}=".to_sym + alias_method "#{alias_as}=".to_sym, "#{element}=".to_sym end end - def metadata_accessor(method, alias_as=nil) - metadata_reader method, alias_as - metadata_writer method, alias_as + def metadata_accessor(element, alias_as=nil) + metadata_reader element, alias_as + metadata_writer element, alias_as end - def metadata_profile(profile, *methods) - methods.each do |method| - element = [profile, method].join(".") - alias_as = [profile, method].join("_") - metadata_accessor element, alias_as + def metadata_profile(profile, *elements) + elements.each do |element| + profile_element = [profile, element].join(".") + method = [profile, element].join("_") + + define_method method do + get(profile_element) + end + + define_method "#{method}=" do |value| + set(profile_element, value) + end end end end @@ -71,6 +83,20 @@ def metadata_profile(profile, *methods) # @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata READONLY = %w( _owner _ownergroup _shadows _shadowedby _datacenter _created _updated ) + # EZID metadata profiles - a hash of (profile => elements) + # @see http://ezid.cdlib.org/doc/apidoc.html#metadata-profiles + # @note crossref is not included because it is a simple element + PROFILES = { + dc: [:creator, :title, :publisher, :date, :type], + datacite: [:creator, :title, :publisher, :publicationyear, :resourcetype], + erc: [:who, :what, :when] + } + + PROFILES.each do |profile, elements| + metadata_profile profile, *elements + end + + # Accessors for EZID internal metadata elements metadata_accessor :_coowners, :coowners metadata_accessor :_crossref metadata_accessor :_export, :export @@ -78,10 +104,7 @@ def metadata_profile(profile, *methods) metadata_accessor :_status, :status metadata_accessor :_target, :target - metadata_accessor :crossref - metadata_accessor :datacite - metadata_accessor :erc - + # Readers for EZID read-only internal metadata elements metadata_reader :_created metadata_reader :_datacenter, :datacenter metadata_reader :_owner, :owner @@ -90,12 +113,13 @@ def metadata_profile(profile, *methods) metadata_reader :_shadows, :shadows metadata_reader :_updated - metadata_profile :dc, :creator, :title, :publisher, :date, :type - metadata_profile :datacite, :creator, :title, :publisher, :publicationyear, :resourcetype - metadata_profile :erc, :who, :what, :when + # Accessors for + metadata_accessor :crossref + metadata_accessor :datacite + metadata_accessor :erc def initialize(data={}) - super coerce(data) + @elements = coerce(data) end def created @@ -110,19 +134,41 @@ def updated # @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies # @return [String] the ANVL output def to_anvl(include_readonly = true) - hsh = __getobj__.dup + hsh = elements.dup hsh.reject! { |k, v| READONLY.include?(k) } unless include_readonly - elements = hsh.map do |name, value| + lines = hsh.map do |name, value| element = [escape(ESCAPE_NAMES_RE, name), escape(ESCAPE_VALUES_RE, value)] element.join(ANVL_SEPARATOR) end - elements.join("\n").force_encoding(Encoding::UTF_8) + lines.join("\n").force_encoding(Encoding::UTF_8) + end + + def inspect + "#<#{self.class.name} elements=#{elements.inspect}>" end def to_s to_anvl end + def get(element) + self[element.to_s] + end + + def set(element, value) + self[element.to_s] = value + end + + protected + + def method_missing(method, *args) + return get(method) if args.size == 0 + if element = method.to_s[/^([^=]+)=$/, 1] + return set(element, *args) + end + super + end + private def to_time(value) diff --git a/spec/unit/identifier_spec.rb b/spec/unit/identifier_spec.rb index d24e802..b488743 100644 --- a/spec/unit/identifier_spec.rb +++ b/spec/unit/identifier_spec.rb @@ -78,6 +78,13 @@ module Ezid end end + describe "#update_metadata" do + it "should update 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 + describe "#reload" do let(:metadata) { "_profile: erc" } before { allow(subject).to receive(:id) { "id" } } @@ -89,29 +96,29 @@ module Ezid end describe "#reset" do + before { subject.metadata = Metadata.new(status: "public") } it "should clear the local metadata" do - expect(subject.metadata).to receive(:clear) - subject.reset + expect { subject.reset }.to change { subject.metadata.empty? }.from(false).to(true) end end describe "#persisted?" do - it "should be false if id is nil" do - expect(subject).not_to be_persisted + describe "after initialization" do + it { is_expected.not_to be_persisted } end - context "when `created' is nil" do - before { allow(subject).to receive(:id) { "ark:/99999/fk4fn19h88" } } - it "should be false" do - expect(subject).not_to be_persisted + describe "when saving an unpersisted object" do + before { allow(subject).to receive(:create_or_mint) { nil } } + it "should mark it as persisted" do + expect { subject.save }.to change(subject, :persisted?).from(false).to(true) end end - context "when id and `created' are present" do + describe "when saving a persisted object" do before do - allow(subject).to receive(:id) { "ark:/99999/fk4fn19h88" } - subject.metadata["_created"] = "1416507086" + allow(subject).to receive(:persisted?) { true } + allow(subject).to receive(:modify) { nil } end - it "should be true" do - expect(subject).to be_persisted + it "should not change the persisted status" do + expect { subject.save }.not_to change(subject, :persisted?) end end end @@ -143,14 +150,15 @@ module Ezid end describe "#save" do - before { allow(subject).to receive(:reload) { double } } 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 "should modify the identifier" do - expect(subject.client).to receive(:modify_identifier).with("id", subject.metadata) { double(id: "id") } + expect(subject.client).to receive(:modify_identifier).with("id", metadata) { double(id: "id") } subject.save end end diff --git a/spec/unit/metadata_spec.rb b/spec/unit/metadata_spec.rb index 6794861..1a7085a 100644 --- a/spec/unit/metadata_spec.rb +++ b/spec/unit/metadata_spec.rb @@ -195,15 +195,15 @@ module Ezid EOS end it "should treat the string as an ANVL document, splitting into keys and values and unescaping" do - expect(subject).to eq({ "_updated" => "1416507086", - "_target" => "http://example.com/path%20with%20spaces", - "_profile" => "erc", - "_erc" => "who: Proust, Marcel\nwhat: Remembrance of Things Past", - "_ownergroup" => "apitest", - "_owner" => "apitest", - "_export" => "yes", - "_created" => "1416507086", - "_status" => "public" }) + expect(subject.elements).to eq({ "_updated" => "1416507086", + "_target" => "http://example.com/path%20with%20spaces", + "_profile" => "erc", + "_erc" => "who: Proust, Marcel\nwhat: Remembrance of Things Past", + "_ownergroup" => "apitest", + "_owner" => "apitest", + "_export" => "yes", + "_created" => "1416507086", + "_status" => "public" }) end end context "of a hash-like object" do @@ -219,14 +219,14 @@ module Ezid end context "which is a normal Hash" do let(:data) { hsh } - it "should set the metadata to the hash" do - expect(subject).to eq(hsh) + it "should set the metadata elements to the hash" do + expect(subject.elements).to eq(hsh) end end context "which is a Metadata instance" do let(:data) { Metadata.new(hsh) } - it "should set the metadata to the hash" do - expect(subject).to eq(hsh) + it "should set the metadata elements to the hash" do + expect(subject.elements).to eq(hsh) end end end