elevate 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rvmrc +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +41 -0
- data/Guardfile +9 -0
- data/LICENSE +7 -0
- data/README.md +179 -0
- data/Rakefile +20 -0
- data/app/app_delegate.rb +5 -0
- data/elevate.gemspec +21 -0
- data/lib/elevate.rb +11 -0
- data/lib/elevate/api.rb +33 -0
- data/lib/elevate/callback.rb +13 -0
- data/lib/elevate/dispatcher.rb +53 -0
- data/lib/elevate/dsl.rb +18 -0
- data/lib/elevate/http/base64.rb +9 -0
- data/lib/elevate/http/http_client.rb +100 -0
- data/lib/elevate/http/request.rb +95 -0
- data/lib/elevate/http/response.rb +22 -0
- data/lib/elevate/http/uri.rb +19 -0
- data/lib/elevate/io_coordinator.rb +69 -0
- data/lib/elevate/operation.rb +74 -0
- data/lib/elevate/promise.rb +27 -0
- data/lib/elevate/version.rb +3 -0
- data/spec/api_spec.rb +23 -0
- data/spec/callback_spec.rb +12 -0
- data/spec/dispatcher_spec.rb +40 -0
- data/spec/dsl_spec.rb +21 -0
- data/spec/helpers/target.rb +23 -0
- data/spec/http/http_client_spec.rb +40 -0
- data/spec/http/http_request_spec.rb +103 -0
- data/spec/io_coordinator_spec.rb +44 -0
- data/spec/operation_spec.rb +133 -0
- metadata +157 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
module Elevate
|
2
|
+
module HTTP
|
3
|
+
class HTTPClient
|
4
|
+
def initialize(base_url)
|
5
|
+
@base_url = NSURL.URLWithString(base_url)
|
6
|
+
@credentials = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(path, query={}, &block)
|
10
|
+
issue(:GET, path, nil, query: query, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def post(path, body, &block)
|
14
|
+
issue(:post, path, body, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def put(path, body, &block)
|
18
|
+
issue(:put, path, body, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(path, &block)
|
22
|
+
issue(:delete, path, nil, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_credentials(username, password)
|
26
|
+
@credentials = { username: username, password: password }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def issue(method, path, body, options={}, &block)
|
32
|
+
url = url_for(path)
|
33
|
+
|
34
|
+
options[:headers] ||= {}
|
35
|
+
options[:headers]["Accept"] = "application/json"
|
36
|
+
|
37
|
+
if @credentials
|
38
|
+
options[:credentials] = @credentials
|
39
|
+
end
|
40
|
+
|
41
|
+
if body
|
42
|
+
options[:body] = NSJSONSerialization.dataWithJSONObject(body, options:0, error:nil)
|
43
|
+
options[:headers]["Content-Type"] = "application/json"
|
44
|
+
end
|
45
|
+
|
46
|
+
request = HTTPRequest.new(method, url, options)
|
47
|
+
|
48
|
+
coordinator = IOCoordinator.for_thread
|
49
|
+
|
50
|
+
coordinator.signal_blocked(request) if coordinator
|
51
|
+
response = JSONHTTPResponse.new(request.response)
|
52
|
+
coordinator.signal_unblocked(request) if coordinator
|
53
|
+
|
54
|
+
if response.error == nil && block_given?
|
55
|
+
result = yield response.body
|
56
|
+
puts result.inspect
|
57
|
+
|
58
|
+
result
|
59
|
+
else
|
60
|
+
response
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def url_for(path)
|
65
|
+
path = CFURLCreateStringByAddingPercentEscapes(nil, path.to_s, "[]", ";=&,", KCFStringEncodingUTF8)
|
66
|
+
|
67
|
+
NSURL.URLWithString(path, relativeToURL:@base_url).absoluteString
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class JSONHTTPResponse
|
72
|
+
def initialize(response)
|
73
|
+
@response = response
|
74
|
+
@body = decode(response.body)
|
75
|
+
end
|
76
|
+
|
77
|
+
def decode(data)
|
78
|
+
return nil if data.nil?
|
79
|
+
|
80
|
+
NSJSONSerialization.JSONObjectWithData(data, options:0, error:nil)
|
81
|
+
end
|
82
|
+
|
83
|
+
attr_reader :body
|
84
|
+
|
85
|
+
# TODO: delegate
|
86
|
+
def error
|
87
|
+
@response.error
|
88
|
+
end
|
89
|
+
|
90
|
+
def headers
|
91
|
+
@response.headers
|
92
|
+
end
|
93
|
+
|
94
|
+
def status_code
|
95
|
+
@response.status_code
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Elevate
|
2
|
+
module HTTP
|
3
|
+
# TODO: redirects
|
4
|
+
class HTTPRequest
|
5
|
+
METHODS = [:get, :post, :put, :delete, :patch, :head, :options].freeze
|
6
|
+
QUEUE = NSOperationQueue.alloc.init
|
7
|
+
|
8
|
+
def initialize(method, url, options={})
|
9
|
+
raise ArgumentError, "invalid HTTP method" unless METHODS.include? method.downcase
|
10
|
+
raise ArgumentError, "invalid URL" unless url.start_with? "http"
|
11
|
+
raise ArgumentError, "invalid body type; must be NSData" if options[:body] && ! options[:body].is_a?(NSData)
|
12
|
+
|
13
|
+
unless options.fetch(:query, {}).empty?
|
14
|
+
url += "?" + URI.encode_query(options[:query])
|
15
|
+
end
|
16
|
+
|
17
|
+
@request = NSMutableURLRequest.alloc.init
|
18
|
+
@request.CachePolicy = NSURLRequestReloadIgnoringLocalCacheData
|
19
|
+
@request.HTTPBody = options[:body]
|
20
|
+
@request.HTTPMethod = method
|
21
|
+
@request.URL = NSURL.URLWithString(url)
|
22
|
+
|
23
|
+
headers = options.fetch(:headers, {})
|
24
|
+
|
25
|
+
if credentials = options[:credentials]
|
26
|
+
headers["Authorization"] = get_authorization_header(credentials)
|
27
|
+
end
|
28
|
+
|
29
|
+
headers.each do |key, value|
|
30
|
+
@request.setValue(value.to_s, forHTTPHeaderField:key.to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
@response = HTTPResponse.new
|
34
|
+
|
35
|
+
@connection = nil
|
36
|
+
@promise = Promise.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def cancel
|
40
|
+
return unless started?
|
41
|
+
|
42
|
+
@connection.cancel()
|
43
|
+
@promise.set(nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
def response
|
47
|
+
unless started?
|
48
|
+
start()
|
49
|
+
end
|
50
|
+
|
51
|
+
@promise.get()
|
52
|
+
end
|
53
|
+
|
54
|
+
def start
|
55
|
+
@connection = NSURLConnection.alloc.initWithRequest(@request, delegate:self, startImmediately:false)
|
56
|
+
@connection.setDelegateQueue(QUEUE)
|
57
|
+
@connection.start()
|
58
|
+
end
|
59
|
+
|
60
|
+
def started?
|
61
|
+
@connection != nil
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def connection(connection, didReceiveResponse: response)
|
67
|
+
@response.headers = response.allHeaderFields
|
68
|
+
@response.status_code = response.statusCode
|
69
|
+
end
|
70
|
+
|
71
|
+
def connection(connection, didReceiveData: data)
|
72
|
+
@response.append_data(data)
|
73
|
+
end
|
74
|
+
|
75
|
+
def connection(connection, didFailWithError: error)
|
76
|
+
puts "ERROR: #{error.localizedDescription}"
|
77
|
+
|
78
|
+
@response.error = error
|
79
|
+
@response.freeze
|
80
|
+
|
81
|
+
@promise.set(@response)
|
82
|
+
end
|
83
|
+
|
84
|
+
def connectionDidFinishLoading(connection)
|
85
|
+
@response.freeze
|
86
|
+
|
87
|
+
@promise.set(@response)
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_authorization_header(credentials)
|
91
|
+
"Basic " + Base64.encode("#{credentials[:username]}:#{credentials[:password]}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Elevate
|
2
|
+
module HTTP
|
3
|
+
class HTTPResponse
|
4
|
+
def initialize
|
5
|
+
@body = nil
|
6
|
+
@headers = nil
|
7
|
+
@status_code = nil
|
8
|
+
@error = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def append_data(data)
|
12
|
+
@body ||= NSMutableData.alloc.init
|
13
|
+
@body.appendData(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :body
|
17
|
+
attr_accessor :headers
|
18
|
+
attr_accessor :status_code
|
19
|
+
attr_accessor :error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Elevate
|
2
|
+
module HTTP
|
3
|
+
module URI
|
4
|
+
def self.encode_query(hash)
|
5
|
+
return "" if hash.nil? || hash.empty?
|
6
|
+
|
7
|
+
hash.map do |key, value|
|
8
|
+
"#{URI.escape_query_component(key.to_s)}=#{URI.escape_query_component(value.to_s)}"
|
9
|
+
end.join("&")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.escape_query_component(component)
|
13
|
+
component.gsub(/([^ a-zA-Z0-9_.-]+)/) do
|
14
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
15
|
+
end.tr(' ', '+')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Elevate
|
2
|
+
class IOCoordinator
|
3
|
+
def self.for_thread
|
4
|
+
Thread.current[:io_coordinator]
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@lock = NSLock.alloc.init
|
9
|
+
@blocking_operation = nil
|
10
|
+
@cancelled = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def cancel
|
14
|
+
blocking_operation = nil
|
15
|
+
|
16
|
+
@lock.lock()
|
17
|
+
@cancelled = true
|
18
|
+
blocking_operation = @blocking_operation
|
19
|
+
@lock.unlock()
|
20
|
+
|
21
|
+
if blocking_operation
|
22
|
+
blocking_operation.cancel()
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def cancelled?
|
27
|
+
cancelled = nil
|
28
|
+
|
29
|
+
@lock.lock()
|
30
|
+
cancelled = @cancelled
|
31
|
+
@lock.unlock()
|
32
|
+
|
33
|
+
cancelled
|
34
|
+
end
|
35
|
+
|
36
|
+
def install
|
37
|
+
Thread.current[:io_coordinator] = self
|
38
|
+
end
|
39
|
+
|
40
|
+
def signal_blocked(operation)
|
41
|
+
check_for_cancellation
|
42
|
+
|
43
|
+
@lock.lock()
|
44
|
+
@blocking_operation = operation
|
45
|
+
@lock.unlock()
|
46
|
+
end
|
47
|
+
|
48
|
+
def signal_unblocked(operation)
|
49
|
+
@lock.lock()
|
50
|
+
@blocking_operation = nil
|
51
|
+
@lock.unlock()
|
52
|
+
|
53
|
+
check_for_cancellation
|
54
|
+
end
|
55
|
+
|
56
|
+
def uninstall
|
57
|
+
Thread.current[:io_coordinator] = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def check_for_cancellation
|
63
|
+
raise CancelledError if cancelled?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class CancelledError < StandardError
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Elevate
|
2
|
+
class ElevateOperation < NSOperation
|
3
|
+
def initWithTarget(target)
|
4
|
+
if init()
|
5
|
+
@target = target
|
6
|
+
@coordinator = IOCoordinator.new
|
7
|
+
@dispatcher = Dispatcher.new
|
8
|
+
|
9
|
+
setCompletionBlock(lambda do
|
10
|
+
@target = nil
|
11
|
+
|
12
|
+
@dispatcher.invoke_finished_callback() unless isCancelled()
|
13
|
+
@dispatcher.dispose()
|
14
|
+
end)
|
15
|
+
end
|
16
|
+
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def cancel
|
21
|
+
@coordinator.cancel()
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def dealloc
|
27
|
+
#puts 'dealloc!'
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
details = []
|
34
|
+
details << "<canceled>" if @coordinator.cancelled?
|
35
|
+
details << "@target=#{@target.class.name}"
|
36
|
+
|
37
|
+
"#<#{self.class.name}: #{details.join(" ")}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
def log(line)
|
41
|
+
puts line unless RUBYMOTION_ENV == "test"
|
42
|
+
end
|
43
|
+
|
44
|
+
def main
|
45
|
+
log " START: #{inspect}"
|
46
|
+
|
47
|
+
@coordinator.install()
|
48
|
+
|
49
|
+
begin
|
50
|
+
unless @coordinator.cancelled?
|
51
|
+
@result = @target.execute
|
52
|
+
end
|
53
|
+
|
54
|
+
rescue => e
|
55
|
+
@exception = e
|
56
|
+
end
|
57
|
+
|
58
|
+
@coordinator.uninstall()
|
59
|
+
|
60
|
+
log "FINISH: #{inspect}"
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :exception
|
64
|
+
attr_reader :result
|
65
|
+
|
66
|
+
def on_started=(callback)
|
67
|
+
@dispatcher.on_started = callback
|
68
|
+
end
|
69
|
+
|
70
|
+
def on_finished=(callback)
|
71
|
+
@dispatcher.on_finished = callback
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Elevate
|
2
|
+
class Promise
|
3
|
+
OUTSTANDING = 0
|
4
|
+
FULFILLED = 1
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@lock = NSConditionLock.alloc.initWithCondition(OUTSTANDING)
|
8
|
+
@result = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def get
|
12
|
+
result = nil
|
13
|
+
|
14
|
+
@lock.lockWhenCondition(FULFILLED)
|
15
|
+
result = @result
|
16
|
+
@lock.unlockWithCondition(FULFILLED)
|
17
|
+
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(result)
|
22
|
+
@lock.lockWhenCondition(OUTSTANDING)
|
23
|
+
@result = result
|
24
|
+
@lock.unlockWithCondition(FULFILLED)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Bacon
|
2
|
+
class Context
|
3
|
+
include ::Elevate
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
describe Elevate do
|
8
|
+
describe "#async" do
|
9
|
+
it "runs the specified interactor asynchronously" do
|
10
|
+
|
11
|
+
async Target.new() do
|
12
|
+
on_completed do |operation|
|
13
|
+
@called = true
|
14
|
+
resume
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
wait_max 1.0 do
|
19
|
+
@called.should.be.true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|