ezid-client 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 034acbeee70fe9fe84f4e8b26c90e30cb158501c
4
- data.tar.gz: 57a075ccf73a3c29e2fe2945acb4a9675a8c3224
3
+ metadata.gz: 1a8c3c449a9447e7176421f705a537c9919442a9
4
+ data.tar.gz: 9d16f67d27605bc7066f6dd3df2e9a9be5dc4e26
5
5
  SHA512:
6
- metadata.gz: 11a3a93987cccc2c665abf3a0cd1f822980186ce0547aae500f5cd509ccdf5868b9fc41bcef16215a6fac4c0ef1f09e0eddd32f8baf11a5d347fc66154df3d09
7
- data.tar.gz: b5193f91303db313b4a385d2c557d19b2ee97290acfa08ec912c4201d8164504a62d9f962a4a6d67d194ebfbadd81b27a04659dbae139c8327b4271f7be1e99a
6
+ metadata.gz: 94f6cf47f52bb030e0f7cf4442ef5aedbcb31f9348ebb1e528a11e9727b2a3e94260002ff43dd2fb1240fdeadbc3b2adde341fa83e5ad6b28f44e9e0f982eedd
7
+ data.tar.gz: 4a70172958a729e05c2b5ba2d5a51081e5a4865135c9107111045a9993f9e5881f10d3ca50d0b180d2f1b73cc383913955111beeac1660da9f6635f17754ef01
data/README.md CHANGED
@@ -113,7 +113,43 @@ I, [2014-12-04T15:12:48.853964 #86734] INFO -- : EZID DELETE ark:/99999/fk4n58p
113
113
 
114
114
  ## Metadata handling
115
115
 
116
- Although "EZID imposes no requirements on the presence or form of citation metadata"[*](http://ezid.cdlib.org/doc/apidoc.html#metadata-requirements-mapping), `ezid-client` is intended to support the EZID [reserved metadata elements](http://ezid.cdlib.org/doc/apidoc.html#internal-metadata) and [metadata profiles](http://ezid.cdlib.org/doc/apidoc.html#metadata-profiles). While it is possible to use the client to send and receive any metadata, the object methods are geared towards the defined elements. Therefore it was seen fit, for example, to map the method `Ezid::Identifier#status` to the `_status` element. Likewise, all the reserved elements, except for `_crossref`, have readers and -- for user-writable elements -- writers without the leading underscores. Since there are both `_crossref` and `crossref` elements, their accessors match the elements names. Similarly, accessors for metadata profile elements use underscores in place of dots -- for example, `Ezid::Identifer#dc_title` and `#dc_title=` for the `dc.title` element.
116
+ In order to ease metadata management access to EZID [reserved metadata elements](http://ezid.cdlib.org/doc/apidoc.html#internal-metadata) and [metadata profiles](http://ezid.cdlib.org/doc/apidoc.html#metadata-profiles) is provided through `#method_missing` according to these heuristics:
117
+
118
+ **Reserved elements** can be read and written using the name of the element without the leading underscore:
119
+
120
+ ```ruby
121
+ >> identifier.status # reads "_status" element
122
+ => "public"
123
+ >> identifier.status = "unavailable" # writes "_status" element
124
+ => "unavailable"
125
+ ```
126
+
127
+ Notes:
128
+ - `_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.
129
+ - Reserved elements which are not user-writeable do not implement writers.
130
+ - 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.
131
+
132
+ **Metadata profile elements** can be read and written using the name of the element, replacing the dot (".") with an underscore:
133
+
134
+ ```ruby
135
+ >> identifier.dc_type # reads "dc.type" element
136
+ => "Collection"
137
+ >> identifier.dc_type = "Image" # writes "dc.type" element
138
+ => "Image"
139
+ ```
140
+
141
+ **Registering custom metadata elements**
142
+
143
+ Custom metadata element accessors can be created by a registration process:
144
+
145
+ ```ruby
146
+ Ezid::Client.configure do |config|
147
+ # register the element "custom"
148
+ config.metadata.register_element :custom
149
+ # register the element "dc.identifier" under the accessor :dc_identifier
150
+ config.metadata.register_element :dc_identifier, name: "dc.identifier"
151
+ end
152
+ ```
117
153
 
118
154
  **Setting default metadata values**
119
155
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.1
1
+ 0.10.0
@@ -54,5 +54,9 @@ module Ezid
54
54
  Identifier
55
55
  end
56
56
 
57
+ def metadata
58
+ Metadata
59
+ end
60
+
57
61
  end
58
62
  end
@@ -1,20 +1,16 @@
1
- require "forwardable"
2
-
3
1
  module Ezid
4
2
  #
5
3
  # Represents an EZID identifier as a resource.
6
4
  #
5
+ # Ezid::Identifier delegates access to registered metadata elements through #method_missing.
6
+ #
7
7
  # @api public
8
8
  #
9
9
  class Identifier
10
- extend Forwardable
11
10
 
12
11
  attr_reader :id, :client
13
12
  attr_accessor :shoulder, :metadata
14
13
 
15
- def_delegators :metadata, *(Metadata.elements.readers)
16
- def_delegators :metadata, *(Metadata.elements.writers)
17
-
18
14
  # Attributes to display on inspect
19
15
  INSPECT_ATTRS = %w( id status target created )
20
16
 
@@ -126,10 +122,11 @@ module Ezid
126
122
  end
127
123
 
128
124
  # Deletes the identifier from EZID
125
+ # @see http://ezid.cdlib.org/doc/apidoc.html#operation-delete-identifier
129
126
  # @return [Ezid::Identifier] the identifier
130
127
  # @raise [Ezid::Error]
131
128
  def delete
132
- raise Error, "Status must be \"reserved\" to delete (status: \"#{status}\")." unless reserved?
129
+ raise Error, "Only persisted, reserved identifiers may be deleted: #{inspect}." unless deletable?
133
130
  client.delete_identifier(id)
134
131
  @deleted = true
135
132
  reset
@@ -150,40 +147,71 @@ module Ezid
150
147
  # Is the identifier unavailable?
151
148
  # @return [Boolean]
152
149
  def unavailable?
153
- status == UNAVAILABLE
150
+ status =~ /^#{UNAVAILABLE}/
154
151
  end
155
152
 
156
- private
157
-
158
- def refresh_metadata
159
- response = client.get_identifier_metadata(id)
160
- @metadata = Metadata.new(response.metadata)
153
+ # Is the identifier deletable?
154
+ # @return [Boolean]
155
+ def deletable?
156
+ persisted? && reserved?
161
157
  end
162
158
 
163
- def clear_metadata
164
- @metadata.clear
159
+ # Mark the identifier as unavailable
160
+ # @param reason [String] an optional reason
161
+ # @return [String] the new status
162
+ def unavailable!(reason = nil)
163
+ raise Error, "Cannot make a reserved identifier unavailable." if persisted? && reserved?
164
+ value = UNAVAILABLE
165
+ value << " | #{reason}" if reason
166
+ self.status = value
165
167
  end
166
168
 
167
- def modify
168
- client.modify_identifier(id, metadata)
169
+ # Mark the identifier as public
170
+ # @return [String] the new status
171
+ def public!
172
+ self.status = PUBLIC
169
173
  end
170
174
 
171
- def create_or_mint
172
- id ? create : mint
173
- end
175
+ protected
174
176
 
175
- def mint
176
- response = client.mint_identifier(shoulder, metadata)
177
- @id = response.id
178
- end
177
+ def method_missing(method, *args)
178
+ metadata.send(method, *args)
179
+ rescue NoMethodError
180
+ super
181
+ end
179
182
 
180
- def create
181
- client.create_identifier(id, metadata)
182
- end
183
+ private
184
+
185
+ def refresh_metadata
186
+ response = client.get_identifier_metadata(id)
187
+ @metadata = Metadata.new(response.metadata)
188
+ end
189
+
190
+ def clear_metadata
191
+ @metadata.clear
192
+ end
193
+
194
+ def modify
195
+ client.modify_identifier(id, metadata)
196
+ end
197
+
198
+ def create_or_mint
199
+ id ? create : mint
200
+ end
201
+
202
+ def mint
203
+ response = client.mint_identifier(shoulder, metadata)
204
+ @id = response.id
205
+ end
206
+
207
+ def create
208
+ client.create_identifier(id, metadata)
209
+ end
210
+
211
+ def init_metadata(args)
212
+ @metadata = Metadata.new(args.delete(:metadata))
213
+ update_metadata(self.class.defaults.merge(args))
214
+ end
183
215
 
184
- def init_metadata(args)
185
- @metadata = Metadata.new(args.delete(:metadata))
186
- update_metadata(self.class.defaults.merge(args))
187
- end
188
216
  end
189
217
  end
data/lib/ezid/metadata.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  require "delegate"
2
- require "singleton"
3
2
 
4
3
  module Ezid
5
4
  #
6
- # EZID metadata collection for an identifier
5
+ # EZID metadata collection for an identifier.
7
6
  #
8
7
  # @note Although this API is not private, its direct use is discouraged.
9
8
  # Instead use the metadata element accessors through Ezid::Identifier.
@@ -64,71 +63,53 @@ module Ezid
64
63
  # @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata
65
64
  RESERVED_ELEMENTS = RESERVED_READONLY_ELEMENTS + RESERVED_READWRITE_ELEMENTS
66
65
 
67
- # Metadata element registry
68
- class ElementRegistry < SimpleDelegator
69
- include Singleton
70
-
71
- def initialize
72
- super(Hash.new)
73
- end
74
-
75
- def readers
76
- keys
77
- end
78
-
79
- def writers
80
- keys.select { |k| self[k].writer }.map(&:to_s).map { |k| k.concat("=") }.map(&:to_sym)
81
- end
82
- end
83
-
84
66
  def self.initialize!
85
67
  register_elements
86
- define_element_accessors
87
68
  end
88
69
 
89
- def self.elements
90
- ElementRegistry.instance
70
+ def self.registered_elements
71
+ @@registered_elements ||= {}
91
72
  end
92
73
 
93
74
  def self.register_elements
94
75
  register_profile_elements
95
76
  register_reserved_elements
96
- elements.freeze
97
77
  end
98
78
 
99
- def self.define_element_accessors
100
- elements.each do |accessor, element|
101
- define_method(accessor) { reader(element.name) }
102
-
103
- if element.writer
104
- define_method("#{accessor}=") { |value| writer(element.name, value) }
105
- end
79
+ def self.register_element(accessor, opts={})
80
+ if element = registered_elements[accessor.to_sym]
81
+ raise Error, "Element \"#{element.name}\" already registered under key :#{accessor}"
106
82
  end
83
+ writer = opts.fetch(:writer, true)
84
+ name = opts.fetch(:name, accessor.to_s)
85
+ registered_elements[accessor.to_sym] = Element.new(name, writer).freeze
107
86
  end
108
87
 
109
- def self.register_element(accessor, element, opts={})
110
- writer = opts.fetch(:writer, true)
111
- elements[accessor] = Element.new(element, writer).freeze
88
+ def self.register_profile_element(profile, element)
89
+ register_element("#{profile}_#{element}", name: "#{profile}.#{element}")
112
90
  end
113
91
 
114
- def self.register_profile_elements
115
- PROFILES.each do |profile, profile_elements|
116
- profile_elements.each do |element|
117
- register_element("#{profile}_#{element}".to_sym, "#{profile}.#{element}")
92
+ def self.register_profile_elements(profile = nil)
93
+ if profile
94
+ PROFILES[profile].each { |element| register_profile_element(profile, element) }
95
+ else
96
+ PROFILES.keys.each do |profile|
97
+ register_profile_elements(profile)
98
+ register_element(profile) unless profile == "dc"
118
99
  end
119
- register_element(profile.to_sym, profile) unless profile == "dc"
120
100
  end
121
101
  end
122
102
 
123
103
  def self.register_reserved_elements
124
104
  RESERVED_ELEMENTS.each do |element|
125
- accessor = ((element == "_crossref") ? element : element.sub("_", "")).to_sym
126
- register_element(accessor, element, writer: RESERVED_READWRITE_ELEMENTS.include?(element))
105
+ accessor = (element == "_crossref") ? element : element.sub("_", "")
106
+ register_element(accessor, name: element, writer: RESERVED_READWRITE_ELEMENTS.include?(element))
127
107
  end
128
108
  end
129
109
 
130
- private_class_method :register_element, :register_elements, :register_reserved_elements,
131
- :register_profile_elements, :define_element_accessors
110
+ private_class_method :register_elements,
111
+ :register_reserved_elements,
112
+ :register_profile_elements
132
113
 
133
114
  def initialize(data={})
134
115
  super(coerce(data))
@@ -147,8 +128,28 @@ module Ezid
147
128
  to_anvl
148
129
  end
149
130
 
131
+ def registered_elements
132
+ self.class.registered_elements
133
+ end
134
+
135
+ protected
136
+
137
+ def method_missing(method, *args)
138
+ return registered_reader(method) if registered_reader?(method, *args)
139
+ return registered_writer(method, *args) if registered_writer?(method, *args)
140
+ super
141
+ end
142
+
150
143
  private
151
144
 
145
+ def registered_reader?(accessor, *args)
146
+ args.empty? && registered_elements.include?(accessor)
147
+ end
148
+
149
+ def registered_reader(accessor)
150
+ reader registered_elements[accessor].name
151
+ end
152
+
152
153
  def reader(element)
153
154
  value = self[element]
154
155
  if RESERVED_TIME_ELEMENTS.include?(element)
@@ -158,6 +159,17 @@ module Ezid
158
159
  value
159
160
  end
160
161
 
162
+ def registered_writer?(method, *args)
163
+ return false unless method.to_s.end_with?("=") && args.size == 1
164
+ accessor = method.to_s.sub("=", "").to_sym
165
+ registered_elements.include?(accessor) && registered_elements[accessor].writer
166
+ end
167
+
168
+ def registered_writer(method, *args)
169
+ accessor = method.to_s.sub("=", "").to_sym
170
+ writer(registered_elements[accessor].name, *args)
171
+ end
172
+
161
173
  def writer(element, value)
162
174
  self[element] = value
163
175
  end
@@ -108,7 +108,7 @@ module Ezid
108
108
  context "when id and `created' are present" do
109
109
  before do
110
110
  allow(subject).to receive(:id) { "ark:/99999/fk4fn19h88" }
111
- allow(subject.metadata).to receive(:created) { Time.at(1416507086) }
111
+ subject.metadata["_created"] = "1416507086"
112
112
  end
113
113
  it "should be true" do
114
114
  expect(subject).to be_persisted
@@ -117,11 +117,28 @@ module Ezid
117
117
  end
118
118
 
119
119
  describe "#delete" do
120
- subject { described_class.new(id: "id", status: "reserved") }
121
- it "should delete the identifier" do
122
- expect(subject.client).to receive(:delete_identifier).with("id") { double(id: "id") }
123
- subject.delete
124
- expect(subject).to be_deleted
120
+ context "when the identifier is reserved" do
121
+ subject { described_class.new(id: "id", status: Identifier::RESERVED) }
122
+ context "and is persisted" do
123
+ before { allow(subject).to receive(:persisted?) { true } }
124
+ it "should delete the identifier" do
125
+ expect(subject.client).to receive(:delete_identifier).with("id") { double(id: "id") }
126
+ subject.delete
127
+ expect(subject).to be_deleted
128
+ end
129
+ end
130
+ context "and is not persisted" do
131
+ before { allow(subject).to receive(:persisted?) { false } }
132
+ it "should raise an exception" do
133
+ expect { subject.delete }.to raise_error
134
+ end
135
+ end
136
+ end
137
+ context "when identifier is not reserved" do
138
+ subject { described_class.new(id: "id", status: Identifier::PUBLIC) }
139
+ it "should raise an exception" do
140
+ expect { subject.delete }.to raise_error
141
+ end
125
142
  end
126
143
  end
127
144
 
@@ -167,23 +184,70 @@ module Ezid
167
184
  end
168
185
 
169
186
  describe "boolean status methods" do
170
- context "when the status is 'public'" do
171
- before { allow(subject.metadata).to receive(:status) { Identifier::PUBLIC } }
187
+ context "when the identifier is public" do
188
+ before { subject.public! }
172
189
  it { is_expected.to be_public }
173
190
  it { is_expected.not_to be_reserved }
174
191
  it { is_expected.not_to be_unavailable }
175
192
  end
176
- context "when the status is 'reserved'" do
177
- before { allow(subject.metadata).to receive(:status) { Identifier::RESERVED } }
193
+ context "when the identifier is reserved" do
194
+ before { subject.status = Identifier::RESERVED }
178
195
  it { is_expected.not_to be_public }
179
196
  it { is_expected.to be_reserved }
180
197
  it { is_expected.not_to be_unavailable }
181
198
  end
182
- context "when the status is 'unavailable'" do
183
- before { allow(subject.metadata).to receive(:status) { Identifier::UNAVAILABLE } }
184
- it { is_expected.not_to be_public }
185
- it { is_expected.not_to be_reserved }
186
- it { is_expected.to be_unavailable }
199
+ context "when the identifier is unavailable" do
200
+ context "and it has no reason" do
201
+ before { subject.unavailable! }
202
+ it { is_expected.not_to be_public }
203
+ it { is_expected.not_to be_reserved }
204
+ it { is_expected.to be_unavailable }
205
+ end
206
+ context "and it has a reason" do
207
+ before { subject.unavailable!("withdrawn") }
208
+ it { is_expected.not_to be_public }
209
+ it { is_expected.not_to be_reserved }
210
+ it { is_expected.to be_unavailable }
211
+ end
212
+ end
213
+ end
214
+
215
+ describe "status-changing methods" do
216
+ describe "#unavailable!" do
217
+ context "when the identifier is reserved" do
218
+ subject { described_class.new(id: "id", status: Identifier::RESERVED) }
219
+ context "and persisted" do
220
+ before { allow(subject).to receive(:persisted?) { true } }
221
+ it "should raise an exception" do
222
+ expect { subject.unavailable! }.to raise_error
223
+ end
224
+ end
225
+ context "and not persisted" do
226
+ before { allow(subject).to receive(:persisted?) { false } }
227
+ it "should changed the status" do
228
+ expect { subject.unavailable! }.to change(subject, :status).from(Identifier::RESERVED).to(Identifier::UNAVAILABLE)
229
+ end
230
+ end
231
+ end
232
+ context "when the identifier is public" do
233
+ subject { described_class.new(id: "id", status: Identifier::PUBLIC) }
234
+ context "and no reason is given" do
235
+ it "should change the status" do
236
+ expect { subject.unavailable! }.to change(subject, :status).from(Identifier::PUBLIC).to(Identifier::UNAVAILABLE)
237
+ end
238
+ end
239
+ context "and a reason is given" do
240
+ it "should change the status and append the reason" do
241
+ expect { subject.unavailable!("withdrawn") }.to change(subject, :status).from(Identifier::PUBLIC).to("#{Identifier::UNAVAILABLE} | withdrawn")
242
+ end
243
+ end
244
+ end
245
+ end
246
+ describe "#public!" do
247
+ subject { described_class.new(id: "id", status: Identifier::UNAVAILABLE) }
248
+ it "should change the status" do
249
+ expect { subject.public! }.to change(subject, :status).from(Identifier::UNAVAILABLE).to(Identifier::PUBLIC)
250
+ end
187
251
  end
188
252
  end
189
253
 
@@ -33,6 +33,7 @@ module Ezid
33
33
  end
34
34
  end
35
35
  end
36
+
36
37
  describe "metadata profiles" do
37
38
  Metadata::PROFILES.each do |profile, elements|
38
39
  describe "the '#{profile}' metadata profile" do
@@ -65,6 +66,22 @@ module Ezid
65
66
  end
66
67
  end
67
68
 
69
+ describe "custom element" do
70
+ let(:element) { Metadata::Element.new("custom", true) }
71
+ before do
72
+ allow(subject.registered_elements).to receive(:include?).with(:custom) { true }
73
+ allow(subject.registered_elements).to receive(:[]).with(:custom) { element }
74
+ end
75
+ it "should have a reader" do
76
+ expect(subject).to receive(:reader).with("custom")
77
+ subject.custom
78
+ end
79
+ it "should have a writer" do
80
+ expect(subject).to receive(:writer).with("custom", "value")
81
+ subject.custom = "value"
82
+ end
83
+ end
84
+
68
85
  describe "ANVL output" do
69
86
  let(:elements) do
70
87
  { "_target" => "http://example.com/path%20with%20spaces",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ezid-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Chandek-Stark
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-18 00:00:00.000000000 Z
11
+ date: 2014-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler