ezid-client 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c103eaae29bd61679e4e349a5a14ab7964edd291
4
- data.tar.gz: 047f1df3708982722bcbad3fac050183cf6285a6
3
+ metadata.gz: 7c3a0edbc74bbd749bf0c099fa07cad69bfebcdb
4
+ data.tar.gz: 4fdea290531b0f68d4d652a17b800585a4514f34
5
5
  SHA512:
6
- metadata.gz: 53b9aa4362dbeea70ad165b6977756b63ac0ca6ad970fae98caa11ba135e83603f102b23de54b27577a38293f3701bf89712bdc60e7fd19409de0dc1f9a2ea73
7
- data.tar.gz: ac9b5fa24406b489ce716d6683d2d68c2d6c7e01d59da813e1848385f811ce3d9f4158e6e3b8649935c216624fb273742097bf639d181af7c8a5c04a828cff61
6
+ metadata.gz: 8eecfcf10284ab9c6ccbe0916547d704d75ee3b0e9b8d6da0ff4dcadbe35a3d698400f741483c5529f6ce1809caee462b812d76356231ac06732d3ebbac49f7f
7
+ data.tar.gz: 0c137cf93d57cff8186821ace6125ca390833ed8a07327d1cffb50e708a8dae374594e374fe3f2cd603e725295b8e0c367645cb3639c568ca230e48814f3b01d
data/README.md CHANGED
@@ -16,51 +16,109 @@ Or install it yourself as:
16
16
 
17
17
  $ gem install ezid-client
18
18
 
19
- ## Usage
19
+ ## Basic Usage
20
20
 
21
- Create a client
21
+ See `Ezid::Client` class for details.
22
22
 
23
- ```ruby
24
- >> client = Ezid::Client.new(user: "apitest", password: "********")
25
- => #<Ezid::Client:0x007f857c23ca40 @user="apitest", @password="********", @session=#<Ezid::Session:0x007f857c2515a8 @cookie="sessionid=quyclw5bbnwsay0qh05isalt86xj5o1l">>
23
+ **Create a client**
24
+
25
+ ```
26
+ >> client = Ezid::Client.new(user: "apitest")
27
+ => #<Ezid::Client:0x007f8ce651a890 , @user="apitest", @password="********">
26
28
  ```
27
29
 
28
- Mint an identifier
30
+ Initialize with a block (wraps in a session)
29
31
 
30
- ```ruby
31
- >> response = client.mint_identifier("ark:/99999/fk4")
32
- => #<Ezid::Response:0x007f857c488010 @http_response=#<Net::HTTPCreated 201 CREATED readbody=true>, @metadata=#<Ezid::Metadata:0x007f857c488448 @elements={}>, @result="success", @message="ark:/99999/fk4988cc8j">
33
- >> response.identifier
34
- => "ark:/99999/fk4988cc8j"
32
+ ```
33
+ >> Ezid::Client.new(user: "apitest") do |client|
34
+ ?> client.server_status("*")
35
+ >> end
36
+ I, [2014-11-20T13:23:23.120797 #86059] INFO -- : success: session cookie returned
37
+ I, [2014-11-20T13:23:25.336596 #86059] INFO -- : success: EZID is up
38
+ I, [2014-11-20T13:23:25.804790 #86059] INFO -- : success: authentication credentials flushed
39
+ => #<Ezid::Client:0x007faa5a6a9ee0 , @user="apitest", @password="********">
35
40
  ```
36
41
 
37
- Modify identifier metadata
42
+ **Login**
38
43
 
