ezid-client 0.1.0 → 0.1.1

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: 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