elevate 0.5.0 → 0.6.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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/README.md +46 -32
- data/Rakefile +11 -1
- data/elevate.gemspec +1 -3
- data/lib/elevate/dsl.rb +15 -0
- data/lib/elevate/{api.rb → elevate.rb} +12 -0
- data/lib/elevate/http.rb +25 -0
- data/lib/elevate/http/errors.rb +2 -0
- data/lib/elevate/http/http_client.rb +2 -61
- data/lib/elevate/http/request.rb +89 -9
- data/lib/elevate/http/response.rb +94 -4
- data/lib/elevate/http/uri.rb +29 -0
- data/lib/elevate/io_coordinator.rb +76 -11
- data/lib/elevate/operation.rb +134 -1
- data/lib/elevate/task_context.rb +6 -0
- data/lib/elevate/version.rb +1 -1
- data/spec/elevate_spec.rb +159 -0
- data/spec/http/{http_request_spec.rb → request_spec.rb} +13 -13
- data/spec/http_spec.rb +195 -0
- data/spec/io_coordinator_spec.rb +8 -0
- metadata +19 -54
- data/spec/api_spec.rb +0 -72
@@ -1,22 +1,112 @@
|
|
1
1
|
module Elevate
|
2
2
|
module HTTP
|
3
|
-
|
3
|
+
# Encapsulates a response received from a HTTP server.
|
4
|
+
#
|
5
|
+
# @api public
|
6
|
+
class Response
|
4
7
|
def initialize
|
5
8
|
@body = nil
|
6
9
|
@headers = nil
|
7
10
|
@status_code = nil
|
8
11
|
@error = nil
|
12
|
+
@raw_body = nil
|
13
|
+
@url = nil
|
9
14
|
end
|
10
15
|
|
16
|
+
# Appends a chunk of data to the body.
|
17
|
+
#
|
18
|
+
# @api private
|
11
19
|
def append_data(data)
|
12
|
-
@
|
13
|
-
@
|
20
|
+
@raw_body ||= NSMutableData.alloc.init
|
21
|
+
@raw_body.appendData(data)
|
14
22
|
end
|
15
23
|
|
16
|
-
|
24
|
+
# Returns the body of the response.
|
25
|
+
#
|
26
|
+
# If the body is JSON-encoded, it will be decoded and returned.
|
27
|
+
#
|
28
|
+
# @return [NSData, Hash, Array, nil]
|
29
|
+
# response body, if any. If the response is JSON-encoded, the decoded body.
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def body
|
33
|
+
@body ||= begin
|
34
|
+
if json?
|
35
|
+
NSJSONSerialization.JSONObjectWithData(@raw_body, options: 0, error: nil)
|
36
|
+
else
|
37
|
+
@raw_body
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Freezes this instance, making it immutable.
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
def freeze
|
46
|
+
body
|
47
|
+
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
# Forwards unknown methods to +body+, enabling this object to behave like +body+.
|
52
|
+
#
|
53
|
+
# This only occurs if +body+ is a Ruby collection.
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def method_missing(m, *args, &block)
|
57
|
+
return super unless json?
|
58
|
+
|
59
|
+
body.send(m, *args, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Handles missing method queries, allowing +body+ masquerading.
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
def respond_to_missing?(m, include_private = false)
|
66
|
+
return false unless json?
|
67
|
+
|
68
|
+
body.respond_to_missing?(m, include_private)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the HTTP headers
|
72
|
+
#
|
73
|
+
# @return [Hash]
|
74
|
+
# returned headers
|
75
|
+
#
|
76
|
+
# @api public
|
17
77
|
attr_accessor :headers
|
78
|
+
|
79
|
+
# Returns the HTTP status code
|
80
|
+
#
|
81
|
+
# @return [Integer]
|
82
|
+
# status code of the response
|
83
|
+
#
|
84
|
+
# @api public
|
18
85
|
attr_accessor :status_code
|
86
|
+
|
19
87
|
attr_accessor :error
|
88
|
+
|
89
|
+
# Returns the raw body
|
90
|
+
#
|
91
|
+
# @return [NSData]
|
92
|
+
# response body
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
attr_reader :raw_body
|
96
|
+
|
97
|
+
# Returns the URL
|
98
|
+
#
|
99
|
+
# @return [String]
|
100
|
+
# URL of the response
|
101
|
+
#
|
102
|
+
# @api public
|
103
|
+
attr_accessor :url
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def json?
|
108
|
+
headers && headers["Content-Type"] =~ %r{application/json}
|
109
|
+
end
|
20
110
|
end
|
21
111
|
end
|
22
112
|
end
|
data/lib/elevate/http/uri.rb
CHANGED
@@ -1,6 +1,35 @@
|
|
1
1
|
module Elevate
|
2
2
|
module HTTP
|
3
3
|
module URI
|
4
|
+
def self.encode_www_form(enum)
|
5
|
+
enum.map do |k,v|
|
6
|
+
if v.nil?
|
7
|
+
encode_www_form_component(k)
|
8
|
+
elsif v.respond_to?(:to_ary)
|
9
|
+
v.to_ary.map do |w|
|
10
|
+
str = encode_www_form_component(k)
|
11
|
+
|
12
|
+
if w.nil?
|
13
|
+
str
|
14
|
+
else
|
15
|
+
str + "=" + encode_www_form_component(w)
|
16
|
+
end
|
17
|
+
end.join('&')
|
18
|
+
else
|
19
|
+
encode_www_form_component(k) + "=" + encode_www_form_component(v)
|
20
|
+
end
|
21
|
+
end.join('&')
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.encode_www_form_component(str)
|
25
|
+
# From AFNetworking :)
|
26
|
+
CFURLCreateStringByAddingPercentEscapes(nil,
|
27
|
+
str,
|
28
|
+
"[].",
|
29
|
+
":/?&=;+!@\#$()~',*",
|
30
|
+
CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding))
|
31
|
+
end
|
32
|
+
|
4
33
|
def self.encode_query(hash)
|
5
34
|
return "" if hash.nil? || hash.empty?
|
6
35
|
|
@@ -1,58 +1,114 @@
|
|
1
1
|
module Elevate
|
2
|
+
# Implements task cancellation.
|
3
|
+
#
|
4
|
+
# Compliant I/O mechanisms (such as HTTP requests) register long-running
|
5
|
+
# operations with a well-known instance of this class. When a cancellation
|
6
|
+
# request is received from another thread, the long-running operation is
|
7
|
+
# cancelled.
|
2
8
|
class IOCoordinator
|
9
|
+
# Retrieves the current IOCoordinator for this thread.
|
10
|
+
#
|
11
|
+
# @return [IOCoordinator,nil]
|
12
|
+
# IOCoordinator previously installed to this thread
|
13
|
+
#
|
14
|
+
# @api public
|
3
15
|
def self.for_thread
|
4
16
|
Thread.current[:io_coordinator]
|
5
17
|
end
|
6
18
|
|
19
|
+
# Initializes a new IOCoordinator with the default state.
|
20
|
+
#
|
21
|
+
# @api private
|
7
22
|
def initialize
|
8
23
|
@lock = NSLock.alloc.init
|
9
24
|
@blocking_operation = nil
|
10
25
|
@cancelled = false
|
26
|
+
@exception_class = nil
|
11
27
|
end
|
12
28
|
|
13
|
-
|
29
|
+
# Cancels the I/O operation (if any), raising an exception of type
|
30
|
+
# +exception_class+ in the worker thread.
|
31
|
+
#
|
32
|
+
# If the thread is not currently blocked, then set a flag requesting cancellation.
|
33
|
+
#
|
34
|
+
# @return [void]
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
def cancel(exception_class = CancelledError)
|
14
38
|
blocking_operation = nil
|
15
39
|
|
16
|
-
@lock.lock
|
40
|
+
@lock.lock
|
17
41
|
@cancelled = true
|
42
|
+
@exception_class = exception_class
|
18
43
|
blocking_operation = @blocking_operation
|
19
|
-
@lock.unlock
|
44
|
+
@lock.unlock
|
20
45
|
|
21
46
|
if blocking_operation
|
22
|
-
blocking_operation.cancel
|
47
|
+
blocking_operation.cancel
|
23
48
|
end
|
24
49
|
end
|
25
50
|
|
51
|
+
# Returns the cancelled flag.
|
52
|
+
#
|
53
|
+
# @return [Boolean]
|
54
|
+
# true if this coordinator has been +cancel+ed previously.
|
55
|
+
#
|
56
|
+
# @api private
|
26
57
|
def cancelled?
|
27
58
|
cancelled = nil
|
28
59
|
|
29
|
-
@lock.lock
|
60
|
+
@lock.lock
|
30
61
|
cancelled = @cancelled
|
31
|
-
@lock.unlock
|
62
|
+
@lock.unlock
|
32
63
|
|
33
64
|
cancelled
|
34
65
|
end
|
35
66
|
|
67
|
+
# Installs this IOCoordinator to a well-known thread-local.
|
68
|
+
#
|
69
|
+
# @return [void]
|
70
|
+
#
|
71
|
+
# @api private
|
36
72
|
def install
|
37
73
|
Thread.current[:io_coordinator] = self
|
38
74
|
end
|
39
75
|
|
76
|
+
# Marks the specified operation as one that will potentially block the
|
77
|
+
# worker thread for a significant amount of time.
|
78
|
+
#
|
79
|
+
# @param operation [#cancel]
|
80
|
+
# operation responsible for blocking
|
81
|
+
#
|
82
|
+
# @return [void]
|
83
|
+
#
|
84
|
+
# @api public
|
40
85
|
def signal_blocked(operation)
|
41
86
|
check_for_cancellation
|
42
87
|
|
43
|
-
@lock.lock
|
88
|
+
@lock.lock
|
44
89
|
@blocking_operation = operation
|
45
|
-
@lock.unlock
|
90
|
+
@lock.unlock
|
46
91
|
end
|
47
92
|
|
93
|
+
# Signals that the specified operation has completed, and is no longer
|
94
|
+
# responsible for blocking the worker thread.
|
95
|
+
#
|
96
|
+
# @return [void]
|
97
|
+
#
|
98
|
+
# @api public
|
48
99
|
def signal_unblocked(operation)
|
49
|
-
@lock.lock
|
100
|
+
@lock.lock
|
50
101
|
@blocking_operation = nil
|
51
|
-
@lock.unlock
|
102
|
+
@lock.unlock
|
52
103
|
|
53
104
|
check_for_cancellation
|
54
105
|
end
|
55
106
|
|
107
|
+
# Removes the thread-local for the calling thread.
|
108
|
+
#
|
109
|
+
# @return [void]
|
110
|
+
#
|
111
|
+
# @api private
|
56
112
|
def uninstall
|
57
113
|
Thread.current[:io_coordinator] = nil
|
58
114
|
end
|
@@ -60,10 +116,19 @@ module Elevate
|
|
60
116
|
private
|
61
117
|
|
62
118
|
def check_for_cancellation
|
63
|
-
raise
|
119
|
+
raise @exception_class if cancelled?
|
64
120
|
end
|
65
121
|
end
|
66
122
|
|
123
|
+
# Raised when a task is cancelled.
|
124
|
+
#
|
125
|
+
# @api public
|
67
126
|
class CancelledError < StandardError
|
68
127
|
end
|
128
|
+
|
129
|
+
# Raised when a task's timeout expires
|
130
|
+
#
|
131
|
+
# @api public
|
132
|
+
class TimeoutError < CancelledError
|
133
|
+
end
|
69
134
|
end
|
data/lib/elevate/operation.rb
CHANGED
@@ -1,9 +1,19 @@
|
|
1
1
|
module Elevate
|
2
|
+
# Executes an Elevate task, firing callbacks along the way.
|
3
|
+
#
|
2
4
|
class ElevateOperation < NSOperation
|
5
|
+
# Designated initializer.
|
6
|
+
#
|
7
|
+
# @return [ElevateOperation]
|
8
|
+
# newly initialized instance
|
9
|
+
#
|
10
|
+
# @api private
|
3
11
|
def initWithTarget(target, args:args)
|
4
|
-
if init
|
12
|
+
if init
|
5
13
|
@coordinator = IOCoordinator.new
|
6
14
|
@context = TaskContext.new(args, &target)
|
15
|
+
@timeout_callback = nil
|
16
|
+
@timer = nil
|
7
17
|
@update_callback = nil
|
8
18
|
@finish_callback = nil
|
9
19
|
|
@@ -14,6 +24,13 @@ module Elevate
|
|
14
24
|
|
15
25
|
Dispatch::Queue.main.sync do
|
16
26
|
@context = nil
|
27
|
+
|
28
|
+
if @timer
|
29
|
+
@timer.invalidate
|
30
|
+
@timer = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
@timeout_callback = nil
|
17
34
|
@update_callback = nil
|
18
35
|
@finish_callback = nil
|
19
36
|
end
|
@@ -23,12 +40,23 @@ module Elevate
|
|
23
40
|
self
|
24
41
|
end
|
25
42
|
|
43
|
+
# Cancels the currently running task.
|
44
|
+
#
|
45
|
+
# @return [void]
|
46
|
+
#
|
47
|
+
# @api public
|
26
48
|
def cancel
|
27
49
|
@coordinator.cancel
|
28
50
|
|
29
51
|
super
|
30
52
|
end
|
31
53
|
|
54
|
+
# Returns information about this task.
|
55
|
+
#
|
56
|
+
# @return [String]
|
57
|
+
# String suitable for debugging purposes.
|
58
|
+
#
|
59
|
+
# @api public
|
32
60
|
def inspect
|
33
61
|
details = []
|
34
62
|
details << "<canceled>" if @coordinator.cancelled?
|
@@ -36,10 +64,20 @@ module Elevate
|
|
36
64
|
"#<#{self.class.name}: #{details.join(" ")}>"
|
37
65
|
end
|
38
66
|
|
67
|
+
# Logs debugging information in certain configurations.
|
68
|
+
#
|
69
|
+
# @return [void]
|
70
|
+
#
|
71
|
+
# @api private
|
39
72
|
def log(line)
|
40
73
|
puts line unless RUBYMOTION_ENV == "test"
|
41
74
|
end
|
42
75
|
|
76
|
+
# Runs the specified task.
|
77
|
+
#
|
78
|
+
# @return [void]
|
79
|
+
#
|
80
|
+
# @api private
|
43
81
|
def main
|
44
82
|
log " START: #{inspect}"
|
45
83
|
|
@@ -54,6 +92,10 @@ module Elevate
|
|
54
92
|
|
55
93
|
rescue Exception => e
|
56
94
|
@exception = e
|
95
|
+
|
96
|
+
if e.is_a?(TimeoutError)
|
97
|
+
@timeout_callback.call if @timeout_callback
|
98
|
+
end
|
57
99
|
end
|
58
100
|
|
59
101
|
@coordinator.uninstall
|
@@ -61,13 +103,49 @@ module Elevate
|
|
61
103
|
log "FINISH: #{inspect}"
|
62
104
|
end
|
63
105
|
|
106
|
+
# Returns the exception that terminated this task, if any.
|
107
|
+
#
|
108
|
+
# If the task has not finished, returns nil.
|
109
|
+
#
|
110
|
+
# @return [Exception, nil]
|
111
|
+
# exception that terminated the task
|
112
|
+
#
|
113
|
+
# @api public
|
64
114
|
attr_reader :exception
|
115
|
+
|
116
|
+
# Returns the result of the task block.
|
117
|
+
#
|
118
|
+
# If the task has not finished, returns nil.
|
119
|
+
#
|
120
|
+
# @return [Object, nil]
|
121
|
+
# result of the task block
|
122
|
+
#
|
123
|
+
# @api public
|
65
124
|
attr_reader :result
|
66
125
|
|
126
|
+
# Sets the callback to be run upon completion of this task. Do not call
|
127
|
+
# this method after the task has started.
|
128
|
+
#
|
129
|
+
# @param callback [Elevate::Callback]
|
130
|
+
# completion callback
|
131
|
+
#
|
132
|
+
# @return [void]
|
133
|
+
#
|
134
|
+
# @api private
|
67
135
|
def on_finish=(callback)
|
68
136
|
@finish_callback = callback
|
69
137
|
end
|
70
138
|
|
139
|
+
# Sets the callback to be run when this task is queued.
|
140
|
+
#
|
141
|
+
# Do not call this method after the task has started.
|
142
|
+
#
|
143
|
+
# @param callback [Elevate::Callback]
|
144
|
+
# callback to be invoked when queueing
|
145
|
+
#
|
146
|
+
# @return [void]
|
147
|
+
#
|
148
|
+
# @api private
|
71
149
|
def on_start=(callback)
|
72
150
|
start_callback = callback
|
73
151
|
start_callback.retain
|
@@ -78,8 +156,63 @@ module Elevate
|
|
78
156
|
end
|
79
157
|
end
|
80
158
|
|
159
|
+
# Handles timeout expiration.
|
160
|
+
#
|
161
|
+
# @return [void]
|
162
|
+
#
|
163
|
+
# @api private
|
164
|
+
def on_timeout_elapsed(timer)
|
165
|
+
@coordinator.cancel(TimeoutError)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Sets the timeout callback.
|
169
|
+
#
|
170
|
+
# @param callback [Elevate::Callback]
|
171
|
+
# callback to run on timeout
|
172
|
+
#
|
173
|
+
# @return [void]
|
174
|
+
#
|
175
|
+
# @api private
|
176
|
+
def on_timeout=(callback)
|
177
|
+
@timeout_callback = callback
|
178
|
+
end
|
179
|
+
|
180
|
+
# Sets the update callback, which is invoked for any yield statements in the task.
|
181
|
+
#
|
182
|
+
# @param callback [Elevate::Callback]
|
183
|
+
# @return [void]
|
184
|
+
#
|
185
|
+
# @api private
|
81
186
|
def on_update=(callback)
|
82
187
|
@update_callback = callback
|
83
188
|
end
|
189
|
+
|
190
|
+
# Sets the timeout interval for this task.
|
191
|
+
#
|
192
|
+
# The timeout starts when the task is queued, not when it is started.
|
193
|
+
#
|
194
|
+
# @param interval [Fixnum]
|
195
|
+
# seconds to allow for task completion
|
196
|
+
#
|
197
|
+
# @return [void]
|
198
|
+
#
|
199
|
+
# @api private
|
200
|
+
def timeout=(interval)
|
201
|
+
@timer = NSTimer.scheduledTimerWithTimeInterval(interval,
|
202
|
+
target: self,
|
203
|
+
selector: :"on_timeout_elapsed:",
|
204
|
+
userInfo: nil,
|
205
|
+
repeats: false)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns whether this task timed out.
|
209
|
+
#
|
210
|
+
# @return [Boolean]
|
211
|
+
# true if this task was aborted due to a time out.
|
212
|
+
#
|
213
|
+
# @api public
|
214
|
+
def timed_out?
|
215
|
+
@exception.class == TimeoutError
|
216
|
+
end
|
84
217
|
end
|
85
218
|
end
|