elevate 0.3
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.
- 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
|