ezid-client 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.travis.yml +10 -0
- data/README.md +38 -59
- data/Rakefile +0 -7
- data/VERSION +1 -1
- data/ezid-client.gemspec +4 -2
- data/lib/ezid/client.rb +70 -100
- data/lib/ezid/configuration.rb +27 -12
- data/lib/ezid/identifier.rb +170 -0
- data/lib/ezid/metadata.rb +52 -94
- data/lib/ezid/metadata_elements.rb +89 -0
- data/lib/ezid/request.rb +20 -34
- data/lib/ezid/response.rb +40 -26
- data/lib/ezid/session.rb +2 -8
- data/lib/ezid/status.rb +23 -0
- data/spec/lib/ezid/client_spec.rb +136 -45
- data/spec/lib/ezid/identifier_spec.rb +203 -0
- data/spec/lib/ezid/metadata_spec.rb +200 -41
- data/spec/spec_helper.rb +7 -10
- metadata +32 -43
- data/lib/ezid/api.rb +0 -67
- data/lib/ezid/logger.rb +0 -31
- data/lib/ezid/test_helper.rb +0 -22
@@ -0,0 +1,170 @@
|
|
1
|
+
module Ezid
|
2
|
+
class Identifier
|
3
|
+
|
4
|
+
attr_reader :id, :client
|
5
|
+
attr_accessor :shoulder, :metadata
|
6
|
+
|
7
|
+
INSPECT_ATTRS = %w( id status target created )
|
8
|
+
|
9
|
+
PUBLIC = "public"
|
10
|
+
RESERVED = "reserved"
|
11
|
+
UNAVAILABLE = "unavailable"
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# @return [Ezid::Identifier] the new identifier
|
15
|
+
# @raise [Ezid::Error]
|
16
|
+
def create(attrs = {})
|
17
|
+
identifier = new(attrs)
|
18
|
+
identifier.save
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Ezid::Identifier] the identifier
|
22
|
+
# @raise [Ezid::Error] if the identifier does not exist in EZID
|
23
|
+
def find(id)
|
24
|
+
identifier = new(id: id)
|
25
|
+
identifier.reload
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(args={})
|
30
|
+
@client = args.delete(:client) || Client.new
|
31
|
+
@id = args.delete(:id)
|
32
|
+
@shoulder = args.delete(:shoulder)
|
33
|
+
@metadata = Metadata.new(args.delete(:metadata))
|
34
|
+
update_attributes(args)
|
35
|
+
@deleted = false
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
attrs = if deleted?
|
40
|
+
"id=\"#{id}\" DELETED"
|
41
|
+
else
|
42
|
+
INSPECT_ATTRS.map { |attr| "#{attr}=\"#{send(attr)}\"" }.join(" ")
|
43
|
+
end
|
44
|
+
"#<#{self.class.name} #{attrs}>"
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
id
|
49
|
+
end
|
50
|
+
|
51
|
+
# Persist the identifer and/or metadata to EZID.
|
52
|
+
# If the identifier is already persisted, this is an update operation;
|
53
|
+
# Otherwise, create (if it has an id) or mint (if it has a shoulder)
|
54
|
+
# the identifier.
|
55
|
+
# @return [Ezid::Identifier] the identifier
|
56
|
+
# @raise [Ezid::Error] if the identifier is deleted, or the host responds
|
57
|
+
# with an error status.
|
58
|
+
def save
|
59
|
+
raise Error, "Cannot save a deleted identifier." if deleted?
|
60
|
+
if persisted?
|
61
|
+
modify
|
62
|
+
else
|
63
|
+
create_or_mint
|
64
|
+
end
|
65
|
+
reload
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_attributes(attrs={})
|
69
|
+
attrs.each { |k, v| send("#{k}=", v) }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Is the identifier persisted?
|
73
|
+
# @return [Boolean]
|
74
|
+
def persisted?
|
75
|
+
return false if deleted?
|
76
|
+
!!(id && created)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Has the identifier been deleted?
|
80
|
+
# @return [Boolean]
|
81
|
+
def deleted?
|
82
|
+
@deleted
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [Ezid::Identifier] the identifier
|
86
|
+
# @raise [Ezid::Error]
|
87
|
+
def update(data)
|
88
|
+
metadata.update(data)
|
89
|
+
save
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Ezid::Identifier] the identifier
|
93
|
+
# @raise [Ezid::Error]
|
94
|
+
def reload
|
95
|
+
refresh_metadata
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
# Empties the (local) metadata
|
100
|
+
# @return [Ezid::Identifier] the identifier
|
101
|
+
def reset
|
102
|
+
clear_metadata
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [Ezid::Identifier] the identifier
|
107
|
+
# @raise [Ezid::Error]
|
108
|
+
def delete
|
109
|
+
raise Error, "Status must be \"reserved\" to delete (status: \"#{status}\")." unless reserved?
|
110
|
+
client.delete_identifier(id)
|
111
|
+
@deleted = true
|
112
|
+
reset
|
113
|
+
end
|
114
|
+
|
115
|
+
def method_missing(name, *args)
|
116
|
+
return metadata.send(name, *args) if metadata.respond_to?(name)
|
117
|
+
super
|
118
|
+
end
|
119
|
+
|
120
|
+
def reserved?
|
121
|
+
status == RESERVED
|
122
|
+
end
|
123
|
+
|
124
|
+
def public?
|
125
|
+
status == PUBLIC
|
126
|
+
end
|
127
|
+
|
128
|
+
def unavailable?
|
129
|
+
status == UNAVAILABLE
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def refresh_metadata
|
135
|
+
response = client.get_identifier_metadata(id)
|
136
|
+
@metadata.replace(response.metadata)
|
137
|
+
end
|
138
|
+
|
139
|
+
def clear_metadata
|
140
|
+
@metadata.clear
|
141
|
+
end
|
142
|
+
|
143
|
+
def modify
|
144
|
+
client.modify_identifier(id, metadata)
|
145
|
+
end
|
146
|
+
|
147
|
+
def create_or_mint
|
148
|
+
if id
|
149
|
+
create
|
150
|
+
elsif shoulder
|
151
|
+
mint
|
152
|
+
else
|
153
|
+
raise Error, "Unable to create or mint identifier when neither `id' nor `shoulder' present."
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def mint
|
158
|
+
response = client.mint_identifier(shoulder, metadata)
|
159
|
+
@id = response.id
|
160
|
+
end
|
161
|
+
|
162
|
+
def create
|
163
|
+
client.create_identifier(id, metadata)
|
164
|
+
end
|
165
|
+
|
166
|
+
def init_metadata(args={})
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
data/lib/ezid/metadata.rb
CHANGED
@@ -1,52 +1,31 @@
|
|
1
|
-
require "
|
1
|
+
require "delegate"
|
2
|
+
require_relative "metadata_elements"
|
2
3
|
|
3
4
|
module Ezid
|
4
5
|
#
|
5
6
|
# EZID metadata collection for an identifier
|
6
7
|
#
|
7
8
|
# @api public
|
8
|
-
|
9
|
-
|
10
|
-
include Enumerable
|
11
|
-
|
12
|
-
# The metadata elements hash
|
13
|
-
attr_reader :elements
|
14
|
-
|
15
|
-
def_delegators :elements, :each, :keys, :values, :empty?, :[], :[]=
|
16
|
-
|
17
|
-
# EZID metadata profiles
|
18
|
-
PROFILES = %w( erc dc datacite crossref )
|
19
|
-
|
20
|
-
# Public status
|
21
|
-
PUBLIC = "public"
|
22
|
-
|
23
|
-
# Reserved status
|
24
|
-
RESERVED = "reserved"
|
25
|
-
|
26
|
-
# Unavailable status
|
27
|
-
UNAVAILABLE = "unavailable"
|
28
|
-
|
29
|
-
# EZID identifier status values
|
30
|
-
STATUS_VALUES = [PUBLIC, RESERVED, UNAVAILABLE].freeze
|
31
|
-
|
32
|
-
# EZID internal read-only metadata elements
|
33
|
-
INTERNAL_READONLY_ELEMENTS = %w( _owner _ownergroup _created _updated _shadows _shadowedby _datacenter ).freeze
|
9
|
+
#
|
10
|
+
class Metadata < SimpleDelegator
|
34
11
|
|
35
|
-
|
36
|
-
INTERNAL_READWRITE_ELEMENTS = %w( _coowners _target _profile _status _export _crossref ).freeze
|
12
|
+
include MetadataElements
|
37
13
|
|
38
|
-
# EZID internal metadata elements
|
39
|
-
INTERNAL_ELEMENTS = (INTERNAL_READONLY_ELEMENTS + INTERNAL_READWRITE_ELEMENTS).freeze
|
40
|
-
|
41
14
|
# EZID metadata field/value separator
|
42
15
|
ANVL_SEPARATOR = ": "
|
43
16
|
|
17
|
+
ELEMENT_VALUE_SEPARATOR = " | "
|
18
|
+
|
44
19
|
# Characters to escape in element values on output to EZID
|
20
|
+
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
|
45
21
|
ESCAPE_VALUES_RE = /[%\r\n]/
|
46
22
|
|
47
|
-
|
23
|
+
# Characters to escape in element names on output to EZID
|
24
|
+
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
|
25
|
+
ESCAPE_NAMES_RE = /[%:\r\n]/
|
48
26
|
|
49
27
|
# Character sequence to unescape from EZID
|
28
|
+
# http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
|
50
29
|
UNESCAPE_RE = /%\h\h/
|
51
30
|
|
52
31
|
# A comment line
|
@@ -57,16 +36,18 @@ module Ezid
|
|
57
36
|
|
58
37
|
# A line ending
|
59
38
|
LINE_ENDING_RE = /\r?\n/
|
60
|
-
|
39
|
+
|
61
40
|
def initialize(data={})
|
62
|
-
|
41
|
+
super(coerce(data))
|
63
42
|
end
|
64
43
|
|
65
44
|
# Output metadata in EZID ANVL format
|
66
45
|
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
|
67
46
|
# @return [String] the ANVL output
|
68
|
-
def to_anvl
|
69
|
-
|
47
|
+
def to_anvl(include_readonly = true)
|
48
|
+
elements = __getobj__.dup # copy, don't modify!
|
49
|
+
elements.reject! { |k, v| RESERVED_READONLY_ELEMENTS.include?(k) } unless include_readonly
|
50
|
+
escape_elements(elements).map { |e| e.join(ANVL_SEPARATOR) }.join("\n")
|
70
51
|
end
|
71
52
|
|
72
53
|
def to_s
|
@@ -77,55 +58,20 @@ module Ezid
|
|
77
58
|
# @param data [String, Hash, Ezid::Metadata] the data to add
|
78
59
|
# @return [Ezid::Metadata] the updated metadata
|
79
60
|
def update(data)
|
80
|
-
|
61
|
+
__getobj__.update(coerce(data))
|
81
62
|
self
|
82
63
|
end
|
83
64
|
|
84
|
-
#
|
85
|
-
# @
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# The time the identifier was created
|
91
|
-
# @return [Time] the time
|
92
|
-
def created
|
93
|
-
value = reader("_created")
|
94
|
-
return Time.at(value.to_i) if value
|
95
|
-
value
|
96
|
-
end
|
97
|
-
|
98
|
-
# The time the identifier was last updated
|
99
|
-
# @return [Time] the time
|
100
|
-
def updated
|
101
|
-
value = reader("_updated")
|
102
|
-
return Time.at(value.to_i) if value
|
103
|
-
value
|
104
|
-
end
|
105
|
-
|
106
|
-
# The identifier's preferred metadata profile
|
107
|
-
# @see http://ezid.cdlib.org/doc/apidoc.html#metadata-profiles
|
108
|
-
# @return [String] the profile
|
109
|
-
def profile
|
110
|
-
reader("_profile")
|
111
|
-
end
|
112
|
-
|
113
|
-
# The identifier's target URL
|
114
|
-
# @return [String] the URL
|
115
|
-
def target
|
116
|
-
reader("_target")
|
65
|
+
# Replaces the collection with new metadata
|
66
|
+
# @param data [String, Hash, Ezid::Metadata] the metadata replacing the current metadata
|
67
|
+
# @return [Ezid::Metadata] the replaced metadata
|
68
|
+
def replace(data)
|
69
|
+
__getobj__.replace(coerce(data))
|
70
|
+
self
|
117
71
|
end
|
118
72
|
|
119
73
|
private
|
120
74
|
|
121
|
-
def reader(element)
|
122
|
-
self[element]
|
123
|
-
end
|
124
|
-
|
125
|
-
def writer(element, value)
|
126
|
-
self[element] = value
|
127
|
-
end
|
128
|
-
|
129
75
|
# Coerce data into a Hash of elements
|
130
76
|
def coerce(data)
|
131
77
|
begin
|
@@ -135,25 +81,35 @@ module Ezid
|
|
135
81
|
end
|
136
82
|
end
|
137
83
|
|
84
|
+
# Coerce hash keys to strings
|
138
85
|
def stringify_keys(hsh)
|
139
|
-
hsh.
|
86
|
+
hsh.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v }
|
140
87
|
end
|
141
88
|
|
142
|
-
|
143
|
-
|
89
|
+
# Escape elements hash keys and values
|
90
|
+
def escape_elements(hsh)
|
91
|
+
hsh.each_with_object({}) do |(n, v), memo|
|
92
|
+
memo[escape_name(n)] = escape_value(v)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Escape an element name
|
97
|
+
def escape_name(n)
|
98
|
+
escape(ESCAPE_NAMES_RE, n)
|
144
99
|
end
|
145
100
|
|
146
|
-
|
147
|
-
|
101
|
+
# Escape an element value
|
102
|
+
def escape_value(v)
|
103
|
+
escape(ESCAPE_VALUES_RE, v)
|
148
104
|
end
|
149
105
|
|
150
|
-
# Escape
|
106
|
+
# Escape string for sending to EZID host
|
151
107
|
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
|
152
108
|
# @param re [Regexp] the regular expression to match for escaping
|
153
|
-
# @param
|
154
|
-
# @return [String] the escaped
|
155
|
-
def escape(re,
|
156
|
-
|
109
|
+
# @param s [String] the string to escape
|
110
|
+
# @return [String] the escaped string
|
111
|
+
def escape(re, s)
|
112
|
+
s.gsub(re) { |m| URI.encode_www_form_component(m.force_encoding(Encoding::UTF_8)) }
|
157
113
|
end
|
158
114
|
|
159
115
|
# Unescape value from EZID host (or other source)
|
@@ -165,14 +121,16 @@ module Ezid
|
|
165
121
|
end
|
166
122
|
|
167
123
|
# Coerce a string of metadata (e.g., from EZID host) into a Hash
|
124
|
+
# @note EZID host does not send comments or line continuations.
|
168
125
|
# @param data [String] the string to coerce
|
169
126
|
# @return [Hash] the hash of coerced data
|
170
127
|
def coerce_string(data)
|
171
|
-
data.gsub(COMMENT_RE, "")
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
.
|
128
|
+
data.gsub!(COMMENT_RE, "")
|
129
|
+
data.gsub!(LINE_CONTINUATION_RE, " ")
|
130
|
+
data.split(LINE_ENDING_RE).each_with_object({}) do |line, memo|
|
131
|
+
element, value = line.split(ANVL_SEPARATOR, 2)
|
132
|
+
memo[unescape(element.strip)] = unescape(value.strip)
|
133
|
+
end
|
176
134
|
end
|
177
135
|
|
178
136
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "active_support"
|
2
|
+
|
3
|
+
module Ezid
|
4
|
+
module MetadataElements
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
DC_ELEMENTS = %w( creator title publisher date type )
|
8
|
+
DATACITE_ELEMENTS = %w( creator title publisher publicationyear resourcetype )
|
9
|
+
ERC_ELEMENTS = %w( who what when )
|
10
|
+
|
11
|
+
RESERVED_TIME_ELEMENTS = %w( _created _updated )
|
12
|
+
RESERVED_READONLY_ELEMENTS = %w( _owner _ownergroup _shadows _shadowedby _datacenter _created _updated )
|
13
|
+
RESERVED_READWRITE_ELEMENTS = %w( _coowners _target _profile _status _export _crossref )
|
14
|
+
RESERVED_ELEMENTS = RESERVED_READONLY_ELEMENTS + RESERVED_READWRITE_ELEMENTS + RESERVED_TIME_ELEMENTS
|
15
|
+
|
16
|
+
included do
|
17
|
+
reserved_accessor *(RESERVED_READWRITE_ELEMENTS - ["_crossref"])
|
18
|
+
reserved_reader *(RESERVED_READONLY_ELEMENTS - RESERVED_TIME_ELEMENTS)
|
19
|
+
reserved_time_reader *RESERVED_TIME_ELEMENTS
|
20
|
+
|
21
|
+
profile_accessor :dc, *DC_ELEMENTS
|
22
|
+
profile_accessor :datacite, *DATACITE_ELEMENTS
|
23
|
+
profile_accessor :erc, *ERC_ELEMENTS
|
24
|
+
|
25
|
+
element_accessor "datacite", "crossref", "_crossref"
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def reserved_accessor(*elements)
|
30
|
+
reserved_reader(*elements)
|
31
|
+
reserved_writer(*elements)
|
32
|
+
end
|
33
|
+
|
34
|
+
def reserved_reader(*elements)
|
35
|
+
elements.each do |element|
|
36
|
+
define_method(element.sub("_", "")) { reader(element) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def reserved_time_reader(*elements)
|
41
|
+
elements.each do |element|
|
42
|
+
define_method(element.sub("_", "")) do
|
43
|
+
time = reader(element).to_i
|
44
|
+
return nil if time == 0 # value is nil or empty string
|
45
|
+
Time.at(time).utc
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def reserved_writer(*elements)
|
51
|
+
elements.each do |element|
|
52
|
+
define_method("#{element.sub('_', '')}=") do |value|
|
53
|
+
writer(element, value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def profile_accessor(profile, *elements)
|
59
|
+
elements.each do |element|
|
60
|
+
define_method("#{profile}_#{element}") do
|
61
|
+
reader("#{profile}.#{element}")
|
62
|
+
end
|
63
|
+
|
64
|
+
define_method("#{profile}_#{element}=") do |value|
|
65
|
+
writer("#{profile}.#{element}", value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def element_accessor(*elements)
|
71
|
+
elements.each do |element|
|
72
|
+
define_method(element) { reader(element) }
|
73
|
+
define_method("#{element}=") { |value| writer(element, value) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def reader(element)
|
81
|
+
self[element]
|
82
|
+
end
|
83
|
+
|
84
|
+
def writer(element, value)
|
85
|
+
self[element] = value
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|