aspire 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +59 -0
  3. data/.rbenv-gemsets +1 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Dockerfile +20 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +851 -0
  10. data/Rakefile +10 -0
  11. data/aspire.gemspec +40 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/entrypoint.sh +11 -0
  15. data/exe/build-cache +13 -0
  16. data/lib/aspire.rb +11 -0
  17. data/lib/aspire/api.rb +2 -0
  18. data/lib/aspire/api/base.rb +198 -0
  19. data/lib/aspire/api/json.rb +195 -0
  20. data/lib/aspire/api/linked_data.rb +214 -0
  21. data/lib/aspire/caching.rb +4 -0
  22. data/lib/aspire/caching/builder.rb +356 -0
  23. data/lib/aspire/caching/cache.rb +365 -0
  24. data/lib/aspire/caching/cache_entry.rb +296 -0
  25. data/lib/aspire/caching/cache_logger.rb +63 -0
  26. data/lib/aspire/caching/util.rb +210 -0
  27. data/lib/aspire/cli/cache_builder.rb +123 -0
  28. data/lib/aspire/cli/command.rb +20 -0
  29. data/lib/aspire/enumerator/base.rb +29 -0
  30. data/lib/aspire/enumerator/json_enumerator.rb +130 -0
  31. data/lib/aspire/enumerator/linked_data_uri_enumerator.rb +32 -0
  32. data/lib/aspire/enumerator/report_enumerator.rb +64 -0
  33. data/lib/aspire/exceptions.rb +36 -0
  34. data/lib/aspire/object.rb +7 -0
  35. data/lib/aspire/object/base.rb +155 -0
  36. data/lib/aspire/object/digitisation.rb +43 -0
  37. data/lib/aspire/object/factory.rb +87 -0
  38. data/lib/aspire/object/list.rb +590 -0
  39. data/lib/aspire/object/module.rb +36 -0
  40. data/lib/aspire/object/resource.rb +371 -0
  41. data/lib/aspire/object/time_period.rb +47 -0
  42. data/lib/aspire/object/user.rb +46 -0
  43. data/lib/aspire/properties.rb +20 -0
  44. data/lib/aspire/user_lookup.rb +103 -0
  45. data/lib/aspire/util.rb +185 -0
  46. data/lib/aspire/version.rb +3 -0
  47. data/lib/retry.rb +197 -0
  48. metadata +274 -0
@@ -0,0 +1,46 @@
1
+ require 'aspire/object/base'
2
+
3
+ module Aspire
4
+ module Object
5
+ # Represents a user profile in the Aspire API
6
+ class User < Base
7
+ # @!attribute [rw] email
8
+ # @return [Array<String>] the list of email addresses for the user
9
+ attr_accessor :email
10
+
11
+ # @!attribute [rw] first_name
12
+ # @return [String] the user's first name
13
+ attr_accessor :first_name
14
+
15
+ # @!attribute [rw] role
16
+ # @return [Array<String>] the Aspire roles associated with the user
17
+ attr_accessor :role
18
+
19
+ # @!attribute [rw] surname
20
+ # @return [String] the user's last name
21
+ attr_accessor :surname
22
+
23
+ # Initialises a new User instance
24
+ # @param uri [String] the URI of the user profile
25
+ # @param factory [Aspire::Object::Factory] the data object factory
26
+ # @param json [Hash] the user profile data from the Aspire JSON API
27
+ # @param ld [Hash] the user profile data from the Aspire linked data API
28
+ # @return [void]
29
+ def initialize(uri, factory, json: nil, ld: nil)
30
+ super(uri, factory)
31
+ json ||= {}
32
+ self.email = json['email']
33
+ self.first_name = json['firstName']
34
+ self.role = json['role']
35
+ self.surname = json['surname']
36
+ end
37
+
38
+ # Returns a string representation of the user profile (name and emails)
39
+ # @return [String] the string representation of the user profile
40
+ def to_s
41
+ emails = email.nil? || email.empty? ? '' : " <#{email.join('; ')}>"
42
+ "#{first_name} #{surname}#{emails}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Aspire
2
+ # Aspire linked data API property names
3
+ module Properties
4
+ AIISO_CODE = 'http://purl.org/vocab/aiiso/schema#code'.freeze
5
+ AIISO_NAME = 'http://putl.org/vocab/aiiso/schema#name'.freeze
6
+ CREATED = 'http://purl.org/vocab/resourcelist/schema#created'.freeze
7
+ DESCRIPTION = 'http://purl.org/vocab/resourcelist/schema#description'
8
+ .freeze
9
+ HAS_CREATOR = 'http://rdfs.org/sioc/spec/has_creator'.freeze
10
+ HAS_OWNER = 'http://purl.org/vocab/resourcelist/schema#hasOwner'.freeze
11
+ LAST_PUBLISHED = 'http://purl.org/vocab/resourcelist/schema#lastPublished'
12
+ .freeze
13
+ LAST_UPDATED = 'http://purl.org/vocab/resourcelist/schema#lastUpdated'
14
+ .freeze
15
+ NAME = 'http://rdfs.org/sioc/spec/name'.freeze
16
+ PUBLISHED_BY = 'http://purl.org/vocab/resourcelist/schema#publishedBy'
17
+ .freeze
18
+ USED_BY = 'http://purl.org/vocab/resourcelist/schema#usedBy'.freeze
19
+ end
20
+ end
@@ -0,0 +1,103 @@
1
+ require 'csv'
2
+
3
+ require 'aspire/enumerator/report_enumerator'
4
+ require 'aspire/object/user'
5
+
6
+ module Aspire
7
+ # Implements a hash of User instances indexed by URI
8
+ # The hash can be populated from an Aspire All User Profiles report CSV file
9
+ class UserLookup
10
+ # @!attribute [rw] store
11
+ # @return [Object] a hash-like object mapping user URIs to their JSON data
12
+ attr_accessor :store
13
+
14
+ # Initialises a new UserLookup instance
15
+ # @see (Hash#initialize)
16
+ # @param filename [String] the filename of the CSV file to populate the hash
17
+ # @return [void]
18
+ def initialize(filename: nil, store: nil)
19
+ self.store = store || {}
20
+ load(filename) if filename
21
+ end
22
+
23
+ # Returns an Aspire::Object::User instance for a URI
24
+ # @param uri [String] the URI of the user
25
+ # @param factory [Aspire::Object::Factory] the data object factory
26
+ # @return [Aspire::Object::User] the user
27
+ def [](uri, factory = nil)
28
+ data = store[uri]
29
+ data.nil? ? nil : Aspire::Object::User.new(uri, factory, json: data)
30
+ end
31
+
32
+ # Populates the store from an All User Profiles report CSV file
33
+ # @param filename [String] the filename of the CSV file
34
+ # @return [void]
35
+ def load(filename = nil)
36
+ delim = /\s*;\s*/ # The delimiter for email and role lists
37
+ enum = Aspire::Enumerator::ReportEnumerator.new(filename).enumerator
38
+ enum.each do |row|
39
+ # Construct a JSON data structure for the user
40
+ uri = row[3]
41
+ data = csv_to_json_api(row, email_delim: delim, role_delim: delim)
42
+ csv_to_json_other(row, data)
43
+ # Store the JSON data in the lookup table
44
+ store[uri] = data
45
+ end
46
+ end
47
+
48
+ # Proxies missing methods to the store
49
+ # @param method [Symbol] the method name
50
+ # @param args [Array] the method arguments
51
+ # @param block [Proc] the code block
52
+ # @return [Object] the store method result
53
+ def method_missing(method, *args, &block)
54
+ super unless store.respond_to?(method)
55
+ store.public_send(method, *args, &block)
56
+ end
57
+
58
+ # Proxies missing method respond_to? to the store
59
+ # @param method [Symbol] the method name
60
+ # @param include_private [Boolean] if true, include private methods,
61
+ # otherwise include only public methods
62
+ # @return [Boolean] true if the store supports the method, false otherwise
63
+ def respond_to_missing?(method, include_private = false)
64
+ store.respond_to?(method, include_private)
65
+ end
66
+
67
+ private
68
+
69
+ def csv_to_json(row)
70
+ # Recreate the Aspire user profile JSON API response from the CSV record
71
+ data = csv_to_json_api(row)
72
+ # Add other report fields which aren't part of the JSON API response
73
+ csv_to_json_other(row, data)
74
+ end
75
+
76
+ # Adds CSV fields which mirror the Aspire user profile JSON API fields
77
+ # @param row [Array] the fields from the All User Profiles report CSV
78
+ # @param data [Hash] the JSON representation of the user profile
79
+ # @return [Hash] the JSON data hash
80
+ def csv_to_json_api(row, data = {}, email_delim: nil, role_delim: nil)
81
+ data['email'] = (row[4] || '').split(email_delim)
82
+ data['firstName'] = row[0]
83
+ data['role'] = (row[7] || '').split(role_delim)
84
+ data['surname'] = row[1]
85
+ data['uri'] = row[3]
86
+ data
87
+ end
88
+
89
+ # Adds CSV fields which aren't part of the Aspire user profile JSON API
90
+ # @param row [Array] the fields from the All User Profiles report CSV
91
+ # @param data [Hash] the JSON representation of the user profile
92
+ # @return [Hash] the JSON data hash
93
+ def csv_to_json_other(row, data = {})
94
+ # The following fields are not present in the JSON API response but are in
95
+ # the All User Profiles report - they are included for completeness.
96
+ data['jobRole'] = row[5] || ''
97
+ data['lastLogin'] = row[8]
98
+ data['name'] = row[2] || ''
99
+ data['visibility'] = row[6] || ''
100
+ data
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,185 @@
1
+ module Aspire
2
+ # Utility methods mixin
3
+ module Util
4
+ # Regular expression to parse a Linked Data API URI
5
+ LD_API_URI = Regexp.new('https?://(?<tenancy_host>[^/]*)/' \
6
+ '(?<type>[^/]*)/' \
7
+ '(?<id>[^/\.]*)' \
8
+ '(\.(?<format>[^/]*))?' \
9
+ '(/' \
10
+ '(?<child_type>[^/.]*)' \
11
+ '(/(?<child_id>[^/\.]*))?' \
12
+ '(\.(?<child_format>[^/]*))?' \
13
+ ')?(?<rest>.*)').freeze
14
+
15
+ # Returns true if the first URL is the child of the second URL
16
+ # @param url1 [Aspire::Caching::CacheEntry, String] the first URL
17
+ # @param url2 [Aspire::Caching::CacheEntry, String] the second URL
18
+ # @param api [Aspire::API::LinkedData] the API for generating canonical URLs
19
+ # @param strict [Boolean] if true, the URL must be a parent of this entry,
20
+ # otherwise the URL must be a parent or the same as this entry
21
+ # @return [Boolean] true if the URL is a child of the cache entry, false
22
+ # otherwise
23
+ def child_url?(url1, url2, api = nil, strict: false)
24
+ parent_url?(url2, url1, api, strict: strict)
25
+ end
26
+
27
+ # Returns a HH:MM:SS string given a Benchmark time
28
+ # @param benchmark_time [Benchmark:Tms] the Benchmark time object
29
+ # @return [String] the HH:HM:SS string
30
+ def duration(benchmark_time)
31
+ secs = benchmark_time.real
32
+ hours = secs / 3600
33
+ format('%2.2d:%2.2d:%2.2d', hours, hours % 60, secs % 60)
34
+ end
35
+
36
+ # Returns the ID of an object from its URL
37
+ # @param u [String] the URL of the API object
38
+ # @return [String] the object ID
39
+ def id_from_uri(u, parsed: nil)
40
+ parsed ||= parse_url(u)
41
+ parsed[:id]
42
+ end
43
+
44
+ # Returns true if URI is a list item
45
+ # @param uri [String] the URI
46
+ # @return [Boolean] true if the URI is a list item, otherwise false
47
+ def item?(uri)
48
+ uri.include?('/items/')
49
+ end
50
+
51
+ # Returns the data for a URI from a parsed linked data API response
52
+ # which may contain multiple objects
53
+ # @param uri [String] the URI of the object
54
+ # @param ld [Hash] the parsed JSON data from the Aspire linked data API
55
+ # @return [Hash] the parsed JSON data for the URI
56
+ def linked_data(uri, ld)
57
+ uri = linked_data_path(uri)
58
+ return nil unless uri && ld
59
+ # The URI used to retrieve the data may be the canonical URI or a
60
+ # tenancy aliases. We ignore the host part of the URIs and match just
61
+ # the path
62
+ ld.each { |u, data| return data if uri == linked_data_path(u) }
63
+ # No match was found
64
+ nil
65
+ end
66
+
67
+ # Returns the path of a URI
68
+ # @param uri [String] the URI
69
+ # @return [String, nil] the URI path or nil if invalid
70
+ def linked_data_path(uri)
71
+ URI.parse(uri).path
72
+ rescue URI::InvalidComponentError, URI::InvalidURIError
73
+ nil
74
+ end
75
+
76
+ # Returns true if URI is a list
77
+ # @param uri [String] the URI
78
+ # @return [Boolean] true if the URI is a list, otherwise false
79
+ def list?(uri)
80
+ uri.include?('/lists/')
81
+ end
82
+
83
+ # Returns true if a URL is a list URL, false otherwise
84
+ # @param u [String] the URL of the API object
85
+ # @return [Boolean] true if the URL is a list URL, false otherwise
86
+ def list_url?(u = nil, parsed: nil)
87
+ return false if (u.nil? || u.empty?) && parsed.nil?
88
+ parsed ||= parse_url(u)
89
+ child_type = parsed[:child_type]
90
+ parsed[:type] == 'lists' && (child_type.nil? || child_type.empty?)
91
+ end
92
+
93
+ # Returns true if URI is a module
94
+ # @param uri [String] the URI
95
+ # @return [Boolean] true if the URI is a module, otherwise false
96
+ def module?(uri)
97
+ uri.include?('/modules/')
98
+ end
99
+
100
+ # Returns true if the first URL is the parent of the second URL
101
+ # @param url1 [Aspire::Caching::CacheEntry, String] the first URL
102
+ # @param url2 [Aspire::Caching::CacheEntry, String] the second URL
103
+ # @param api [Aspire::API::LinkedData] the API for generating canonical URLs
104
+ # @param strict [Boolean] if true, the first URL must be a parent of the
105
+ # second URL, otherwise the first URL must be a parent or the same as the
106
+ # second.
107
+ # @return [Boolean] true if the URL has the same parent as this entry
108
+ def parent_url?(url1, url2, api = nil, strict: false)
109
+ u1 = url_for_comparison(url1, api, parsed: true)
110
+ u2 = url_for_comparison(url2, api, parsed: true)
111
+ # Both URLs must have the same parent
112
+ return false unless u1[:type] == u2[:type] && u1[:id] == u2[:id]
113
+ # Non-strict comparison requires only the same parent object
114
+ return true unless strict
115
+ # Strict comparison requires that this entry is a child of the URL
116
+ u1[:child_type].nil? && !u2[:child_type].nil? ? true : false
117
+ end
118
+
119
+ # Returns the components of an object URL
120
+ # @param url [String] the object URL
121
+ # @return [MatchData, nil] the URI components:
122
+ # {
123
+ # tenancy_host: tenancy root (server name),
124
+ # type: type of primary object,
125
+ # id: ID of primary object,
126
+ # child_type: type of child object,
127
+ # child_id: ID of child object
128
+ # }
129
+ def parse_url(url)
130
+ url ? LD_API_URI.match(url) : nil
131
+ end
132
+
133
+ # Returns true if URI is a resource
134
+ # @param uri [String] the URI
135
+ # @return [Boolean] true if the URI is a resource, otherwise false
136
+ def resource?(uri)
137
+ uri.include?('/resources/')
138
+ end
139
+
140
+ # Returns true if URI is a section
141
+ # @param uri [String] the URI
142
+ # @return [Boolean] true if the URI is a section, otherwise false
143
+ def section?(uri)
144
+ uri.include?('/sections/')
145
+ end
146
+
147
+ # Returns a parsed or unparsed URL for comparison
148
+ # @param url [Aspire::Caching::CacheEntry, String] the URL
149
+ # @param api [Aspire::API::LinkedData] the API for generating canonical URLs
150
+ # @param parsed [Boolean] if true, return a parsed URL, otherwise return
151
+ # an unparsed URL string
152
+ # @return [Aspire::Caching::CacheEntry, String] the URL for comparison
153
+ def url_for_comparison(url, api = nil, parsed: false)
154
+ if url.is_a?(MatchData) && parsed
155
+ url
156
+ elsif parsed && url.respond_to?(:parsed_url)
157
+ url.parsed_url
158
+ elsif !parsed && url.respond_to?(url)
159
+ url.url
160
+ else
161
+ result = api.nil? ? url.to_s : api.canonical_url(url.to_s)
162
+ parsed ? parse_url(result) : result
163
+ end
164
+ end
165
+
166
+ # Returns the path from the URL as a relative filename
167
+ def url_path
168
+ # Get the path component of the URL as a relative path
169
+ filename = URI.parse(url).path
170
+ filename.slice!(0) # Remove the leading /
171
+ # Return the path with '.json' extension if not already present
172
+ filename.end_with?('.json') ? filename : "#{filename}.json"
173
+ rescue URI::InvalidComponentError, URI::InvalidURIError
174
+ # Return nil if the URL is invalid
175
+ nil
176
+ end
177
+
178
+ # Returns true if URI is a section
179
+ # @param uri [String] the URI
180
+ # @return [Boolean] true if the URI is a section, otherwise false
181
+ def user?(uri)
182
+ uri.include?('/users/')
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,3 @@
1
+ module Aspire
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,197 @@
1
+ # Support for performing retriable operations
2
+ module Retry
3
+ # Common exceptions suitable for retrying
4
+ module Exceptions
5
+ SOCKET_EXCEPTIONS = [
6
+ Errno::ECONNREFUSED,
7
+ Errno::ECONNRESET,
8
+ Errno::EINTR,
9
+ Errno::EHOSTUNREACH,
10
+ Errno::ENETDOWN,
11
+ Errno::ENETUNREACH,
12
+ Errno::ENOBUFS,
13
+ Errno::ENOSR,
14
+ Errno::ETIMEDOUT,
15
+ IO::WaitReadable
16
+ ].freeze
17
+ end
18
+
19
+ # Retry handlers should raise this exception to stop retry processing and
20
+ # return the return value from the Retry.do method
21
+ class StopRetry < RuntimeError
22
+ attr_accessor :value
23
+ def initialize(value = nil)
24
+ self.value = value
25
+ end
26
+ end
27
+
28
+ # Support for repeatedly calling retriable operations
29
+ class Engine
30
+ attr_accessor :delay
31
+ attr_accessor :exceptions
32
+ attr_accessor :handlers
33
+ attr_accessor :tries
34
+
35
+ # Initialises a new Engine instance
36
+ # @param delay [Float] the default delay before retrying
37
+ # @param exceptions [Hash<Exception, Boolean>] the default retriable
38
+ # exceptions
39
+ # @param handlers [Hash<Exception|Symbol, Proc>] the default exception
40
+ # handlers
41
+ # @param tries [Integer, Proc] the default maximum number of tries or
42
+ # a proc which accepts an Exception and returns true if a retry is allowed
43
+ # or false if not
44
+ # @return [void]
45
+ def initialize(delay: nil, exceptions: nil, handlers: nil, tries: nil)
46
+ self.delay = delay.to_f
47
+ self.exceptions = exceptions || {}
48
+ self.handlers = handlers || {}
49
+ self.tries = tries
50
+ end
51
+
52
+ # Executes the class method do using instance default values
53
+ def do(delay: nil, exceptions: nil, handlers: nil, tries: nil, &block)
54
+ Retry.do(delay: delay || self.delay,
55
+ exceptions: exceptions || self.exceptions,
56
+ handlers: handlers || self.handlers,
57
+ tries: tries || self.tries,
58
+ &block)
59
+ end
60
+ end
61
+
62
+ # Executes the code block until it returns successfully, throws a
63
+ # non-retriable exception or some termination condition is met.
64
+ # @param delay [Float] the number of seconds to wait before retrying.
65
+ # Positive values specify an exact delay, negative values specify a
66
+ # random delay no longer than this value.
67
+ # @param exceptions [Hash<Exception, Boolean>] the hash of retriable
68
+ # exceptions
69
+ # @param handlers [Hash<Exception|Symbol, Proc>] handlers to be invoked
70
+ # when specific exceptions occur. A handler should accept the exception
71
+ # and the number of tries remaining as arguments. It does not need to
72
+ # re-raise its exception argument, but it may throw another exception
73
+ # to prevent a retry.
74
+ # @param tries [Integer] the maximum number of tries
75
+ # @return [Object] the return value of the block
76
+ def self.do(delay: nil, exceptions: nil, handlers: nil, tries: nil)
77
+ yield if block_given?
78
+ rescue StandardError => exception
79
+ # Decrement the tries-remaining count if appropriate
80
+ tries -= 1 if tries.is_a?(Numeric)
81
+ # Handlers may raise StopRetry to force a return value from the method
82
+ # Check if the exception is retriable
83
+ retriable = retry?(exception, exceptions, tries)
84
+ begin
85
+ # Run the exception handler
86
+ # - this will re-raise the exception if it is not retriable
87
+ handle_exception(exception, handlers, tries, retriable)
88
+ # Run the retry handler and retry
89
+ handle_retry(exception, handlers, tries, retriable, delay)
90
+ retry
91
+ rescue StopRetry => exception
92
+ # Force a return value from the handler
93
+ exception.value
94
+ end
95
+ end
96
+
97
+ # Executes a handler for an exception
98
+ # @param exception [Exception] the exception
99
+ # @param handlers [Hash<Exception|Symbol, Proc>] the exception handlers
100
+ # @param tries [Integer] the number of tries remaining
101
+ # @param retriable [Boolean] true if the exception is retriable, false if not
102
+ # @return [Object] the return value of the handler, or nil if no handler
103
+ # was executed
104
+ def self.handle_exception(exception, handlers, tries, retriable)
105
+ # Execute the general exception handler
106
+ handler(exception, handlers, tries, retriable, :all)
107
+ # Execute the exception-specific handler
108
+ handler(exception, handlers, tries, retriable)
109
+ # Re-raise the exception if not retriable
110
+ raise exception unless retriable
111
+ end
112
+
113
+ # Executes the retry handler
114
+ # @param exception [Exception] the exception
115
+ # @param handlers [Hash<Exception|Symbol, Proc>] the exception handlers
116
+ # @param tries [Integer] the number of tries remaining
117
+ # @param retriable [Boolean] true if the exception is retriable, false if not
118
+ # @param delay [Float] the number of seconds to wait before retrying
119
+ def self.handle_retry(exception, handlers, tries, retriable, delay)
120
+ # Wait for the specified delay
121
+ wait(delay) unless delay.zero?
122
+ # Return the result of the retry handler
123
+ handler(exception, handlers, tries, retriable, :retry)
124
+ end
125
+
126
+ # Executes the specified handler
127
+ # @param exception [Exception] the exception
128
+ # @param handlers [Hash<Exception|Symbol, Proc>] the exception handlers
129
+ # @param tries [Integer] the number of tries remaining
130
+ # @param retriable [Boolean] true if the exception is retriable, false if not
131
+ # @param name [Symbol] the handler name, defaults to the exception class
132
+ # @return [Object] the return value of the handler, or nil if no handler
133
+ # was executed
134
+ def self.handler(exception, handlers, tries, retriable, name = nil)
135
+ handler = nil
136
+ if name.nil?
137
+ # Find the handler for the exception class
138
+ handlers.each do |e, h|
139
+ next unless e.is_a?(Class) && exception.is_a?(e)
140
+ handler = h
141
+ break
142
+ end
143
+ # Use the default handler if no match was found
144
+ handler ||= handlers[:default]
145
+ else
146
+ # Use the named handler, do not use the default if not found
147
+ handler = handlers[name]
148
+ end
149
+ handler ? handler.call(exception, tries, retriable) : nil
150
+ end
151
+
152
+ # Returns true if the exception instance is retriable, false if not
153
+ # @param exception [Exception] the exception instance
154
+ # @param tries [Integer, Proc] the number of tries remaining or a proc
155
+ # determining whether tries remain
156
+ # @return [Boolean] true if the exception is retriable, false if not
157
+ def self.retry?(exception, exceptions, tries)
158
+ # Return false if there are no more tries remaining
159
+ return false unless tries_remain?(exception, tries)
160
+ # Return true if the exception matches a retriable exception class
161
+ exceptions.each { |e| return true if exception.is_a?(e) }
162
+ # The exception didn't match any retriable classes
163
+ false
164
+ end
165
+
166
+ # Returns true if there are tries remaining
167
+ # @param exception [Exception] the exception instance
168
+ # @param tries [Integer, Proc] the number of tries remaining or a proc
169
+ # determining whether tries remain
170
+ def self.tries_remain?(exception, tries)
171
+ # If tries is numeric, this is the number of tries remaining
172
+ return false if tries.is_a?(Numeric) && tries.zero?
173
+ # If tries has a #call method, this should return true to allow a retry or
174
+ # false to raise the exception
175
+ return false if tries.respond_to?(:call) && !tries.call(exception)
176
+ # Otherwise allow a retry
177
+ true
178
+ end
179
+
180
+ # Waits for the specified number of seconds. If delay is positive, sleep
181
+ # for that period. If delay is negative, sleep for a random time up to
182
+ # that duration.
183
+ # @param delay [Float] the number of seconds to wait before retrying
184
+ # @return [void]
185
+ def self.wait(delay)
186
+ sleep(delay > 0 ? delay : Random.rand(-delay))
187
+ end
188
+
189
+ class << self
190
+ private :handle_exception
191
+ private :handle_retry
192
+ private :handler
193
+ private :retry?
194
+ private :tries_remain?
195
+ private :wait
196
+ end
197
+ end