Skip to content

Commit

Permalink
Merge branch 'release-1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
dchandekstark committed Feb 25, 2015
2 parents 2087d8b + c71af46 commit ad5cfd2
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 260 deletions.
15 changes: 2 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Accessors are provided to ease the use of EZID [reserved metadata elements](http
Notes:
- `_crossref` is an exception because `crossref` is also the name of a metadata profile and a special element. Use `identifier._crossref` to read and `identifier._crossref = value` to write.
- Reserved elements which are not user-writeable do not implement writers.
- Special readers are implemented for reserved elements having date/time values -- `_created` and `_updated` -- which convert the string time values of EZID to Ruby `Time` instances.
- Special readers are implemented for reserved elements having date/time values (`_created` and `_updated`) which convert the string time values of EZID to Ruby `Time` instances.

**Metadata profile elements** can be read and written using the name of the element, replacing the dot (".") with an underscore:

Expand All @@ -156,18 +156,7 @@ Notes:
=> "Image"
```

**Registering custom metadata elements**

Custom metadata element accessors can be created by a registration process:

```ruby
Ezid::Client.configure do |config|
# register the element "custom"
config.metadata.register_element :custom
# register the element "dc.identifier" under the accessor :dc_identifier
config.metadata.register_element :dc_identifier, name: "dc.identifier"
end
```
Accessors are also implemented for the `crossref`, `datacite`, and `erc` elements as described in the EZID API documentation.

**Setting default metadata values**

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.13.0
1.0.0
259 changes: 97 additions & 162 deletions lib/ezid/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@ module Ezid
# @api private
#
class Metadata < SimpleDelegator

class << self
def metadata_reader(method, alias_as=nil)
define_method method do
self[method.to_s]
end
if alias_as
alias_method alias_as, method
end
end

def metadata_writer(method, alias_as=nil)
define_method "#{method}=" do |value|
self[method.to_s] = value
end
if alias_as
alias_method "#{alias_as}=".to_sym, "#{method}=".to_sym
end
end

def metadata_accessor(method, alias_as=nil)
metadata_reader method, alias_as
metadata_writer method, 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
end
end
end

# EZID metadata field/value separator
ANVL_SEPARATOR = ": "
Expand Down Expand Up @@ -34,195 +67,97 @@ class Metadata < SimpleDelegator
# A line ending
LINE_ENDING_RE = /\r?\n/

# A metadata element
Element = Struct.new(:name, :reader, :writer)

# Metadata profiles
PROFILES = {
"dc" => %w( creator title publisher date type ).freeze,
"datacite" => %w( creator title publisher publicationyear resourcetype ).freeze,
"erc" => %w( who what when ).freeze,
"crossref" => [].freeze
}.freeze

# EZID reserved metadata elements that have time values
# @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata
RESERVED_TIME_ELEMENTS = %w( _created _updated )

# EZID reserved metadata elements that are read-only
# @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata
RESERVED_READONLY_ELEMENTS = %w( _owner _ownergroup _shadows _shadowedby _datacenter _created _updated )

# EZID reserved metadata elements that may be set by clients
# @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata
RESERVED_READWRITE_ELEMENTS = %w( _coowners _target _profile _status _export _crossref )

# All EZID reserved metadata elements
# @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata
RESERVED_ELEMENTS = RESERVED_READONLY_ELEMENTS + RESERVED_READWRITE_ELEMENTS

def self.initialize!
register_elements
end

def self.registered_elements
@@registered_elements ||= {}
end

def self.register_elements
register_profile_elements
register_reserved_elements
end

def self.register_element(accessor, opts={})
if element = registered_elements[accessor.to_sym]
raise Error, "Element \"#{element.name}\" is registered under the accessor :#{accessor}."
end
element = Element.new(opts.fetch(:name, accessor.to_s))
element.reader = define_reader(accessor, element.name)
element.writer = define_writer(accessor, element.name) if opts.fetch(:writer, true)
registered_elements[accessor.to_sym] = element
end

def self.unregister_element(accessor)
element = registered_elements.delete(accessor)
raise Error, "No element is registered under the accessor :#{accessor}." unless element
remove_method(element.reader)
remove_method(element.writer) if element.writer
end

def self.register_profile_element(profile, element)
register_element("#{profile}_#{element}", name: "#{profile}.#{element}")
end

def self.register_profile_elements(profile = nil)
if profile
PROFILES[profile].each { |element| register_profile_element(profile, element) }
else
PROFILES.keys.each do |profile|
register_profile_elements(profile)
register_element(profile) unless profile == "dc"
end
end
end
READONLY = %w( _owner _ownergroup _shadows _shadowedby _datacenter _created _updated )

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

metadata_reader :_created
metadata_reader :_datacenter, :datacenter
metadata_reader :_owner, :owner
metadata_reader :_ownergroup, :ownergroup
metadata_reader :_shadowedby, :shadowedby
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

def self.register_reserved_elements
RESERVED_ELEMENTS.each do |element|
accessor = (element == "_crossref") ? element : element.sub("_", "")
register_element(accessor, name: element, writer: RESERVED_READWRITE_ELEMENTS.include?(element))
end
def initialize(data={})
super coerce(data)
end

def self.define_reader(accessor, element)
define_method(accessor) do
reader(element)
end
def created
to_time _created
end

def self.define_writer(accessor, element)
define_method("#{accessor}=") do |value|
writer(element, value)
end
end

private_class_method :register_elements,
:register_reserved_elements,
:register_profile_elements,
:unregister_element,
:define_reader,
:define_writer

def initialize(data={})
super(coerce(data))
def updated
to_time _updated
end

# Output metadata in EZID ANVL format
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
# @return [String] the ANVL output
def to_anvl(include_readonly = true)
elements = __getobj__.dup # copy, don't modify!
elements.reject! { |k, v| RESERVED_READONLY_ELEMENTS.include?(k) } unless include_readonly
escape_elements(elements).map { |e| e.join(ANVL_SEPARATOR) }.join("\n")
hsh = __getobj__.dup
hsh.reject! { |k, v| READONLY.include?(k) } unless include_readonly
elements = 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)
end

def to_s
to_anvl
end

def registered_elements
self.class.registered_elements
end

private

def reader(element)
value = self[element]
if RESERVED_TIME_ELEMENTS.include?(element)
time = value.to_i
value = (time == 0) ? nil : Time.at(time).utc
end
value
end

def writer(element, value)
self[element] = value
end

# Coerce data into a Hash of elements
def coerce(data)
data.to_h
rescue NoMethodError
coerce_string(data)
end

# Escape elements hash keys and values
def escape_elements(hsh)
hsh.each_with_object({}) do |(n, v), memo|
memo[escape_name(n)] = escape_value(v)
end
end

# Escape an element name
def escape_name(n)
escape(ESCAPE_NAMES_RE, n)
end
def to_time(value)
time = value.to_i
(time == 0) ? nil : Time.at(time).utc
end

# Escape an element value
def escape_value(v)
escape(ESCAPE_VALUES_RE, v)
end
# Coerce data into a Hash of elements
def coerce(data)
data.to_h
rescue NoMethodError
coerce_string(data)
end

# Escape string for sending to EZID host
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
# @param re [Regexp] the regular expression to match for escaping
# @param s [String] the string to escape
# @return [String] the escaped string
def escape(re, s)
s.gsub(re) { |m| URI.encode_www_form_component(m.force_encoding(Encoding::UTF_8)) }
end
# Escape string for sending to EZID host
def escape(regexp, value)
value.gsub(regexp) { |m| URI.encode_www_form_component(m.force_encoding(Encoding::UTF_8)) }
end

# Unescape value from EZID host (or other source)
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
# @param value [String] the value to unescape
# @return [String] the unescaped value
def unescape(value)
value.gsub(UNESCAPE_RE) { |m| URI.decode_www_form_component(m) }
end
# Unescape value from EZID host (or other source)
def unescape(value)
value.gsub(UNESCAPE_RE) { |m| URI.decode_www_form_component(m) }
end

# Coerce a string of metadata (e.g., from EZID host) into a Hash
# @note EZID host does not send comments or line continuations.
# @param data [String] the string to coerce
# @return [Hash] the hash of coerced data
def coerce_string(data)
data.gsub!(COMMENT_RE, "")
data.gsub!(LINE_CONTINUATION_RE, " ")
data.split(LINE_ENDING_RE).each_with_object({}) do |line, memo|
element, value = line.split(ANVL_SEPARATOR, 2)
memo[unescape(element.strip)] = unescape(value.strip)
end
# Coerce a string of metadata (e.g., from EZID host) into a Hash
# @note EZID host does not send comments or line continuations.
def coerce_string(data)
data.gsub!(COMMENT_RE, "")
data.gsub!(LINE_CONTINUATION_RE, " ")
data.split(LINE_ENDING_RE).each_with_object({}) do |line, memo|
element, value = line.split(ANVL_SEPARATOR, 2)
memo[unescape(element.strip)] = unescape(value.strip)
end
end

end
end

Ezid::Metadata.initialize!
6 changes: 0 additions & 6 deletions lib/ezid/requests/batch_download_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,8 @@ class BatchDownloadRequest < Request
self.path = "/download_request"
self.response_class = BatchDownloadResponse

attr_reader :params

def initialize(client, params={})
@params = params
super
end

def customize_request
set_form_data(params)
end

Expand Down
6 changes: 1 addition & 5 deletions lib/ezid/requests/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def short_name
def initialize(client, *args)
@client = client
super build_request
customize_request
set_content_type("text/plain", charset: "UTF-8")
end

# Executes the request and returns the response
Expand Down Expand Up @@ -97,10 +97,6 @@ def get_response_for_request
end
end

def customize_request
set_content_type("text/plain", charset: "UTF-8")
end

def build_request
self.class.http_method.new(uri)
end
Expand Down
6 changes: 3 additions & 3 deletions spec/unit/identifier_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ module Ezid
allow(subject).to receive(:persisted?) { true }
end
it "should modify the identifier" do
expect(subject.client).to receive(:modify_identifier).with("id", {}) { double(id: "id") }
expect(subject.client).to receive(:modify_identifier).with("id", subject.metadata) { double(id: "id") }
subject.save
end
end
Expand All @@ -161,15 +161,15 @@ module Ezid
context "and `id' is present" do
before { allow(subject).to receive(:id) { "id" } }
it "should create the identifier" do
expect(subject.client).to receive(:create_identifier).with("id", {}) { double(id: "id") }
expect(subject.client).to receive(:create_identifier).with("id", subject.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 "should mint the identifier" do
expect(subject.client).to receive(:mint_identifier).with(TEST_ARK_SHOULDER, {}) { double(id: "id") }
expect(subject.client).to receive(:mint_identifier).with(TEST_ARK_SHOULDER, subject.metadata) { double(id: "id") }
subject.save
end
end
Expand Down
Loading

0 comments on commit ad5cfd2

Please sign in to comment.