39
- ```ruby
40
- >> metadata = Ezid::Metadata.new("dc.type" => "Image")
41
- => #<Ezid::Metadata:0x007f857c251c88 @elements={"dc.type"=>"Image"}>
42
- >> response = client.modify_identifier("ark:/99999/fk4988cc8j", metadata)
43
- => #<Ezid::Response:0x007f857c53ab20 @http_response=#<Net::HTTPOK 200 OK readbody=true>, @metadata=#<Ezid::Metadata:0x007f857c53aa30 @elements={}>, @result="success", @message="ark:/99999/fk4988cc8j">
44
+ Note that login is not required to send authenticated requests; it merely establishes a session. See http://ezid.cdlib.org/doc/apidoc.html#authentication.
45
+
46
+ ```
47
+ >> client.login
48
+ I, [2014-11-20T13:10:50.958378 #85954] INFO -- : success: session cookie returned
49
+ => #<Ezid::Client:0x007f8ce651a890 LOGGED_IN, @user="apitest", @password="********">
44
50
  ```
45
51
 
46
- Get identifier metadata
52
+ **Mint an identifier**
47
53
 
48
54
  ```
49
- >> response = client.get_identifier_metadata("ark:/99999/fk4988cc8j")
50
- => #<Ezid::Response:0x007f857c50a060 @http_response=#<Net::HTTPOK 200 OK readbody=true>, @metadata=#<Ezid::Metadata:0x007f857c509f48 @elements={"_updated"=>"1416436386", "_target"=>"http://ezid.cdlib.org/id/ark:/99999/fk4988cc8j", "_profile"=>"erc", "dc.type"=>"Image", "_ownergroup"=>"apitest", "_owner"=>"apitest", "_export"=>"yes", "_created"=>"1416436287", "_status"=>"public"}>, @result="success", @message="ark:/99999/fk4988cc8j">
51
- >> response.metadata["dc.type"]
52
- => "Image"
55
+ >> response = client.mint_identifier("ark:/99999/fk4")
56
+ I, [2014-11-20T13:11:25.894128 #85954] INFO -- : success: ark:/99999/fk4fn19h87
57
+ => #<Net::HTTPCreated 201 CREATED readbody=true>
58
+ >> response.identifier
59
+ => "ark:/99999/fk4fn19h87"
60
+ ```
61
+
62
+ **Get identifier metadata**
63
+
64
+ ```
65
+ >> response = client.get_identifier_metadata(response.identifier)
66
+ I, [2014-11-20T13:12:08.700287 #85954] INFO -- : success: ark:/99999/fk4fn19h87
67
+ => #<Net::HTTPOK 200 OK readbody=true>
53
68
  >> puts response.metadata
54
- _updated: 1416436386
55
- _target: http://ezid.cdlib.org/id/ark:/99999/fk4988cc8j
69
+ _updated: 1416507086
70
+ _target: http://ezid.cdlib.org/id/ark:/99999/fk4fn19h87
56
71
  _profile: erc
57
- dc.type: Image
58
72
  _ownergroup: apitest
59
73
  _owner: apitest
60
74
  _export: yes
61
- _created: 1416436287
75
+ _created: 1416507086
62
76
  _status: public
77
+ => nil
78
+ ```
79
+
80
+ **Logout**
81
+
63
82
  ```
83
+ >> client.logout
84
+ I, [2014-11-20T13:18:47.213566 #86059] INFO -- : success: authentication credentials flushed
85
+ => #<Ezid::Client:0x007faa5a712350 , @user="apitest", @password="********">
86
+ ```
87
+
88
+ ## Resource-oriented Usage
89
+
90
+ Experimental -- see `Ezid::Identifier`.
91
+
92
+ ## Metadata handling
93
+
94
+ See `Ezid::Metadata`.
95
+
96
+ ## Authentication
97
+
98
+ Credentials can be provided in any -- or a combination -- of these ways:
99
+
100
+ - Environment variables `EZID_USER` and/or `EZID_PASSWORD`;
101
+
102
+ - Client configuration:
103
+
104
+ ```ruby
105
+ Ezid::Client.configure do |config|
106
+ config.user = "eziduser"
107
+ config.password = "ezidpass"
108
+ end
109
+ ```
110
+
111
+ - At client initialization:
112
+
113
+ ```ruby
114
+ client = Ezid::Client.new(user: "eziduser", password: "ezidpass")
115
+ ```
116
+
117
+ ## Running the tests
118
+
119
+ By default the tests authenticate as user "apitest"; the password is not provided -- see http://ezid.cdlib.org/doc/apidoc.html#testing-the-api.
120
+
121
+ The test suite uses [VCR](https://relishapp.com/vcr/vcr) and [WebMock](https://github.com/bblimke/webmock) to stub requests and responses after the first run. The VCR "cassettes" are written to `spec/cassettes` and may be cleared with the rake task `test:clean`.
64
122
 
65
123
  ## Contributing
66
124
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
data/lib/ezid/api.rb CHANGED
@@ -2,47 +2,62 @@ module Ezid
2
2
  #
3
3
  # EZID API Version 2 bindings
4
4
  #
5
+ # @api private
5
6
  module Api
6
7
 
7
8
  VERSION = "2"
8
9
 
9
10
  # EZID server subsystems
10
- DATACITE_SUBSYSTEM = "datacite"
11
- NOID_SUBSYSTEM = "noid"
12
- LDAP_SUBSYSTEM = "ldap"
13
- ALL_SUBSYSTEMS = "*"
11
+ # "*" = all subsystems
12
+ SUBSYSTEMS = %w( datacite noid ldap * )
14
13
 
15
14
  class << self
16
-
15
+
16
+ # Start a session
17
+ # @see http://ezid.cdlib.org/doc/apidoc.html#authentication
17
18
  def login
18
19
  [:Get, "/login"]
19
20
  end
20
21
 
22
+ # End the current session
23
+ # @see http://ezid.cdlib.org/doc/apidoc.html#authentication
21
24
  def logout
22
25
  [:Get, "/logout"]
23
26
  end
24
27
 
28
+ # Operation: mint identifier
29
+ # @see http://ezid.cdlib.org/doc/apidoc.html#operation-mint-identifier
25
30
  def mint_identifier(shoulder)
26
31
  [:Post, "/shoulder/#{shoulder}"]
27
32
  end
28
33
 
34
+ # Operation: create identifier
35
+ # @see http://ezid.cdlib.org/doc/apidoc.html#operation-create-identifier
29
36
  def create_identifier(identifier)
30
37
  [:Put, "/id/#{identifier}"]
31
38
  end
32
39
 
40
+ # Operation: modify identifier
41
+ # @see http://ezid.cdlib.org/doc/apidoc.html#operation-modify-identifier
33
42
  def modify_identifier(identifier)
34
43
  [:Post, "/id/#{identifier}"]
35
44
  end
36
45
 
46
+ # Operation: get identifier metadata
47
+ # @see http://ezid.cdlib.org/doc/apidoc.html#operation-get-identifier-metadata
37
48
  def get_identifier_metadata(identifier)
38
49
  [:Get, "/id/#{identifier}"]
39
50
  end
40
51
 
52
+ # Operation: delete identifier
53
+ # @see http://ezid.cdlib.org/doc/apidoc.html#operation-delete-identifier
41
54
  def delete_identifier(identifier)
42
55
  [:Delete, "/id/#{identifier}"]
43
56
  end
44
57
 
45
- def server_status(subsystems)
58
+ # Probe EZID server status
59
+ # @see http://ezid.cdlib.org/doc/apidoc.html#server-status
60
+ def server_status(*subsystems)
46
61
  [:Get, "/status", "subsystems=#{subsystems.join(',')}"]
47
62
  end
48
63
 
data/lib/ezid/client.rb CHANGED
@@ -1,33 +1,52 @@
1
+ require_relative "api"
1
2
  require_relative "identifier"
2
3
  require_relative "request"
4
+ require_relative "response"
5
+ require_relative "metadata"
3
6
  require_relative "session"
4
7
  require_relative "configuration"
5
8
  require_relative "error"
9
+ require_relative "logger"
6
10
 
7
11
  module Ezid
12
+ #
13
+ # EZID client
14
+ #
15
+ # @api public
8
16
  class Client
9
17
 
10
18
  class << self
19
+ # Configuration reader
11
20
  def config
12
21
  @config ||= Configuration.new
13
22
  end
14
-
23
+
24
+ # Yields the configuration to a block
25
+ # @yieldparam [Ezid::Configuration] the configuration
15
26
  def configure
16
27
  yield config
17
28
  end
18
29
 
30
+ # Creates an new identifier
31
+ # @see #create_identifier
19
32
  def create_identifier(*args)
20
33
  Client.new.create_identifier(*args)
21
34
  end
22
35
 
36
+ # Mints a new identifier
37
+ # @see #mint_identifier
23
38
  def mint_identifier(*args)
24
39
  Client.new.mint_identifier(*args)
25
40
  end
26
-
41
+
42
+ # Retrieve the metadata for an identifier
43
+ # @see #get_identifier_metadata
27
44
  def get_identifier_metadata(*args)
28
45
  Client.new.get_identifier_metadata(*args)
29
46
  end
30
47
 
48
+ # Logs into EZID
49
+ # @see #login
31
50
  def login
32
51
  Client.new.login
33
52
  end
@@ -53,131 +72,144 @@ module Ezid
53
72
  out
54
73
  end
55
74
 
75
+ # The client configuration
76
+ # @return [Ezid::Configuration] the configuration object
56
77
  def config
57
78
  self.class.config
58
79
  end
59
80
 
81
+ # The client logger
82
+ # @return [Ezid::Logger] the logger
60
83
  def logger
61
- config.logger
84
+ @logger ||= Ezid::Logger.new(config.logger)
62
85
  end
63
86
 
87
+ # Open a session
88
+ # @return [Ezid::Client] the client
64
89
  def login
65
90
  if logged_in?
66
91
  logger.info("Already logged in, skipping login request.")
67
92
  else
68
93
  do_login
69
94
  end
95
+ self
70
96
  end
71
97
 
98
+ # Close the session
99
+ # @return [Ezid::Client] the client
72
100
  def logout
73
101
  if logged_in?
74
102
  do_logout
75
103
  else
76
104
  logger.info("Not logged in, skipping logout request.")
77
105
  end
106
+ self
78
107
  end
79
108
 
109
+ # @return [true, false] whether the client is logged in
80
110
  def logged_in?
81
111
  session.open?
82
112
  end
83
113
 
114
+ # @param identifier [String] the identifier string to create
115
+ # @param metadata [String, Hash, Ezid::Metadata] optional metadata to set
116
+ # @return [Ezid::Response] the response
84
117
  def create_identifier(identifier, metadata=nil)
85
- request = Request.build(:create_identifier, identifier)
118
+ request = Request.new(:create_identifier, identifier)
86
119
  add_authentication(request)
87
120
  add_metadata(request, metadata)
88
121
  execute(request)
89
122
  end
90
123
 
124
+ # @param shoulder [String] the shoulder on which to mint a new identifier
125
+ # @param metadata [String, Hash, Ezid::Metadata] metadata to set
126
+ # @return [Ezid::Response] the response
91
127
  def mint_identifier(shoulder, metadata=nil)
92
- request = Request.build(:mint_identifier, shoulder)
128
+ request = Request.new(:mint_identifier, shoulder)
93
129
  add_authentication(request)
94
130
  add_metadata(request, metadata)
95
131
  execute(request)
96
132
  end
97
133
 
134
+ # @param identifier [String] the identifier to modify
135
+ # @param metadata [String, Hash, Ezid::Metadata] metadata to set
136
+ # @return [Ezid::Response] the response
98
137
  def modify_identifier(identifier, metadata)
99
- raise ArgumentError, "Metadata is required" if metadata.nil? || metadata.empty?
100
- request = Request.build(:modify_identifier, identifier)
138
+ request = Request.new(:modify_identifier, identifier)
101
139
  add_authentication(request)
102
140
  add_metadata(request, metadata)
103
141
  execute(request)
104
142
  end
105
143
 
144
+ # @param identifier [String] the identifier to retrieve
145
+ # @return [Ezid::Response] the response
106
146
  def get_identifier_metadata(identifier)
107
- request = Request.build(:get_identifier_metadata, identifier)
147
+ request = Request.new(:get_identifier_metadata, identifier)
108
148
  add_authentication(request)
109
149
  execute(request)
110
150
  end
111
151
 
152
+ # @param identifier [String] the identifier to delete
153
+ # @return [Ezid::Response] the response
112
154
  def delete_identifier(identifier)
113
- request = Request.build(:delete_identifier, identifier)
155
+ request = Request.new(:delete_identifier, identifier)
114
156
  add_authentication(request)
115
157
  execute(request)
116
158
  end
117
159
 
160
+ # @param subsystems [Array]
161
+ # @return [Ezid::Response] the response
118
162
  def server_status(*subsystems)
119
- request = Request.build(:server_status, subsystems)
163
+ request = Request.new(:server_status, *subsystems)
120
164
  execute(request)
121
165
  end
122
166
 
123
167
  private
124
168
 
125
- # Executes the request
126
- def execute(request)
127
- response = request.execute
128
- handle_response(response)
169
+ def build_request(*args)
170
+ request = Request.new(*args)
129
171
  end
130
172
 
131
- # Handles the response
132
- def handle_response(response)
173
+ # Executes the request
174
+ # @param request [Ezid::Request] the request
175
+ # @raise [Ezid::Error] if the response status indicates an error
176
+ # @return [Ezid::Response] the response
177
+ def execute(request)
178
+ response = Response.new(request.execute)
179
+ logger.request_and_response(request, response)
133
180
  raise Error, response.message if response.error?
134
181
  response
135
- ensure
136
- log_response(response)
137
- end
138
-
139
- # Logs a message for the response
140
- def log_response(response)
141
- logger.log(log_level(response), log_message(response))
142
- end
143
-
144
- # Returns the log level to use for the response
145
- def log_level(response)
146
- response.error? ? Logger::ERROR : Logger::INFO
147
- end
148
-
149
- # Returns the message to log for the response
150
- def log_message(response)
151
- response.status_line
152
182
  end
153
183
 
154
184
  # Adds metadata to the request
155
185
  def add_metadata(request, metadata)
156
- request.body = metadata.to_anvl unless metadata.nil? || metadata.empty?
186
+ return if metadata.nil? || metadata.empty?
187
+ metadata = Metadata.new(metadata) # copy/coerce
188
+ request.add_metadata(metadata)
157
189
  end
158
190
 
159
191
  # Adds authentication to the request
160
192
  def add_authentication(request)
161
193
  if session.open?
162
- request["Cookie"] = session.cookie
194
+ request.add_authentication(cookie: session.cookie)
163
195
  else
164
- request.basic_auth(user, password)
196
+ request.add_authentication(user: user, password: password)
165
197
  end
166
198
  end
167
199
 
200
+ # Does the login
168
201
  def do_login
169
- request = Request.build(:login)
202
+ request = Request.new(:login)
170
203
  add_authentication(request)
171
204
  response = execute(request)
172
205
  session.open(response)
173
- self
174
206
  end
175
207
 
208
+ # Does the logoug
176
209
  def do_logout
177
- request = Request.build(:logout)
210
+ request = Request.new(:logout)
178
211
  execute(request)
179
212
  session.close
180
- self
181
213
  end
182
214
 
183
215
  end
@@ -1,10 +1,24 @@
1
1
  require "logger"
2
2
 
3
3
  module Ezid
4
+ #
5
+ # EZID client configuration.
6
+ #
7
+ # Use Ezid::Client.configure to set values.
8
+ #
9
+ # @api private
4
10
  class Configuration
5
11
 
6
12
  attr_writer :user, :password, :logger
7
- attr_accessor :metadata_profile, :default_status
13
+
14
+ # Default metadata profile (recommended)
15
+ attr_accessor :default_metadata_profile
16
+
17
+ # Default status - set only if default should not "public" (EZID default)
18
+ attr_accessor :default_status
19
+
20
+ # Default shoulder for minting (recommended)
21
+ attr_accessor :default_shoulder
8
22
 
9
23
  def user
10
24
  @user ||= ENV["EZID_USER"]
@@ -15,7 +29,7 @@ module Ezid
15
29
  end
16
30
 
17
31
  def logger
18
- @logger ||= Logger.new(STDERR)
32
+ @logger ||= ::Logger.new(STDERR)
19
33
  end
20
34
 
21
35
  end
@@ -1,55 +1,128 @@
1
+ require "forwardable"
2
+
1
3
  require_relative "metadata"
2
4
 
3
5
  module Ezid
6
+ #
7
+ # An EZID identifier resource.
8
+ #
9
+ # Identifier objects are instantiated by the class methods `create', `mint' and `find'.
10
+ #
11
+ # @api public
4
12
  class Identifier
13
+ extend Forwardable
14
+
15
+ attr_reader :id
16
+
17
+ def_delegators :metadata, :status
18
+
19
+ private_class_method :new
5
20
 
6
21
  class << self
22
+ # Creates and EZID identifier
7
23
  def create(id, metadata=nil)
8
24
  response = Client.create_identifier(id, metadata)
9
- Identifier.new(response.identifier)
25
+ new(response.identifier)
10
26
  end
11
27
 
12
- def mint(metadata=nil)
28
+ # Mints an EZID identifier
29
+ def mint(shoulder=nil, metadata=nil)
13
30
  response = Client.mint_identifier(metadata)
14
- identifier = Identifier.new(response.identifier)
31
+ identifier = new(response.identifier)
15
32
  end
16
33
 
34
+ # Find an EZID indentifier
17
35
  def find(id)
18
36
  response = Client.get_identifier_metadata(id)
19
- Identifier.new(response.identifier, response.metadata)
37
+ new(response.identifier, response.metadata)
20
38
  end
21
39
  end
22
40
 
23
- attr_reader :id
24
-
25
41
  def initialize(id, metadata=nil)
26
42
  @id = id
27
43
  @metadata = Metadata.new(metadata)
28
44
  end
29
45
 
46
+ # The identifier metadata, cached locally
47
+ # @return [Ezid::Metadata] the metadata
30
48
  def metadata
31
49
  reload if @metadata.empty?
32
50
  @metadata
33
51
  end
34
52
 
53
+ # The identifier which this identifier is shadowed by, or nil.
54
+ # @return [Ezid::Identifier] the shadowing identifier
55
+ def shadowed_by
56
+ Identifer.new(metadata.shadowedby) if metadata.shadowedby
57
+ end
58
+
59
+ # The identifer which this identifier shadows, or nil.
60
+ # @return [Ezid::Identifier] the shadowed identifier
61
+ def shadows
62
+ Identifier.new(metadata.shadows) if metadata.shadows
63
+ end
64
+
65
+ # Retrieve the current metadata for the identifier from EZID
66
+ # @return [Ezid::Identifier] the identifier
35
67
  def reload
36
68
  response = client.get_identifier_metadata(id)
37
- @metadata.update(response.metadata)
69
+ @metadata = response.metadata
38
70
  self
39
71
  end
40
72
 
41
- def client
42
- @client ||= Client.new
73
+ # Clears the metadata on the identifier object
74
+ # @return [Ezid::Identifier] the identifier
75
+ def reset
76
+ @metadata = Metadata.new
77
+ self
43
78
  end
44
79
 
45
- def save
46
- response = client.modify_identifier(id, metadata)
47
- response.success?
80
+ # Returns an EZID client
81
+ # @return [Ezid::Client] the client
82
+ def client
83
+ @client ||= Client.new
48
84
  end
49
85
 
86
+ # Deletes the identifier - caution!
50
87
  def delete
51
88
  response = client.delete_identifier(id)
52
- response.success?
89
+ reset
90
+ freeze
91
+ response.message
92
+ end
93
+
94
+ # Sends the metadata to EZID - caution!
95
+ def update(metadata)
96
+ response = client.modify_identifier_metadata(metadata)
97
+ reset
98
+ response.message
99
+ end
100
+
101
+ def make_public!
102
+ update(_status: Metadata::PUBLIC)
103
+ end
104
+
105
+ def make_unavailable!
106
+ update(_status: Metadata::UNAVAILABLE)
107
+ end
108
+
109
+ def public?
110
+ status == Metadata::PUBLIC
111
+ end
112
+
113
+ def reserved?
114
+ status == Metadata::RESERVED
115
+ end
116
+
117
+ def unavailable?
118
+ status == Metadata::UNAVAILABLE
119
+ end
120
+
121
+ private
122
+
123
+ def remove(*elements)
124
+ metadata = elements.map { |el| [el, ""] }.to_h
125
+ update(metadata)
53
126
  end
54
127
 
55
128
  end
@@ -0,0 +1,31 @@
1
+ require "delegate"
2
+ require "logger"
3
+
4
+ module Ezid
5
+ #
6
+ # Custom logger for EZID client
7
+ #
8
+ # @api private
9
+ class Logger < SimpleDelegator
10
+
11
+ # Logs a message for an EZID request/response
12
+ # @param request [Ezid::Request] the request
13
+ # @param response [Ezid::Response] the response
14
+ def request_and_response(request, response)
15
+ level = response.error? ? ::Logger::ERROR : ::Logger::INFO
16
+ response_message = response.status_line
17
+ message = "EZID #{request_message(request)}: #{response_message}"
18
+ log(level, message)
19
+ end
20
+
21
+ private
22
+
23
+ def request_message(request)
24
+ message = request.operation[0].to_s
25
+ args = request.operation[1..-1]
26
+ message << "(#{args.join(', ')})" if args.any?
27
+ message
28
+ end
29
+
30
+ end
31
+ end
data/lib/ezid/metadata.rb CHANGED
@@ -1,79 +1,151 @@
1
1
  require "forwardable"
2
2
 
3
3
  module Ezid
4
+ #
5
+ # EZID metadata collection for an identifier
6
+ #
7
+ # @api public
4
8
  class Metadata
5
9
  extend Forwardable
10
+ include Enumerable
6
11
 
12
+ # The metadata elements hash
7
13
  attr_reader :elements
8
- def_delegators :elements, :[], :[]=, :empty?, :to_h, :to_a
9
14
 
10
- ERC_PROFILE = "erc"
11
- DC_PROFILE = "dc"
12
- DATACITE_PROFILE = "datacite"
13
- CROSSREF_PROFILE = "crossref"
15
+ def_delegators :elements, :each, :empty?, :[], :[]=
14
16
 
15
- STATUS_PUBLIC = "public"
16
- STATUS_RESERVED = "reserved"
17
- STATUS_UNAVAILABLE = "unavailable"
17
+ # EZID metadata profiles
18
+ PROFILES = %w( erc dc datacite crossref )
18
19
 
19
- # Internal metadata elements
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
20
33
  INTERNAL_READONLY_ELEMENTS = %w( _owner _ownergroup _created _updated _shadows _shadowedby _datacenter ).freeze
34
+
35
+ # EZID internal writable metadata elements
21
36
  INTERNAL_READWRITE_ELEMENTS = %w( _coowners _target _profile _status _export _crossref ).freeze
37
+
38
+ # EZID internal metadata elements
22
39
  INTERNAL_ELEMENTS = (INTERNAL_READONLY_ELEMENTS + INTERNAL_READWRITE_ELEMENTS).freeze
40
+
41
+ # Internal metadata element which are datetime values
42
+ # @note EZID outputs datetime info as epoch seconds.
43
+ DATETIME_ELEMENTS = %w( _created _updated ).freeze
23
44
 
24
- ANVL_SEPARATOR = ": ".freeze
45
+ # EZID metadata field/value separator
46
+ ANVL_SEPARATOR = ": "
25
47
 
26
- # Creates a reader method for each internal metadata element
27
- INTERNAL_ELEMENTS.each do |element|
28
- reader = element.sub("_", "").to_sym
29
- define_method(reader) do
30
- self[element]
31
- end
32
- end
48
+ # Characters to escape on output to EZID
49
+ ESCAPE_RE = /[%:\r\n]/
33
50
 
34
- # Creates a writer method for each writable internal metadata element
35
- INTERNAL_READWRITE_ELEMENTS.each do |element|
36
- writer = "#{element.sub('_', '')}=".to_sym
37
- define_method(writer) do |value|
38
- self[element] = value
39
- end
40
- end
51
+ # Character sequence to unescape from EZID
52
+ UNESCAPE_RE = /%\h\h/
53
+
54
+ # A comment line
55
+ COMMENT_RE = /^#.*(\r?\n)?/
56
+
57
+ # A line continuation
58
+ LINE_CONTINUATION_RE = /\r?\n\s+/
59
+
60
+ # A line ending
61
+ LINE_ENDING_RE = /\r?\n/
41
62
 
42
- # @param data [Hash, String, Ezid::Metadata] EZID metadata
43
63
  def initialize(data={})
44
64
  @elements = coerce(data)
45
65
  end
46
66
 
47
- # @todo escape \n, \r and %
48
- # @todo force UTF-8
67
+ # Output metadata in EZID ANVL format
49
68
  # @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
69
+ # @return [String] the ANVL output
50
70
  def to_anvl
51
- to_a.map { |pair| pair.join(ANVL_SEPARATOR) }.join("\n")
71
+ lines = map { |element| element.map { |e| escape(e) }.join(ANVL_SEPARATOR) }
72
+ lines.join("\n").force_encoding(Encoding::UTF_8)
52
73
  end
53
74
 
54
75
  def to_s
55
76
  to_anvl
56
77
  end
57
78
 
58
- # Add metadata
79
+ # Adds metadata to the collection
80
+ # @param data [String, Hash, Ezid::Metadata] the data to add
81
+ # @return [Ezid::Metadata] the updated metadata
59
82
  def update(data)
60
83
  elements.update(coerce(data))
84
+ self
85
+ end
86
+
87
+ # method_missing is used to provide internal element readers and writers
88
+ def method_missing(name, *args)
89
+ if INTERNAL_ELEMENTS.include?(element = "_#{name}")
90
+ reader(element)
91
+ elsif name.to_s.end_with?("=") && INTERNAL_READWRITE_ELEMENTS.include?(element = "_#{name}".sub("=", ""))
92
+ writer(element, args.first)
93
+ else
94
+ super
95
+ end
61
96
  end
62
97
 
63
98
  private
64
99
 
100
+ def reader(element)
101
+ value = self[element]
102
+ return Time.at(value.to_i) if DATETIME_ELEMENTS.include?(element) && !value.nil?
103
+ value
104
+ end
105
+
106
+ def writer(element, value)
107
+ self[element] = value
108
+ end
109
+
65
110
  # Coerce data into a Hash of elements
66
- # @todo unescape
67
- # @see {#to_anvl}
68
111
  def coerce(data)
69
112
  begin
70
- data.to_h
113
+ stringify_keys(data.to_h)
71
114
  rescue NoMethodError
72
- # This does not account for comments and continuation lines
73
- # http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
74
- data.split(/\r?\n/).map { |line| line.split(ANVL_SEPARATOR, 2) }.to_h
115
+ coerce_string(data)
75
116
  end
76
117
  end
77
118
 
119
+ def stringify_keys(hsh)
120
+ hsh.keys.map(&:to_s).zip(hsh.values).to_h
121
+ end
122
+
123
+ # Escape value for sending to EZID host
124
+ # @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
125
+ # @param value [String] the value to escape
126
+ # @return [String] the escaped value
127
+ def escape(value)
128
+ value.gsub(ESCAPE_RE) { |m| URI.encode(m) }
129
+ end
130
+
131
+ # Unescape value from EZID host (or other source)
132
+ # @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
133
+ # @param value [String] the value to unescape
134
+ # @return [String] the unescaped value
135
+ def unescape(value)
136
+ value.gsub(UNESCAPE_RE) { |m| URI.decode(m) }
137
+ end
138
+
139
+ # Coerce a string of metadata (e.g., from EZID host) into a Hash
140
+ # @param data [String] the string to coerce
141
+ # @return [Hash] the hash of coerced data
142
+ def coerce_string(data)
143
+ data.gsub(COMMENT_RE, "")
144
+ .gsub(LINE_CONTINUATION_RE, " ")
145
+ .split(LINE_ENDING_RE)
146
+ .map { |line| line.split(ANVL_SEPARATOR, 2).map { |v| unescape(v).strip } }
147
+ .to_h
148
+ end
149
+
78
150
  end
79
151
  end
data/lib/ezid/request.rb CHANGED
@@ -1,33 +1,54 @@
1
1
  require "uri"
2
2
  require "net/http"
3
- require "delegate"
4
-
5
- require_relative "api"
6
- require_relative "response"
7
3
 
8
4
  module Ezid
9
5
  #
10
6
  # A request to the EZID service.
11
7
  #
12
- class Request < SimpleDelegator
8
+ # @note A Request should only be created by an Ezid::Client instance.
9
+ # @api private
10
+ class Request
13
11
 
14
12
  EZID_HOST = "ezid.cdlib.org"
15
13
  CHARSET = "UTF-8"
16
14
  CONTENT_TYPE = "text/plain"
17
15
 
18
- def self.build(op, *args)
19
- http_method, path, query = Api.send(op, *args)
20
- uri = URI::HTTPS.build(host: EZID_HOST, path: path, query: query)
21
- http_request = Net::HTTP.const_get(http_method).new(uri)
22
- Request.new(http_request)
16
+ attr_reader :http_request, :uri, :operation
17
+
18
+ def initialize(*args)
19
+ @operation = args
20
+ http_method, path, query = Api.send(*args)
21
+ @uri = URI::HTTPS.build(host: EZID_HOST, path: path, query: query)
22
+ @http_request = Net::HTTP.const_get(http_method).new(uri)
23
+ @http_request.set_content_type(CONTENT_TYPE, charset: CHARSET)
23
24
  end
24
25
 
26
+ # Executes the request and returns the HTTP response
27
+ # @return [Net::HTTPResponse] the response
25
28
  def execute
26
- http_response = Net::HTTP.start(uri.host, use_ssl: true) do |http|
27
- set_content_type(CONTENT_TYPE, charset: CHARSET)
28
- http.request(__getobj__)
29
+ Net::HTTP.start(uri.host, use_ssl: true) do |http|
30
+ http.request(http_request)
29
31
  end
30
- Response.build(http_response)
32
+ end
33
+
34
+ # Adds authentication data to the request
35
+ # @param opts [Hash] the options.
36
+ # Must include either: `:cookie`, or: `:user` and `:password`.
37
+ # @option opts [String] :cookie a session cookie
38
+ # @option opts [String] :user user name for basic auth
39
+ # @option opts [String] :password password for basic auth
40
+ def add_authentication(opts={})
41
+ if opts[:cookie]
42
+ http_request["Cookie"] = opts[:cookie]
43
+ else
44
+ http_request.basic_auth(opts[:user], opts[:password])
45
+ end
46
+ end
47
+
48
+ # Adds EZID metadata (if any) to the request body
49
+ # @param metadata [Ezid::Metadata] the metadata to add
50
+ def add_metadata(metadata)
51
+ http_request.body = metadata.to_anvl unless metadata.empty?
31
52
  end
32
53
 
33
54
  end
data/lib/ezid/response.rb CHANGED
@@ -1,38 +1,55 @@
1
1
  require "delegate"
2
2
 
3
3
  module Ezid
4
+ #
4
5
  # A response from the EZID service.
6
+ #
7
+ # @note A Response should only be created by an Ezid::Client instance.
8
+ # @api private
5
9
  class Response < SimpleDelegator
6
10
 
11
+ # Success response status
7
12
  SUCCESS = "success"
8
- ERROR = "error"
9
13
 
10
- def self.build(http_response)
11
- Response.new(http_response)
12
- end
14
+ # Error response status
15
+ ERROR = "error"
13
16
 
17
+ # The response status -- "success" or "error"
18
+ # @return [String] the status
14
19
  def status
15
20
  @status ||= status_line.split(/: /)
16
21
  end
17
22
 
23
+ # The status line of the response
24
+ # @return [String] the status line
18
25
  def status_line
19
26
  content.first
20
27
  end
21
28
 
29
+ # The body of the response split into: status line and rest of body
30
+ # @return [Array] status line, rest of body
22
31
  def content
23
32
  @content ||= body.split(/\r?\n/, 2)
24
33
  end
25
34
 
35
+ # Metadata (if any) parsed out of the response
36
+ # @return [Ezid::Metadata] the metadata
26
37
  def metadata
27
- content.last if success? && identifier_uri?
38
+ return @metadata if @metadata
39
+ if success? && identifier_uri?
40
+ @metadata = Metadata.new(content.last)
41
+ end
42
+ @metadata
28
43
  end
29
44
 
45
+ # The identifier string parsed out of the response
46
+ # @return [String] the identifier
30
47
  def identifier
31
48
  message.split(/\s/).first if success? && identifier_uri?
32
49
  end
33
50
 
34
51
  def identifier_uri?
35
- uri.path =~ /^\/(id|shoulder)\//
52
+ ( uri.path =~ /^\/(id|shoulder)\// ) && true
36
53
  end
37
54
 
38
55
  def outcome
data/lib/ezid/session.rb CHANGED
@@ -4,6 +4,10 @@ require "net/http"
4
4
  require_relative "request"
5
5
 
6
6
  module Ezid
7
+ #
8
+ # An EZID session
9
+ #
10
+ # @api private
7
11
  class Session
8
12
 
9
13
  attr_reader :cookie
@@ -1,9 +1,14 @@
1
1
  module Ezid
2
2
  module TestHelper
3
-
4
- TEST_USER = "apitest"
3
+
5
4
  ARK_SHOULDER = "ark:/99999/fk4"
6
5
  DOI_SHOULDER = "doi:10.5072/FK2"
6
+ USER = "apitest"
7
+
8
+ Client.configure do |config|
9
+ config.user = USER
10
+ config.default_shoulder = ARK_SHOULDER
11
+ end
7
12
 
8
13
  def doi_metadata
9
14
  Metadata.new("datacite.title" => "Test",
@@ -2,22 +2,19 @@ module Ezid
2
2
  RSpec.describe Client do
3
3
  describe "initialization" do
4
4
  describe "without a block" do
5
- subject { described_class.new(user: TEST_USER) }
6
5
  it "should not be logged in" do
7
6
  expect(subject).not_to be_logged_in
8
7
  end
9
8
  end
10
9
  describe "with a block", :vcr do
11
10
  it "should be logged in" do
12
- described_class.new(user: TEST_USER) do |client|
11
+ described_class.new do |client|
13
12
  expect(client).to be_logged_in
14
13
  end
15
14
  end
16
15
  end
17
- end
18
-
16
+ end
19
17
  describe "authentication", :vcr do
20
- subject { described_class.new(user: TEST_USER) }
21
18
  describe "logging in" do
22
19
  before { subject.login }
23
20
  it "should be logged in" do
@@ -25,54 +22,67 @@ module Ezid
25
22
  end
26
23
  end
27
24
  describe "logging out" do
28
- before { subject.login; subject.logout }
25
+ before { subject.login }
29
26
  it "should not be logged in" do
27
+ subject.logout
30
28
  expect(subject).not_to be_logged_in
31
29
  end
32
30
  end
33
31
  end
34
- describe "creating an identifier" do
32
+ describe "creating an identifier", :vcr do
35
33
  # TODO
36
34
  end
37
35
  describe "minting an identifier", :vcr do
38
- let(:client) { described_class.new(user: TEST_USER) }
39
36
  describe "which is an ARK" do
40
- subject { client.mint_identifier(ARK_SHOULDER) }
41
37
  it "should be a success" do
42
- expect(subject).to be_success
43
- expect(subject.message).to match(/#{ARK_SHOULDER}/)
38
+ response = subject.mint_identifier(ARK_SHOULDER)
39
+ expect(response).to be_success
40
+ expect(response.message).to match(/#{ARK_SHOULDER}/)
44
41
  end
45
42
  end
46
43
  describe "which is a DOI" do
47
- subject { client.mint_identifier(DOI_SHOULDER, doi_metadata) }
48
44
  it "should be a sucess" do
49
- expect(subject).to be_success
50
- expect(subject.message).to match(/#{DOI_SHOULDER}/)
51
- expect(subject.message).to match(/\| ark:/)
45
+ response = subject.mint_identifier(DOI_SHOULDER, doi_metadata)
46
+ expect(response).to be_success
47
+ expect(response.message).to match(/#{DOI_SHOULDER}/)
48
+ expect(response.message).to match(/\| ark:/)
52
49
  end
53
50
  end
54
51
  end
55
- describe "getting identifier metadata", :vcr do
56
- let(:client) { described_class.new(user: TEST_USER) }
57
- let(:metadata) { Metadata.new("dc.title" => "Test") }
58
- let(:identifier) { client.mint_identifier(ARK_SHOULDER, metadata).message }
59
- subject { Metadata.new(client.get_identifier_metadata(identifier).content.last) }
52
+ describe "getting identifier metadata" do
53
+ before do
54
+ @identifier = subject.mint_identifier(ARK_SHOULDER).identifier
55
+ end
60
56
  it "should return the metadata" do
61
- expect(subject["dc.title"]).to eq("Test")
57
+ response = subject.get_identifier_metadata(@identifier)
58
+ expect(response.body).to match(/_status: public/)
62
59
  end
63
60
  end
64
61
  describe "modifying an identifier" do
65
- # TODO
62
+ before do
63
+ @identifier = subject.mint_identifier(ARK_SHOULDER).identifier
64
+ end
65
+ it "should update the metadata" do
66
+ subject.modify_identifier(@identifier, "dc.title" => "Test")
67
+ response = subject.get_identifier_metadata(@identifier)
68
+ expect(response.body).to match(/dc.title: Test/)
69
+ end
66
70
  end
67
71
  describe "deleting an identifier" do
68
- # TODO
72
+ before do
73
+ @identifier = subject.mint_identifier(ARK_SHOULDER, "_status" => "reserved").identifier
74
+ end
75
+ it "should delete the identifier" do
76
+ response = subject.delete_identifier(@identifier)
77
+ expect(response).to be_success
78
+ expect { subject.get_identifier_metadata(@identifier) }.to raise_error
79
+ end
69
80
  end
70
81
  describe "server status", :vcr do
71
- let(:client) { described_class.new(user: TEST_USER) }
72
- subject { client.server_status("*") }
73
82
  it "should report the status of EZID and subsystems" do
74
- expect(subject).to be_success
75
- expect(subject.message).to eq "EZID is up"
83
+ response = subject.server_status("*")
84
+ expect(response).to be_success
85
+ expect(response.message).to eq("EZID is up")
76
86
  end
77
87
  end
78
88
  end
@@ -0,0 +1,5 @@
1
+ module Ezid
2
+ RSpec.describe Identifier do
3
+
4
+ end
5
+ end
@@ -0,0 +1,62 @@
1
+ module Ezid
2
+ RSpec.describe Metadata do
3
+ describe "method missing" do
4
+ context "for an internal metadata element name w/o leading underscore" do
5
+ it "should call the reader method" do
6
+ expect(subject).to receive(:reader).with("_status")
7
+ subject.status
8
+ end
9
+ end
10
+ context "for an internal writable metadata element name + '=' and w/o leading underscore" do
11
+ it "should call the writer method" do
12
+ expect(subject).to receive(:writer).with("_status", "public")
13
+ subject.status = "public"
14
+ end
15
+ end
16
+ end
17
+ describe "internal element reader" do
18
+ context "for a datetime element" do
19
+ before { subject["_created"] = "1416507086" }
20
+ it "should return a Time" do
21
+ expect(subject.created).to be_a(Time)
22
+ end
23
+ end
24
+ context "for a non-datetime element" do
25
+ before { subject["_status"] = "public" }
26
+ it "should return the value" do
27
+ expect(subject.status).to eq("public")
28
+ end
29
+ end
30
+ end
31
+ describe "internal element writer" do
32
+ before { subject["_status"] = "reserved" }
33
+ it "should set the element" do
34
+ expect { subject.status = "public" }.to change { subject["_status"] }.from("reserved").to("public")
35
+ end
36
+ end
37
+ describe "ANVL output" do
38
+ let(:metadata) { described_class.new(_updated: "1416507086",
39
+ _target: "http://ezid.cdlib.org/id/ark:/99999/fk4fn19h87",
40
+ _profile: "erc",
41
+ _ownergroup: "apitest",
42
+ _owner: "apitest",
43
+ _export: "yes",
44
+ _created: "1416507086",
45
+ _status: "public") }
46
+ it "should output the proper format" do
47
+ expect(metadata.to_anvl).to eq("\
48
+ _updated: 1416507086
49
+ _target: http://ezid.cdlib.org/id/ark:/99999/fk4fn19h87
50
+ _profile: erc
51
+ _ownergroup: apitest
52
+ _owner: apitest
53
+ _export: yes
54
+ _created: 1416507086
55
+ _status: public")
56
+ end
57
+ end
58
+ describe "coercion" do
59
+
60
+ end
61
+ end
62
+ end
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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dchandekstark
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-20 00:00:00.000000000 Z
11
+ date: 2014-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -101,12 +101,15 @@ files:
101
101
  - lib/ezid/configuration.rb
102
102
  - lib/ezid/error.rb
103
103
  - lib/ezid/identifier.rb
104
+ - lib/ezid/logger.rb
104
105
  - lib/ezid/metadata.rb
105
106
  - lib/ezid/request.rb
106
107
  - lib/ezid/response.rb
107
108
  - lib/ezid/session.rb
108
109
  - lib/ezid/test_helper.rb
109
110
  - spec/lib/ezid/client_spec.rb
111
+ - spec/lib/ezid/identifier_spec.rb
112
+ - spec/lib/ezid/metadata_spec.rb
110
113
  - spec/spec_helper.rb
111
114
  homepage: https://github.com/duke-libraries/ezid-client
112
115
  licenses:
@@ -134,4 +137,6 @@ specification_version: 4
134
137
  summary: Ruby client for EZID API Version 2
135
138
  test_files:
136
139
  - spec/lib/ezid/client_spec.rb
140
+ - spec/lib/ezid/identifier_spec.rb
141
+ - spec/lib/ezid/metadata_spec.rb
137
142
  - spec/spec_helper.rb