ezid-client 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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