ezid-client 0.3.0 → 0.4.0

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