Skip to content

Commit

Permalink
Merge pull request #43 from duke-libraries/lazy-metadata
Browse files Browse the repository at this point in the history
Identifier metadata is now loaded lazily from EZID
dchandekstark committed Feb 27, 2015
2 parents 6cd2857 + 3012f66 commit a367f41
Showing 4 changed files with 166 additions and 99 deletions.
97 changes: 55 additions & 42 deletions lib/ezid/identifier.rb
Original file line number Diff line number Diff line change
@@ -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
104 changes: 75 additions & 29 deletions lib/ezid/metadata.rb
Original file line number Diff line number Diff line change
@@ -1,42 +1,54 @@
require "delegate"
require "forwardable"

module Ezid
#
# EZID metadata collection for an identifier.
#
# @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,17 +83,28 @@ 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
metadata_accessor :_profile, :profile
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)
38 changes: 23 additions & 15 deletions spec/unit/identifier_spec.rb
Original file line number Diff line number Diff line change
@@ -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
26 changes: 13 additions & 13 deletions spec/unit/metadata_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a367f41

Please sign in to comment.