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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 960a321bb02005b2ff94c974ae37afbd9905b027
|
4
|
+
data.tar.gz: c5a04645e4e674e4d7285ab4c705a8b52c0561e2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f56b43560ddf1be5e5c1cd9cc940bf15cc44719442e5d28fb7ff7022958f67e699245ef4698e6f1ec34210f00ede2e7e57f01f07d9ec144ef8f3eab1d51af700
|
7
|
+
data.tar.gz: 64a8d83ae84ec87a4daf9f78c40f220b1763bc15a6d6370e3ff142b9619acf6e718571e033085a2675c175e031f0ddee2f1d962b40149e2c0c51a11e3410ab6f
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -3,20 +3,19 @@ Elevate
|
|
3
3
|
|
4
4
|
Stop scattering your domain logic across your view controller. Consolidate it to a single conceptual unit with Elevate.
|
5
5
|
|
6
|
-
[![Code Climate](https://codeclimate.com/github/mattgreen/elevate.png)](https://codeclimate.com/github/mattgreen/elevate) [![Travis](https://api.travis-ci.org/mattgreen/elevate.png)](https://travis-ci.org/mattgreen/elevate)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/mattgreen/elevate.png)](https://codeclimate.com/github/mattgreen/elevate) [![Travis](https://api.travis-ci.org/mattgreen/elevate.png)](https://travis-ci.org/mattgreen/elevate) [![Gem Version](https://badge.fury.io/rb/elevate.png)](http://badge.fury.io/rb/elevate)
|
7
7
|
|
8
8
|
Example
|
9
9
|
-------
|
10
10
|
|
11
11
|
```ruby
|
12
12
|
@login_task = async username: username.text, password: password.text do
|
13
|
+
# This block runs on a background thread.
|
13
14
|
task do
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
# which blocks until the request returns, yet can be interrupted.
|
19
|
-
credentials = API.login(@username, @password)
|
15
|
+
# @username and @password correspond to the Hash keys provided to async.
|
16
|
+
args = { username: @username, password: @password }
|
17
|
+
|
18
|
+
credentials = Elevate::HTTP.post(LOGIN_URL, args)
|
20
19
|
if credentials
|
21
20
|
UserRegistration.store(credentials.username, credentials.token)
|
22
21
|
end
|
@@ -57,7 +56,7 @@ end
|
|
57
56
|
|
58
57
|
Background
|
59
58
|
-----------
|
60
|
-
Many iOS apps have fairly simple domain logic that is obscured by several programming 'taxes':
|
59
|
+
Many iOS/OS X apps have fairly simple domain logic that is obscured by several programming 'taxes':
|
61
60
|
|
62
61
|
* UI management
|
63
62
|
* asynchronous network requests
|
@@ -65,23 +64,21 @@ Many iOS apps have fairly simple domain logic that is obscured by several progra
|
|
65
64
|
|
66
65
|
These are necessary to ensure a good user experience, but they splinter your domain logic (that is, what your application does) through your view controller. Gross.
|
67
66
|
|
68
|
-
Elevate is a mini task queue for your
|
67
|
+
Elevate is a mini task queue for your app, much like Resque or Sidekiq. Rather than defining part of an operation to run on the UI thread, and a CPU-intensive portion on a background thread, Elevate is designed so you run the *entire* operation in the background, and receive notifications at various times. This has a nice side effect of consolidating all the interaction for a particular task to one place. The UI code is cleanly isolated from the non-UI code. When your tasks become complex, you can elect to extract them out to a service object.
|
69
68
|
|
70
|
-
In a sense, Elevate is almost a control-flow library: it bends the rules of
|
69
|
+
In a sense, Elevate is almost a control-flow library: it bends the rules of app development a bit to ensure that the unique value your application provides is as clear as possible.
|
71
70
|
|
72
|
-
|
71
|
+
Documentation
|
73
72
|
--------
|
73
|
+
- [Tutorial](https://github.com/mattgreen/elevate/wiki/Tutorial) - start here
|
74
74
|
|
75
|
-
|
76
|
-
* Actor-style concurrency
|
77
|
-
* Simplifies asynchronous HTTP requests when used with Elevate::HTTP
|
78
|
-
* Built atop of NSOperationQueue
|
75
|
+
- [Wiki](https://github.com/mattgreen/elevate/wiki)
|
79
76
|
|
80
77
|
Installation
|
81
78
|
------------
|
82
79
|
Update your Gemfile:
|
83
80
|
|
84
|
-
gem "elevate", "~> 0.
|
81
|
+
gem "elevate", "~> 0.6.0"
|
85
82
|
|
86
83
|
Bundle:
|
87
84
|
|
@@ -98,21 +95,15 @@ class ArtistsSearchViewController < UIViewController
|
|
98
95
|
```
|
99
96
|
|
100
97
|
Launch an async task with the `async` method:
|
101
|
-
|
102
|
-
* Pass all the data the task needs to operate (such as credentials or search terms) in to the `async` method.
|
103
|
-
* Define a block that contains a `task` block. The `task` block should contain all of your non-UI code. It will be run on a background thread. Any data passed into the `async` method will be available as instance variables, keyed by the provided hash key.
|
104
|
-
* Optionally:
|
105
|
-
* Define an `on_start` block to be run when the task starts
|
106
|
-
* Define an `on_finish` block to be run when the task finishes
|
107
|
-
* Define an `on_update` block to be called any time the task calls yield (useful for relaying status information back during long operations)
|
108
|
-
|
109
|
-
All of the `on_` blocks are called on the UI thread. `on_start` is guaranteed to precede `on_update` and `on_finish`.
|
110
|
-
|
111
98
|
```ruby
|
112
99
|
@track_task = async artist: searchBar.text do
|
113
100
|
task do
|
114
|
-
|
101
|
+
response = Elevate::HTTP.get("http://example.com/artists", query: { artist: @artist })
|
102
|
+
|
103
|
+
artist = Artist.from_hash(response)
|
115
104
|
ArtistDB.update(artist)
|
105
|
+
|
106
|
+
response["name"]
|
116
107
|
end
|
117
108
|
|
118
109
|
on_start do
|
@@ -125,13 +116,36 @@ All of the `on_` blocks are called on the UI thread. `on_start` is guaranteed to
|
|
125
116
|
end
|
126
117
|
```
|
127
118
|
|
128
|
-
|
119
|
+
If you might need to cancel the task later, call `cancel` on the object returned by `async`:
|
120
|
+
```ruby
|
121
|
+
@track_task.cancel
|
122
|
+
```
|
123
|
+
|
124
|
+
Timeouts
|
125
|
+
--------
|
126
|
+
Elevate 0.6.0 includes support for timeouts. Timeouts are declared using the `timeout` method within the `async` block. They start when an operation is queued, and automatically abort the task when the duration passes. If the task takes longer than the specified duration, the `on_timeout` callback is run.
|
129
127
|
|
130
|
-
|
128
|
+
Example:
|
131
129
|
|
132
|
-
|
133
|
-
|
134
|
-
|
130
|
+
```ruby
|
131
|
+
async do
|
132
|
+
timeout 0.1
|
133
|
+
|
134
|
+
task do
|
135
|
+
Elevate::HTTP.get("http://example.com/")
|
136
|
+
end
|
137
|
+
|
138
|
+
on_timeout do
|
139
|
+
puts 'timed out'
|
140
|
+
end
|
141
|
+
|
142
|
+
on_finish do |result, exception|
|
143
|
+
puts 'completed!'
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
```
|
135
149
|
|
136
150
|
Caveats
|
137
151
|
---------
|
data/Rakefile
CHANGED
@@ -1,6 +1,16 @@
|
|
1
1
|
$:.unshift("/Library/RubyMotion/lib")
|
2
2
|
|
3
|
-
|
3
|
+
begin
|
4
|
+
if ENV['osx']
|
5
|
+
require 'motion/project/template/osx'
|
6
|
+
else
|
7
|
+
require 'motion/project/template/ios'
|
8
|
+
end
|
9
|
+
|
10
|
+
rescue LoadError
|
11
|
+
require 'motion/project'
|
12
|
+
end
|
13
|
+
|
4
14
|
require "bundler/gem_tasks"
|
5
15
|
require "bundler/setup"
|
6
16
|
Bundler.require :default
|
data/elevate.gemspec
CHANGED
@@ -15,7 +15,5 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.version = Elevate::VERSION
|
16
16
|
|
17
17
|
gem.add_development_dependency 'rake', '>= 0.9.0'
|
18
|
-
gem.add_development_dependency '
|
19
|
-
gem.add_development_dependency 'rb-fsevent', '~> 0.9.1'
|
20
|
-
gem.add_development_dependency 'webstub', '~> 0.3.3'
|
18
|
+
gem.add_development_dependency 'webstub', '~> 0.6.0'
|
21
19
|
end
|
data/lib/elevate/dsl.rb
CHANGED
@@ -9,6 +9,9 @@ module Elevate
|
|
9
9
|
attr_reader :task_callback
|
10
10
|
attr_reader :update_callback
|
11
11
|
|
12
|
+
attr_reader :timeout_callback
|
13
|
+
attr_reader :timeout_interval
|
14
|
+
|
12
15
|
def on_finish(&block)
|
13
16
|
raise "on_finish blocks must accept two parameters" unless block.arity == 2
|
14
17
|
|
@@ -21,6 +24,12 @@ module Elevate
|
|
21
24
|
@start_callback = block
|
22
25
|
end
|
23
26
|
|
27
|
+
def on_timeout(&block)
|
28
|
+
raise "on_timeout blocks must accept zero parameters" unless block.arity == 0
|
29
|
+
|
30
|
+
@timeout_callback = block
|
31
|
+
end
|
32
|
+
|
24
33
|
def on_update(&block)
|
25
34
|
@update_callback = block
|
26
35
|
end
|
@@ -30,5 +39,11 @@ module Elevate
|
|
30
39
|
|
31
40
|
@task_callback = block
|
32
41
|
end
|
42
|
+
|
43
|
+
def timeout(seconds)
|
44
|
+
raise "timeout argument must be a number" unless seconds.is_a?(Numeric)
|
45
|
+
|
46
|
+
@timeout_interval = seconds
|
47
|
+
end
|
33
48
|
end
|
34
49
|
end
|
@@ -1,4 +1,13 @@
|
|
1
1
|
module Elevate
|
2
|
+
# Launches a new asynchronous task.
|
3
|
+
#
|
4
|
+
# @param args [Hash]
|
5
|
+
# input arguments for the task, available to the +task+ block
|
6
|
+
#
|
7
|
+
# @return [NSOperation]
|
8
|
+
# operation representing this task
|
9
|
+
#
|
10
|
+
# @api public
|
2
11
|
def async(args = {}, &block)
|
3
12
|
with_operation(args, block) do |operation|
|
4
13
|
queue.addOperation(operation)
|
@@ -25,6 +34,9 @@ module Elevate
|
|
25
34
|
operation.on_start = Callback.new(self, dsl.start_callback) if dsl.start_callback
|
26
35
|
operation.on_finish = Callback.new(self, dsl.finish_callback) if dsl.finish_callback
|
27
36
|
operation.on_update = Callback.new(self, dsl.update_callback) if dsl.update_callback
|
37
|
+
operation.on_timeout= Callback.new(self, dsl.timeout_callback) if dsl.timeout_callback
|
38
|
+
|
39
|
+
operation.timeout = dsl.timeout_interval if dsl.timeout_interval
|
28
40
|
|
29
41
|
yield operation
|
30
42
|
|
data/lib/elevate/http.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Elevate
|
2
|
+
module HTTP
|
3
|
+
Request::METHODS.each do |m|
|
4
|
+
define_singleton_method(m) do |url, options = {}|
|
5
|
+
coordinator = IOCoordinator.for_thread
|
6
|
+
|
7
|
+
request = Request.new(m, url, options)
|
8
|
+
|
9
|
+
coordinator.signal_blocked(request) if coordinator
|
10
|
+
response = request.response
|
11
|
+
coordinator.signal_unblocked(request) if coordinator
|
12
|
+
|
13
|
+
if error = response.error
|
14
|
+
if error.code == NSURLErrorNotConnectedToInternet
|
15
|
+
raise OfflineError, error
|
16
|
+
else
|
17
|
+
raise RequestError, response.error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
response
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/elevate/http/errors.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Elevate
|
2
2
|
module HTTP
|
3
|
+
# Raised when a request could not be completed.
|
3
4
|
class RequestError < RuntimeError
|
4
5
|
def initialize(error)
|
5
6
|
super(error.localizedDescription)
|
@@ -10,6 +11,7 @@ module HTTP
|
|
10
11
|
attr_reader :code
|
11
12
|
end
|
12
13
|
|
14
|
+
# Raised when the internet connection is offline.
|
13
15
|
class OfflineError < RequestError
|
14
16
|
end
|
15
17
|
end
|
@@ -7,7 +7,7 @@ module HTTP
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def get(path, query={}, &block)
|
10
|
-
issue(:
|
10
|
+
issue(:get, path, nil, query: query, &block)
|
11
11
|
end
|
12
12
|
|
13
13
|
def post(path, body, &block)
|
@@ -43,37 +43,7 @@ module HTTP
|
|
43
43
|
options[:headers]["Content-Type"] = "application/json"
|
44
44
|
end
|
45
45
|
|
46
|
-
|
47
|
-
raise_error(response.error) if response.error
|
48
|
-
|
49
|
-
response = JSONHTTPResponse.new(response)
|
50
|
-
if response.error == nil && block_given?
|
51
|
-
result = yield response.body
|
52
|
-
|
53
|
-
result
|
54
|
-
else
|
55
|
-
response
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def raise_error(error)
|
60
|
-
if error.code == -1009
|
61
|
-
raise OfflineError, error
|
62
|
-
else
|
63
|
-
raise RequestError, response.error
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def send_request(method, url, options)
|
68
|
-
request = HTTPRequest.new(method, url, options)
|
69
|
-
|
70
|
-
coordinator = IOCoordinator.for_thread
|
71
|
-
|
72
|
-
coordinator.signal_blocked(request) if coordinator
|
73
|
-
response = request.response
|
74
|
-
coordinator.signal_unblocked(request) if coordinator
|
75
|
-
|
76
|
-
response
|
46
|
+
Elevate::HTTP.send(method, url, options)
|
77
47
|
end
|
78
48
|
|
79
49
|
def url_for(path)
|
@@ -82,34 +52,5 @@ module HTTP
|
|
82
52
|
NSURL.URLWithString(path, relativeToURL:@base_url).absoluteString
|
83
53
|
end
|
84
54
|
end
|
85
|
-
|
86
|
-
class JSONHTTPResponse
|
87
|
-
def initialize(response)
|
88
|
-
@response = response
|
89
|
-
@body = decode(response.body)
|
90
|
-
end
|
91
|
-
|
92
|
-
def decode(data)
|
93
|
-
return nil if data.nil?
|
94
|
-
|
95
|
-
NSJSONSerialization.JSONObjectWithData(data, options:0, error:nil)
|
96
|
-
end
|
97
|
-
|
98
|
-
attr_reader :body
|
99
|
-
|
100
|
-
# TODO: delegate
|
101
|
-
def error
|
102
|
-
@response.error
|
103
|
-
end
|
104
|
-
|
105
|
-
def headers
|
106
|
-
@response.headers
|
107
|
-
end
|
108
|
-
|
109
|
-
def status_code
|
110
|
-
@response.status_code
|
111
|
-
end
|
112
|
-
|
113
|
-
end
|
114
55
|
end
|
115
56
|
end
|
data/lib/elevate/http/request.rb
CHANGED
@@ -1,9 +1,46 @@
|
|
1
1
|
module Elevate
|
2
2
|
module HTTP
|
3
|
-
#
|
4
|
-
|
3
|
+
# Encapsulates a HTTP request.
|
4
|
+
#
|
5
|
+
# +NSURLConnection+ is responsible for fulfilling the request. The response
|
6
|
+
# is buffered in memory as it is received, and made available through the
|
7
|
+
# +response+ method.
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
class Request
|
5
11
|
METHODS = [:get, :post, :put, :delete, :patch, :head, :options].freeze
|
6
12
|
|
13
|
+
# Initializes a HTTP request with the specified parameters.
|
14
|
+
#
|
15
|
+
# @param [String] method
|
16
|
+
# HTTP method to use
|
17
|
+
# @param [String] url
|
18
|
+
# URL to load
|
19
|
+
# @param [Hash] options
|
20
|
+
# Options to use
|
21
|
+
#
|
22
|
+
# @option options [Hash] :query
|
23
|
+
# Hash to construct the query string from.
|
24
|
+
# @option options [Hash] :headers
|
25
|
+
# Headers to append to the request.
|
26
|
+
# @option options [Hash] :credentials
|
27
|
+
# Credentials to be used with HTTP Basic Authentication. Must have a
|
28
|
+
# +:username+ and/or +:password+ key.
|
29
|
+
# @option options [NSData] :body
|
30
|
+
# Raw bytes to use as the body.
|
31
|
+
# @option options [Hash,Array] :json
|
32
|
+
# Hash/Array to be JSON-encoded as the request body. Sets the
|
33
|
+
# +Content-Type+ header to +application/json+.
|
34
|
+
# @option options [Hash] :form
|
35
|
+
# Hash to be form encoded as the request body. Sets the +Content-Type+
|
36
|
+
# header to +application/x-www-form-urlencoded+.
|
37
|
+
#
|
38
|
+
# @raise [ArgumentError]
|
39
|
+
# if an illegal HTTP method is used
|
40
|
+
# @raise [ArgumentError]
|
41
|
+
# if the URL does not start with 'http'
|
42
|
+
# @raise [ArgumentError]
|
43
|
+
# if the +:body+ option is not an instance of +NSData+
|
7
44
|
def initialize(method, url, options={})
|
8
45
|
raise ArgumentError, "invalid HTTP method" unless METHODS.include? method.downcase
|
9
46
|
raise ArgumentError, "invalid URL" unless url.start_with? "http"
|
@@ -13,6 +50,16 @@ module HTTP
|
|
13
50
|
url += "?" + URI.encode_query(options[:query])
|
14
51
|
end
|
15
52
|
|
53
|
+
options[:headers] ||= {}
|
54
|
+
|
55
|
+
if root = options.delete(:json)
|
56
|
+
options[:body] = NSJSONSerialization.dataWithJSONObject(root, options: 0, error: nil)
|
57
|
+
options[:headers]["Content-Type"] = "application/json"
|
58
|
+
elsif root = options.delete(:form)
|
59
|
+
options[:body] = URI.encode_www_form(root).dataUsingEncoding(NSASCIIStringEncoding)
|
60
|
+
options[:headers]["Content-Type"] ||= "application/x-www-form-urlencoded"
|
61
|
+
end
|
62
|
+
|
16
63
|
@request = NSMutableURLRequest.alloc.init
|
17
64
|
@request.CachePolicy = NSURLRequestReloadIgnoringLocalCacheData
|
18
65
|
@request.HTTPBody = options[:body]
|
@@ -29,14 +76,22 @@ module HTTP
|
|
29
76
|
@request.setValue(value.to_s, forHTTPHeaderField:key.to_s)
|
30
77
|
end
|
31
78
|
|
32
|
-
@response =
|
79
|
+
@response = Response.new
|
80
|
+
@response.url = url
|
33
81
|
|
34
82
|
@connection = nil
|
35
83
|
@promise = Promise.new
|
36
84
|
end
|
37
85
|
|
86
|
+
# Cancels an in-flight request.
|
87
|
+
#
|
88
|
+
# This method is safe to call from any thread.
|
89
|
+
#
|
90
|
+
# @return [void]
|
91
|
+
#
|
92
|
+
# @api public
|
38
93
|
def cancel
|
39
|
-
return unless
|
94
|
+
return unless sent?
|
40
95
|
|
41
96
|
NetworkThread.cancel(@connection)
|
42
97
|
ActivityIndicator.instance.hide
|
@@ -44,22 +99,41 @@ module HTTP
|
|
44
99
|
@promise.fulfill(nil)
|
45
100
|
end
|
46
101
|
|
102
|
+
# Returns a response to this request, sending it if necessary
|
103
|
+
#
|
104
|
+
# This method blocks the calling thread, unless interrupted.
|
105
|
+
#
|
106
|
+
# @return [Elevate::HTTP::Response, nil]
|
107
|
+
# response to this request, or nil, if this request was canceled
|
108
|
+
#
|
109
|
+
# @api public
|
47
110
|
def response
|
48
|
-
unless
|
49
|
-
|
111
|
+
unless sent?
|
112
|
+
send
|
50
113
|
end
|
51
114
|
|
52
115
|
@promise.value
|
53
116
|
end
|
54
117
|
|
55
|
-
|
118
|
+
# Sends this request. The caller is not blocked.
|
119
|
+
#
|
120
|
+
# @return [void]
|
121
|
+
#
|
122
|
+
# @api public
|
123
|
+
def send
|
56
124
|
@connection = NSURLConnection.alloc.initWithRequest(@request, delegate:self, startImmediately:false)
|
57
125
|
|
58
126
|
NetworkThread.start(@connection)
|
59
127
|
ActivityIndicator.instance.show
|
60
128
|
end
|
61
129
|
|
62
|
-
|
130
|
+
# Returns true if this request is in-flight
|
131
|
+
#
|
132
|
+
# @return [Boolean]
|
133
|
+
# true if this request is in-flight
|
134
|
+
#
|
135
|
+
# @api public
|
136
|
+
def sent?
|
63
137
|
@connection != nil
|
64
138
|
end
|
65
139
|
|
@@ -75,7 +149,7 @@ module HTTP
|
|
75
149
|
end
|
76
150
|
|
77
151
|
def connection(connection, didFailWithError: error)
|
78
|
-
puts "ERROR: #{error.localizedDescription} (code: #{error.code})"
|
152
|
+
puts "ERROR: #{error.localizedDescription} (code: #{error.code})" unless RUBYMOTION_ENV == "test"
|
79
153
|
|
80
154
|
@response.error = error
|
81
155
|
@response.freeze
|
@@ -93,6 +167,12 @@ module HTTP
|
|
93
167
|
@promise.fulfill(@response)
|
94
168
|
end
|
95
169
|
|
170
|
+
def connection(connection, willSendRequest: request, redirectResponse: response)
|
171
|
+
@response.url = request.URL.absoluteString
|
172
|
+
|
173
|
+
request
|
174
|
+
end
|
175
|
+
|
96
176
|
def get_authorization_header(credentials)
|
97
177
|
"Basic " + Base64.encode("#{credentials[:username]}:#{credentials[:password]}")
|
98
178
|
end
|