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
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
elevate (0.3)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
coderay (1.0.8)
|
10
|
+
guard (1.6.2)
|
11
|
+
listen (>= 0.6.0)
|
12
|
+
lumberjack (>= 1.0.2)
|
13
|
+
pry (>= 0.9.10)
|
14
|
+
terminal-table (>= 1.4.3)
|
15
|
+
thor (>= 0.14.6)
|
16
|
+
guard-motion (0.1.1)
|
17
|
+
guard (>= 1.1.0)
|
18
|
+
rake (>= 0.9)
|
19
|
+
listen (0.7.2)
|
20
|
+
lumberjack (1.0.2)
|
21
|
+
method_source (0.8.1)
|
22
|
+
pry (0.9.11.4)
|
23
|
+
coderay (~> 1.0.5)
|
24
|
+
method_source (~> 0.8)
|
25
|
+
slop (~> 3.4)
|
26
|
+
rake (0.9.6)
|
27
|
+
rb-fsevent (0.9.3)
|
28
|
+
slop (3.4.3)
|
29
|
+
terminal-table (1.4.5)
|
30
|
+
thor (0.17.0)
|
31
|
+
webstub (0.3.4)
|
32
|
+
|
33
|
+
PLATFORMS
|
34
|
+
ruby
|
35
|
+
|
36
|
+
DEPENDENCIES
|
37
|
+
elevate!
|
38
|
+
guard-motion (~> 0.1.1)
|
39
|
+
rake (>= 0.9.0)
|
40
|
+
rb-fsevent (~> 0.9.1)
|
41
|
+
webstub (~> 0.3.3)
|
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013 Matt Green
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
Elevate
|
2
|
+
======
|
3
|
+
How do we convey the intent of our application?
|
4
|
+
|
5
|
+
Background
|
6
|
+
-----------
|
7
|
+
iOS applications employ the MVC architecture to delineate responsibilities:
|
8
|
+
|
9
|
+
* Models represent the entities important to our app
|
10
|
+
* Views display those entities
|
11
|
+
* Controllers react to user input, adjusting the model
|
12
|
+
|
13
|
+
However, iOS view controllers seem to attract complexity: not only do they coordinate models, but they also coordinate boundary objects (persistence mechanisms, backend APIs) and view-related concerns. This conflation of responsibilities shrouds the domain logic (that is, what your application does), making it harder to reason about and test. Pure model-related logic can be tested easily on its own, the difficulty arises when models interact with boundaries. Asynchronous behavior only makes it worse.
|
14
|
+
|
15
|
+
Elevate posits that the essence of your system are the use cases. They constitute the unique value that your application delivers. Their correctness is too important to be mixed in with presentation-level concerns. This results in a bit more code (one class per use case), but it allows the view controller to be relentlessly focused on view-related concerns.
|
16
|
+
|
17
|
+
Extracting use cases into their own class has several benefits:
|
18
|
+
|
19
|
+
* Consolidates domain and boundary interactions (read: all non-UI code) to a single conceptual unit
|
20
|
+
* Clarifies the intent of the code, both within the view controller and the use case
|
21
|
+
* Simplifies IO within the use case, allowing it feel blocking, while remaining interruptible (see below)
|
22
|
+
* Eases testing, allow you to employ either mock-based tests or acceptance tests
|
23
|
+
|
24
|
+
Implementation
|
25
|
+
--------------
|
26
|
+
Use cases are executed one at a time in a single, global `NSOperationQueue`. Each use case invocation is contained within a `NSOperation` subclass called `ElevateOperation`. `ElevateOperation` sets up an execution environment enabling compliant IO libraries to use traditional blocking control flows, rather than the traditional asynchronous style employed by iOS. Calls may be interrupted by invoking `cancel` on the `ElevateOperation`, triggering a `CancelledError` to be raised within the use case.
|
27
|
+
|
28
|
+
The `Elevate::HTTP` module wraps NSURLRequest to work with this control flow. (Unfortunately, most iOS HTTP libraries do not work well with this paradigm.)
|
29
|
+
|
30
|
+
Example
|
31
|
+
------------
|
32
|
+
Synchronizing data between a remote API and a local DB:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class SyncArtists < Action
|
36
|
+
def execute
|
37
|
+
stale_artists.each do |stale_artist|
|
38
|
+
artist = api.get_artist(stale_artist.name)
|
39
|
+
tracked_artists.add(artist)
|
40
|
+
end
|
41
|
+
|
42
|
+
tracked_artists.all
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def stale_artists
|
47
|
+
stale = []
|
48
|
+
|
49
|
+
current = api.get_artists()
|
50
|
+
current.each do |artist|
|
51
|
+
if stale?(artist)
|
52
|
+
stale << artist
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
stale
|
57
|
+
end
|
58
|
+
|
59
|
+
def stale?(artist)
|
60
|
+
existing = tracked_artists.find_by_name(artist.name)
|
61
|
+
if existing.nil?
|
62
|
+
return true
|
63
|
+
end
|
64
|
+
|
65
|
+
existing.updated_at < artist.updated_at
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
Notice the use case (`SyncArtists`) describes the algorithm at a high level. It is not concerned with the UI, and it depends on abstractions.
|
71
|
+
|
72
|
+
The view controller retains a similar focus. In fact, it is completely ignorant of how the sync algorithm operates. It only knows that it will return a list of artists to display:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class ArtistsViewController < UITableViewController
|
76
|
+
include Elevate
|
77
|
+
|
78
|
+
def artists
|
79
|
+
@artists ||= []
|
80
|
+
end
|
81
|
+
|
82
|
+
def artists=(artists)
|
83
|
+
@artists = artists
|
84
|
+
view.reloadData()
|
85
|
+
end
|
86
|
+
|
87
|
+
def viewWillAppear(animated)
|
88
|
+
super
|
89
|
+
|
90
|
+
async SyncArtists.new do
|
91
|
+
on_completed do |operation|
|
92
|
+
self.artists = operation.result
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
Requirements
|
100
|
+
------------
|
101
|
+
* **iOS 6.x and higher** (due to `setDelegateQueue` being [horribly broken](http://openradar.appspot.com/10529053) on iOS 5.x.)
|
102
|
+
|
103
|
+
Installation
|
104
|
+
------------
|
105
|
+
Update your Gemfile:
|
106
|
+
|
107
|
+
gem "elevate", "~> 0.3.0"
|
108
|
+
|
109
|
+
Bundle:
|
110
|
+
|
111
|
+
$ bundle install
|
112
|
+
|
113
|
+
Usage
|
114
|
+
-----
|
115
|
+
|
116
|
+
Write a use case. Use case classes must respond to `execute`. Anything returned from `execute` is made available to the controller callbacks:
|
117
|
+
```ruby
|
118
|
+
class TrackArtist < Action
|
119
|
+
def initialize(artist_name)
|
120
|
+
@artist_name = artist_name
|
121
|
+
end
|
122
|
+
|
123
|
+
def execute
|
124
|
+
unless registration.completed?
|
125
|
+
user = api.register()
|
126
|
+
registration.save(user)
|
127
|
+
end
|
128
|
+
|
129
|
+
artist = api.track(@artist_name)
|
130
|
+
tracked_artists.add(artist)
|
131
|
+
|
132
|
+
artist
|
133
|
+
end
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
Include the module in your view controller:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class ArtistsSearchViewController < UIViewController
|
141
|
+
include Elevate
|
142
|
+
```
|
143
|
+
|
144
|
+
Execute a use case:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
async TrackArtist.new(artist_name) do
|
148
|
+
on_started do |operation|
|
149
|
+
SVProgressHUD.showWithStatus("Adding...", maskType:SVProgressHUDMaskTypeGradient)
|
150
|
+
end
|
151
|
+
|
152
|
+
# operation.result contains the return value of #execute
|
153
|
+
# operation.exception contains the raised exception (if any)
|
154
|
+
on_completed do |operation|
|
155
|
+
SVProgressHUD.dismiss()
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
Caveats
|
161
|
+
---------
|
162
|
+
* **DSL is not finalized**
|
163
|
+
* Sending CoreData entities across threads is dangerous
|
164
|
+
* The callback DSL is clunky to try to avoid retain issues
|
165
|
+
* Must use Elevate's HTTP client instead of other iOS networking libs
|
166
|
+
* No way to report progress (idea: `execute` could yield status information via optional block)
|
167
|
+
|
168
|
+
Inspiration
|
169
|
+
-----------
|
170
|
+
* [PoEAA: Transaction Script](http://martinfowler.com/eaaCatalog/transactionScript.html)
|
171
|
+
* [The Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
172
|
+
* [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture)
|
173
|
+
* [Architecture: The Lost Years](http://www.youtube.com/watch?v=WpkDN78P884)
|
174
|
+
* [Android SDK's AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html)
|
175
|
+
* Go (asynchronous IO done correctly)
|
176
|
+
|
177
|
+
License
|
178
|
+
---------
|
179
|
+
MIT License
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$:.unshift("/Library/RubyMotion/lib")
|
2
|
+
|
3
|
+
require 'motion/project'
|
4
|
+
require "bundler/gem_tasks"
|
5
|
+
require "bundler/setup"
|
6
|
+
Bundler.require :default
|
7
|
+
|
8
|
+
require 'webstub'
|
9
|
+
|
10
|
+
$:.unshift("./lib/")
|
11
|
+
require './lib/elevate'
|
12
|
+
|
13
|
+
Motion::Project::App.setup do |app|
|
14
|
+
app.name = "elevate"
|
15
|
+
|
16
|
+
if ENV["DEFAULT_PROVISIONING_PROFILE"]
|
17
|
+
app.provisioning_profile = ENV["DEFAULT_PROVISIONING_PROFILE"]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
data/app/app_delegate.rb
ADDED
data/elevate.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/elevate/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Matt Green"]
|
6
|
+
gem.email = ["mattgreenrocks@gmail.com"]
|
7
|
+
gem.description = "Distill the essence of your RubyMotion app"
|
8
|
+
gem.summary = "Distill the essence of your RubyMotion app"
|
9
|
+
gem.homepage = "http://github.com/mattgreen/elevate"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "elevate"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = Elevate::VERSION
|
16
|
+
|
17
|
+
gem.add_development_dependency 'rake', '>= 0.9.0'
|
18
|
+
gem.add_development_dependency 'guard-motion', '~> 0.1.1'
|
19
|
+
gem.add_development_dependency 'rb-fsevent', '~> 0.9.1'
|
20
|
+
gem.add_development_dependency 'webstub', '~> 0.3.3'
|
21
|
+
end
|
data/lib/elevate.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'elevate/version'
|
2
|
+
|
3
|
+
unless defined?(Motion::Project::Config)
|
4
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
5
|
+
end
|
6
|
+
|
7
|
+
Motion::Project::App.setup do |app|
|
8
|
+
Dir.glob(File.join(File.dirname(__FILE__), "elevate/**/*.rb")).each do |file|
|
9
|
+
app.files.unshift(file)
|
10
|
+
end
|
11
|
+
end
|
data/lib/elevate/api.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Elevate
|
2
|
+
def async(target, &block)
|
3
|
+
with_operation(target, block) do |operation|
|
4
|
+
queue.addOperation(operation)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def queue
|
11
|
+
Dispatch.once do
|
12
|
+
$elevate_queue = NSOperationQueue.alloc.init
|
13
|
+
$elevate_queue.maxConcurrentOperationCount = 1
|
14
|
+
end
|
15
|
+
|
16
|
+
$elevate_queue
|
17
|
+
end
|
18
|
+
|
19
|
+
def with_operation(target, dsl_block, &block)
|
20
|
+
operation = ElevateOperation.alloc.initWithTarget(target)
|
21
|
+
|
22
|
+
if dsl_block
|
23
|
+
dsl = DSL.new(&dsl_block)
|
24
|
+
|
25
|
+
operation.on_started = Callback.new(self, operation, dsl.started_callback) if dsl.started_callback
|
26
|
+
operation.on_finished = Callback.new(self, operation, dsl.finished_callback) if dsl.finished_callback
|
27
|
+
end
|
28
|
+
|
29
|
+
yield operation
|
30
|
+
|
31
|
+
operation
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Elevate
|
2
|
+
class Dispatcher
|
3
|
+
def initialize
|
4
|
+
@on_finished = nil
|
5
|
+
@on_started = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def dispose
|
9
|
+
return if @on_started.nil? && @on_finished.nil?
|
10
|
+
|
11
|
+
# Callbacks must be released on the main thread, because they may contain a strong
|
12
|
+
# reference to a UIKit component. See "The Deallocation Problem" for more info.
|
13
|
+
unless NSThread.isMainThread
|
14
|
+
self.performSelectorOnMainThread(:dispose, withObject: nil, waitUntilDone: true)
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
@on_started = nil
|
19
|
+
@on_finished = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def invoke_finished_callback
|
23
|
+
invoke(:@on_finished)
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_finished=(callback)
|
27
|
+
@on_finished = callback
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_started=(callback)
|
31
|
+
@on_started = callback
|
32
|
+
|
33
|
+
Dispatch::Queue.main.async do
|
34
|
+
invoke(:@on_started)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def invoke(callback_name)
|
41
|
+
unless NSThread.isMainThread
|
42
|
+
self.performSelectorOnMainThread(:"invoke:", withObject: callback_name, waitUntilDone: true)
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
if callback = instance_variable_get(callback_name)
|
47
|
+
callback.call()
|
48
|
+
|
49
|
+
instance_variable_set(callback_name, nil)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/elevate/dsl.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Elevate
|
2
|
+
class DSL
|
3
|
+
def initialize(&block)
|
4
|
+
instance_eval(&block)
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :started_callback
|
8
|
+
attr_reader :finished_callback
|
9
|
+
|
10
|
+
def on_started(&block)
|
11
|
+
@started_callback = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_completed(&block)
|
15
|
+
@finished_callback = block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|