elevate 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|