elevate 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ pkg
4
4
  resources/*.nib
5
5
  resources/*.momd
6
6
  resources/*.storyboardc
7
+ .rvmrc
data/.travis.yml ADDED
@@ -0,0 +1 @@
1
+ language: objective-c
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- elevate (0.3.3)
4
+ elevate (0.4.0)
5
5
 
6
6
  GEM
7
- remote: http://rubygems.org/
7
+ remote: https://rubygems.org/
8
8
  specs:
9
9
  coderay (1.0.8)
10
10
  guard (1.6.2)
data/README.md CHANGED
@@ -1,111 +1,77 @@
1
- Elevate
1
+ Elevate [![Code Climate](https://codeclimate.com/github/mattgreen/elevate.png)](https://codeclimate.com/github/mattgreen/elevate)
2
2
  ======
3
- How do we convey the intent of our application?
4
3
 
5
- **Status:** beta quality. Feedback desired!
6
-
7
- [![Code Climate](https://codeclimate.com/github/mattgreen/elevate.png)](https://codeclimate.com/github/mattgreen/elevate)
8
-
9
-
10
- Background
11
- -----------
12
- iOS applications employ the MVC architecture to delineate responsibilities:
13
-
14
- * Models represent the entities important to our app
15
- * Views display those entities
16
- * Controllers react to user input, adjusting the model
17
-
18
- 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.
19
-
20
- 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.
21
-
22
- Extracting use cases into their own class has several benefits:
23
-
24
- * Consolidates domain and boundary interactions (read: all non-UI code) to a single conceptual unit
25
- * Clarifies the intent of the code, both within the view controller and the use case
26
- * Simplifies IO within the use case, allowing it feel blocking, while remaining interruptible (see below)
27
- * Eases testing, allow you to employ either mock-based tests or acceptance tests
28
-
29
- Implementation
30
- --------------
31
- 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.
32
-
33
- The `Elevate::HTTP` module wraps NSURLRequest to work with this control flow. (Unfortunately, most iOS HTTP libraries do not work well with this paradigm.)
4
+ Stop scattering your domain logic across your view controller. Consolidate it to a single conceptual unit with Elevate.
34
5
 
35
6
  Example
36
- ------------
37
- Synchronizing data between a remote API and a local DB:
7
+ -------
38
8
 
39
9
  ```ruby
40
- class SyncArtists < Action
41
- def execute
42
- stale_artists.each do |stale_artist|
43
- artist = api.get_artist(stale_artist.name)
44
- tracked_artists.add(artist)
10
+ @login_task = async username: username.text, password: password.text do
11
+ task do
12
+ # This block runs on a background thread.
13
+ #
14
+ # The @username and @password instance variables correspond to the args
15
+ # passed into async. API is a thin wrapper class over Elevate::HTTP,
16
+ # which blocks until the request returns, yet can be interrupted.
17
+ credentials = API.login(@username, @password)
18
+ if credentials
19
+ UserRegistration.store(credentials.username, credentials.token)
45
20
  end
46
21
 
47
- tracked_artists.all
22
+ # Return value of block is passed back to on_completed
23
+ credentials != nil
48
24
  end
49
25
 
50
- private
51
- def stale_artists
52
- stale = []
53
-
54
- current = api.get_artists()
55
- current.each do |artist|
56
- if stale?(artist)
57
- stale << artist
58
- end
59
- end
60
-
61
- stale
26
+ on_started do
27
+ # This block runs on the UI thread after the operation has been queued.
28
+ SVProgressHUD.showWithStatus("Logging In...")
62
29
  end
63
30
 
64
- def stale?(artist)
65
- existing = tracked_artists.find_by_name(artist.name)
66
- if existing.nil?
67
- return true
68
- end
31
+ on_completed do |result, exception|
32
+ # This block runs on the UI thread after the task block has finished.
33
+ SVProgressHUD.dismiss
69
34
 
70
- existing.updated_at < artist.updated_at
35
+ if exception == nil
36
+ if result
37
+ alert("Logged in successfully!")
38
+ else
39
+ alert("Invalid username/password!")
40
+ end
41
+ else
42
+ alert(exception)
43
+ end
71
44
  end
72
45
  end
73
46
  ```
74
47
 
75
- Notice the use case (`SyncArtists`) describes the algorithm at a high level. It is not concerned with the UI, and it depends on abstractions.
48
+ Background
49
+ -----------
50
+ Many iOS apps have fairly simple domain logic that is obscured by several programming 'taxes':
76
51
 
77
- 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:
52
+ * UI management
53
+ * asynchronous network requests
54
+ * I/O-heavy operations, such as storing large datasets to disk
78
55
 
79
- ```ruby
80
- class ArtistsViewController < UITableViewController
81
- include Elevate
56
+ 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.
82
57
 
83
- def artists
84
- @artists ||= []
85
- end
58
+ Elevate is a mini task queue for your iOS 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 when it starts and finishes. 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.
86
59
 
87
- def artists=(artists)
88
- @artists = artists
89
- view.reloadData()
90
- end
60
+ In a sense, Elevate is almost a control-flow library: it bends the rules of iOS development a bit to ensure that the unique value your application provides is as clear as possible. This is most apparent with how Elevate handles network I/O: it provides a blocking HTTP client built from NSURLRequest for use within your tasks. This lets you write your tasks in a simple, blocking manner, while letting Elevate handle concerns relating to cancellation, and errors.
91
61
 
92
- def viewWillAppear(animated)
93
- super
62
+ Features
63
+ --------
94
64
 
95
- async SyncArtists.new do
96
- on_completed do |operation|
97
- self.artists = operation.result
98
- end
99
- end
100
- end
101
- end
102
- ```
65
+ * Small, beautiful DSL for describing your tasks
66
+ * Actor-style concurrency
67
+ * Simplifies asynchronous HTTP requests when used with Elevate::HTTP
68
+ * Built atop of NSOperationQueue
103
69
 
104
70
  Installation
105
71
  ------------
106
72
  Update your Gemfile:
107
73
 
108
- gem "elevate", "~> 0.3.0"
74
+ gem "elevate", "~> 0.4.0"
109
75
 
110
76
  Bundle:
111
77
 
@@ -114,27 +80,6 @@ Bundle:
114
80
  Usage
115
81
  -----
116
82
 
117
- Write a use case. Use case classes must respond to `execute`. Anything returned from `execute` is made available to the controller callbacks:
118
- ```ruby
119
- class TrackArtist < Action
120
- def initialize(artist_name)
121
- @artist_name = artist_name
122
- end
123
-
124
- def execute
125
- unless registration.completed?
126
- user = api.register()
127
- registration.save(user)
128
- end
129
-
130
- artist = api.track(@artist_name)
131
- tracked_artists.add(artist)
132
-
133
- artist
134
- end
135
- end
136
- ```
137
-
138
83
  Include the module in your view controller:
139
84
 
140
85
  ```ruby
@@ -142,39 +87,50 @@ class ArtistsSearchViewController < UIViewController
142
87
  include Elevate
143
88
  ```
144
89
 
145
- Execute a use case:
90
+ Launch an async task with the `async` method:
91
+
92
+ * Pass all the data the task needs to operate (such as credentials or search terms) in to the `async` method.
93
+ * 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.
94
+ * Optionally, define `on_started` and `on_completed` blocks to run as the task starts and finishes. These are run in the UI thread, and should contain all of your UI code.
146
95
 
147
96
  ```ruby
148
- async TrackArtist.new(artist_name) do
149
- on_started do |operation|
150
- SVProgressHUD.showWithStatus("Adding...", maskType:SVProgressHUDMaskTypeGradient)
97
+ @track_task = async artist: searchBar.text do
98
+ task do
99
+ artist = API.track(@artist)
100
+ ArtistDB.update(artist)
101
+ end
102
+
103
+ on_started do
104
+ SVProgressHUD.showWithStatus("Adding...")
151
105
  end
152
106
 
153
- # operation.result contains the return value of #execute
154
- # operation.exception contains the raised exception (if any)
155
- on_completed do |operation|
156
- SVProgressHUD.dismiss()
107
+ on_completed do |result, exception|
108
+ SVProgressHUD.dismiss
157
109
  end
158
110
  end
159
111
  ```
160
112
 
113
+ To cancel a task (like when the view controller is being dismissed), call `cancel` on the task returned by the `async` method. This causes a `CancelledError` to be raised within the task itself, which is handled by the Elevate runtime. This also prevents any callbacks you have defined from running.
114
+
115
+ **NOTE: Within tasks, do not access the UI or containing view controller! It is extremely dangerous to do so. You must pass data into the `async` method to use it safely.**
116
+
117
+ To Do
118
+ -----
119
+ * Need ability to set timeout for tasks
120
+ * More thought on the semantics
121
+
161
122
  Caveats
162
123
  ---------
163
- * **DSL is not finalized**
164
- * Sending CoreData entities across threads is dangerous
165
- * The callback DSL is clunky to try to avoid retain issues
166
124
  * Must use Elevate's HTTP client instead of other iOS networking libs
167
125
  * No way to report progress (idea: `execute` could yield status information via optional block)
168
126
 
169
127
  Inspiration
170
128
  -----------
171
- * [PoEAA: Transaction Script](http://martinfowler.com/eaaCatalog/transactionScript.html)
172
- * [The Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
173
129
  * [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture)
174
- * [Architecture: The Lost Years](http://www.youtube.com/watch?v=WpkDN78P884)
175
130
  * [Android SDK's AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html)
176
131
  * Go (asynchronous IO done correctly)
177
132
 
178
133
  License
179
134
  ---------
180
135
  MIT License
136
+
data/lib/elevate/api.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Elevate
2
- def async(target, &block)
3
- with_operation(target, block) do |operation|
2
+ def async(args = {}, &block)
3
+ with_operation(args, block) do |operation|
4
4
  queue.addOperation(operation)
5
5
  end
6
6
  end
@@ -16,15 +16,14 @@ module Elevate
16
16
  $elevate_queue
17
17
  end
18
18
 
19
- def with_operation(target, dsl_block, &block)
20
- operation = ElevateOperation.alloc.initWithTarget(target)
19
+ def with_operation(args, dsl_block, &block)
20
+ dsl = DSL.new(&dsl_block)
21
21
 
22
- if dsl_block
23
- dsl = DSL.new(&dsl_block)
22
+ raise "No task block specified!" unless dsl.task_callback
24
23
 
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
24
+ operation = ElevateOperation.alloc.initWithTarget(dsl.task_callback, args: args)
25
+ operation.on_started = Callback.new(self, dsl.started_callback) if dsl.started_callback
26
+ operation.on_finished = Callback.new(self, dsl.finished_callback) if dsl.finished_callback
28
27
 
29
28
  yield operation
30
29
 
@@ -1,13 +1,22 @@
1
1
  module Elevate
2
2
  class Callback
3
- def initialize(context, operation, block)
4
- @context = context
5
- @operation = operation
3
+ def initialize(controller, block)
4
+ @controller = controller
6
5
  @block = block
7
6
  end
8
7
 
9
- def call
10
- @context.instance_exec(@operation, &@block)
8
+ def call(*args)
9
+ if NSThread.isMainThread
10
+ invoke(*args)
11
+ else
12
+ Dispatch::Queue.main.sync { invoke(*args) }
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def invoke(*args)
19
+ @controller.instance_exec(*args, &@block)
11
20
  end
12
21
  end
13
22
  end
data/lib/elevate/dsl.rb CHANGED
@@ -6,12 +6,23 @@ module Elevate
6
6
 
7
7
  attr_reader :started_callback
8
8
  attr_reader :finished_callback
9
+ attr_reader :task_callback
10
+
11
+ def task(&block)
12
+ raise "task blocks must accept zero parameters" unless block.arity == 0
13
+
14
+ @task_callback = block
15
+ end
9
16
 
10
17
  def on_started(&block)
18
+ raise "on_started blocks must accept zero parameters" unless block.arity == 0
19
+
11
20
  @started_callback = block
12
21
  end
13
22
 
14
23
  def on_completed(&block)
24
+ raise "on_completed blocks must accept two parameters" unless block.arity == 2
25
+
15
26
  @finished_callback = block
16
27
  end
17
28
  end
@@ -1,16 +1,32 @@
1
1
  module Elevate
2
+ class Context
3
+ def initialize(args, &block)
4
+ metaclass = class << self; self; end
5
+ metaclass.send(:define_method, :execute, &block)
6
+
7
+ args.each do |key, value|
8
+ instance_variable_set("@#{key}", value)
9
+ end
10
+ end
11
+ end
12
+
2
13
  class ElevateOperation < NSOperation
3
- def initWithTarget(target)
14
+ def initWithTarget(target, args:args)
4
15
  if init()
5
- @target = target
6
16
  @coordinator = IOCoordinator.new
7
- @dispatcher = Dispatcher.new
17
+ @target = target
18
+ @context = Context.new(args, &target)
19
+ @finished_callback = nil
8
20
 
9
21
  setCompletionBlock(lambda do
10
- @target = nil
11
-
12
- @dispatcher.invoke_finished_callback() unless isCancelled()
13
- @dispatcher.dispose()
22
+ if @finished_callback
23
+ @finished_callback.call(@result, @exception) unless isCancelled
24
+ end
25
+
26
+ Dispatch::Queue.main.sync do
27
+ @target = nil
28
+ @finished_callback = nil
29
+ end
14
30
  end)
15
31
  end
16
32
 
@@ -18,13 +34,7 @@ module Elevate
18
34
  end
19
35
 
20
36
  def cancel
21
- @coordinator.cancel()
22
-
23
- super
24
- end
25
-
26
- def dealloc
27
- #puts 'dealloc!'
37
+ @coordinator.cancel
28
38
 
29
39
  super
30
40
  end
@@ -44,18 +54,18 @@ module Elevate
44
54
  def main
45
55
  log " START: #{inspect}"
46
56
 
47
- @coordinator.install()
57
+ @coordinator.install
48
58
 
49
59
  begin
50
60
  unless @coordinator.cancelled?
51
- @result = @target.execute
61
+ @result = @context.execute
52
62
  end
53
63
 
54
64
  rescue Exception => e
55
65
  @exception = e
56
66
  end
57
67
 
58
- @coordinator.uninstall()
68
+ @coordinator.uninstall
59
69
 
60
70
  log "FINISH: #{inspect}"
61
71
  end
@@ -64,11 +74,17 @@ module Elevate
64
74
  attr_reader :result
65
75
 
66
76
  def on_started=(callback)
67
- @dispatcher.on_started = callback
77
+ started_callback = callback
78
+ started_callback.retain
79
+
80
+ Dispatch::Queue.main.async do
81
+ started_callback.call unless isCancelled
82
+ started_callback.release
83
+ end
68
84
  end
69
85
 
70
86
  def on_finished=(callback)
71
- @dispatcher.on_finished = callback
87
+ @finished_callback = callback
72
88
  end
73
89
  end
74
90
  end
@@ -1,3 +1,3 @@
1
1
  module Elevate
2
- VERSION = "0.3.3"
2
+ VERSION = "0.4.0"
3
3
  end
data/spec/api_spec.rb CHANGED
@@ -6,11 +6,14 @@ end
6
6
 
7
7
  describe Elevate do
8
8
  describe "#async" do
9
- it "runs the specified interactor asynchronously" do
9
+ it "runs the specified task asynchronously" do
10
+ async do
11
+ task do
12
+ true
13
+ end
10
14
 
11
- async Target.new() do
12
- on_completed do |operation|
13
- @called = true
15
+ on_completed do |result, exception|
16
+ @called = result
14
17
  resume
15
18
  end
16
19
  end
@@ -19,5 +22,22 @@ describe Elevate do
19
22
  @called.should.be.true
20
23
  end
21
24
  end
25
+
26
+ it "passes provided args to the task as instance variables" do
27
+ async name: "harry" do
28
+ task do
29
+ @name
30
+ end
31
+
32
+ on_completed do |name, exception|
33
+ @result = name
34
+ resume
35
+ end
36
+ end
37
+
38
+ wait_max 1.0 do
39
+ @result.should == "harry"
40
+ end
41
+ end
22
42
  end
23
43
  end
@@ -1,12 +1,22 @@
1
1
  describe Elevate::Callback do
2
2
  describe "#call" do
3
- it "invokes the block using instance_*" do
4
- callback = Elevate::Callback.new(self, 42, lambda { |v| @value = v })
5
- callback.call
3
+ describe "on the main thread" do
4
+ it "invokes the block within the provided context" do
5
+ callback = Elevate::Callback.new(self, lambda { |v| @value = v })
6
+ callback.call(42)
6
7
 
7
- @value.should == 42
8
+ @value.should == 42
9
+ end
8
10
  end
9
11
 
12
+ describe "on a background thread" do
13
+ it "invokes the block within the provided context on the main thread" do
14
+ callback = Elevate::Callback.new(self, lambda { @thread = NSThread.currentThread })
15
+ callback.call
16
+
17
+ @thread.should == NSThread.mainThread
18
+ end
19
+ end
10
20
  end
11
21
  end
12
22
 
data/spec/dsl_spec.rb CHANGED
@@ -2,7 +2,7 @@ describe Elevate::DSL do
2
2
  describe "#on_started" do
3
3
  it "stores the provided block" do
4
4
  i = Elevate::DSL.new do
5
- on_started { |operation| puts 'hi' }
5
+ on_started { puts 'hi' }
6
6
  end
7
7
 
8
8
  i.started_callback.should.not.be.nil
@@ -12,7 +12,7 @@ describe Elevate::DSL do
12
12
  describe "#on_completed" do
13
13
  it "stores the passed block" do
14
14
  i = Elevate::DSL.new do
15
- on_completed { |o| puts 'hi' }
15
+ on_completed { |result, exception| puts 'hi' }
16
16
  end
17
17
 
18
18
  i.finished_callback.should.not.be.nil
@@ -0,0 +1,44 @@
1
+ describe Elevate::HTTP::ActivityIndicator do
2
+ before do
3
+ UIApplication.sharedApplication.setNetworkActivityIndicatorVisible(false)
4
+
5
+ @indicator = Elevate::HTTP::ActivityIndicator.new
6
+ end
7
+
8
+ describe ".instance" do
9
+ it "returns a singleton instance" do
10
+ instance = Elevate::HTTP::ActivityIndicator.instance
11
+ instance2 = Elevate::HTTP::ActivityIndicator.instance
12
+
13
+ instance.object_id.should == instance2.object_id
14
+ end
15
+ end
16
+
17
+ describe "#hide" do
18
+ it "does nothing if it isn't shown" do
19
+ @indicator.hide
20
+
21
+ UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.false
22
+ end
23
+
24
+ it "hides the indicator only if there are no outstanding show requests" do
25
+ @indicator.show
26
+
27
+ @indicator.show
28
+ @indicator.hide
29
+
30
+ UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.true
31
+
32
+ @indicator.hide
33
+ UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.false
34
+ end
35
+ end
36
+
37
+ describe "#show" do
38
+ it "shows the activity indicator" do
39
+ @indicator.show
40
+
41
+ UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.true
42
+ end
43
+ end
44
+ end
@@ -1,7 +1,7 @@
1
1
  describe Elevate::ElevateOperation do
2
2
  before do
3
- @target = Target.new
4
- @operation = Elevate::ElevateOperation.alloc.initWithTarget(@target)
3
+ @target = lambda { @result }
4
+ @operation = Elevate::ElevateOperation.alloc.initWithTarget(@target, args: {})
5
5
  @queue = NSOperationQueue.alloc.init
6
6
  end
7
7
 
@@ -18,7 +18,7 @@ describe Elevate::ElevateOperation do
18
18
  @lock = NSLock.alloc.init
19
19
  @value = []
20
20
 
21
- @operation.on_started = lambda do
21
+ @operation.on_started = lambda do
22
22
  @lock.lock()
23
23
  if @value == []
24
24
  @value << 1
@@ -26,14 +26,14 @@ describe Elevate::ElevateOperation do
26
26
  @lock.unlock()
27
27
  end
28
28
 
29
- @operation.on_finished = lambda do
29
+ @operation.on_finished = lambda do |result, exception|
30
30
  @lock.lock()
31
31
  if @value == [1]
32
32
  @value << 2
33
33
  end
34
34
  @lock.unlock()
35
35
 
36
- resume
36
+ Dispatch::Queue.main.sync { resume }
37
37
  end
38
38
 
39
39
  @queue.addOperation(@operation)
@@ -58,50 +58,21 @@ describe Elevate::ElevateOperation do
58
58
 
59
59
  describe "when an exception is raised" do
60
60
  it "returns the exception" do
61
- @target.exception = IndexError.new
61
+ @target = lambda { raise IndexError }
62
+ @operation = Elevate::ElevateOperation.alloc.initWithTarget(@target, args: {})
62
63
 
63
64
  @queue.addOperation(@operation)
64
65
  @operation.waitUntilFinished()
65
66
 
66
- @operation.exception.should == @target.exception
67
- end
68
- end
69
- end
70
-
71
- describe "#main" do
72
- describe "when the operation has not been run" do
73
- it "invokes the target" do
74
- @queue.addOperation(@operation)
75
- @operation.waitUntilFinished()
76
-
77
- @target.called.should == 1
78
- end
79
- end
80
-
81
- describe "when the operation has been cancelled prior to starting" do
82
- it "does not invoke the target" do
83
- @operation.cancel()
84
-
85
- @queue.addOperation(@operation)
86
- @operation.waitUntilFinished()
87
-
88
- @target.called.should == 0
89
- end
90
- end
91
-
92
- describe "when the operation is running" do
93
- it "allows IO to be cancelled" do
94
- @queue.addOperation(@operation)
95
- @operation.waitUntilFinished()
96
-
97
- @target.io_coordinator.should.not.be.nil
67
+ @operation.exception.should.not.be.nil
98
68
  end
99
69
  end
100
70
  end
101
71
 
102
72
  describe "#result" do
103
73
  before do
104
- @target.result = 42
74
+ @target = lambda { 42 }
75
+ @operation = Elevate::ElevateOperation.alloc.initWithTarget(@target, args: {})
105
76
  end
106
77
 
107
78
  describe "before starting the operation" do
@@ -122,7 +93,7 @@ describe Elevate::ElevateOperation do
122
93
  end
123
94
 
124
95
  describe "when the operation has finished" do
125
- it "returns the result of the target's #execute method" do
96
+ it "returns the result of the lambda" do
126
97
  @queue.addOperation(@operation)
127
98
  @operation.waitUntilFinished()
128
99
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elevate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-22 00:00:00.000000000 Z
12
+ date: 2013-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -83,7 +83,7 @@ extensions: []
83
83
  extra_rdoc_files: []
84
84
  files:
85
85
  - .gitignore
86
- - .rvmrc
86
+ - .travis.yml
87
87
  - Gemfile
88
88
  - Gemfile.lock
89
89
  - Guardfile
@@ -95,7 +95,6 @@ files:
95
95
  - lib/elevate.rb
96
96
  - lib/elevate/api.rb
97
97
  - lib/elevate/callback.rb
98
- - lib/elevate/dispatcher.rb
99
98
  - lib/elevate/dsl.rb
100
99
  - lib/elevate/http/activity_indicator.rb
101
100
  - lib/elevate/http/base64.rb
@@ -111,9 +110,8 @@ files:
111
110
  - lib/elevate/version.rb
112
111
  - spec/api_spec.rb
113
112
  - spec/callback_spec.rb
114
- - spec/dispatcher_spec.rb
115
113
  - spec/dsl_spec.rb
116
- - spec/helpers/target.rb
114
+ - spec/http/activity_indicator_spec.rb
117
115
  - spec/http/http_client_spec.rb
118
116
  - spec/http/http_request_spec.rb
119
117
  - spec/io_coordinator_spec.rb
@@ -132,7 +130,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
132
130
  version: '0'
133
131
  segments:
134
132
  - 0
135
- hash: 13511162272098335
133
+ hash: -1464207392655779665
136
134
  required_rubygems_version: !ruby/object:Gem::Requirement
137
135
  none: false
138
136
  requirements:
@@ -141,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
139
  version: '0'
142
140
  segments:
143
141
  - 0
144
- hash: 13511162272098335
142
+ hash: -1464207392655779665
145
143
  requirements: []
146
144
  rubyforge_project:
147
145
  rubygems_version: 1.8.24
@@ -151,9 +149,8 @@ summary: Distill the essence of your RubyMotion app
151
149
  test_files:
152
150
  - spec/api_spec.rb
153
151
  - spec/callback_spec.rb
154
- - spec/dispatcher_spec.rb
155
152
  - spec/dsl_spec.rb
156
- - spec/helpers/target.rb
153
+ - spec/http/activity_indicator_spec.rb
157
154
  - spec/http/http_client_spec.rb
158
155
  - spec/http/http_request_spec.rb
159
156
  - spec/io_coordinator_spec.rb
data/.rvmrc DELETED
@@ -1,5 +0,0 @@
1
- rvm ruby-1.9.3-p194@rubymotion --create
2
-
3
- [[ -s "config/rvm_env.sh" ]] && . "config/rvm_env.sh"
4
- [[ -s ".rvm_env" ]] && . ".rvm_env"
5
-
@@ -1,53 +0,0 @@
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
@@ -1,40 +0,0 @@
1
- describe Elevate::Dispatcher do
2
- DELAY = 0.2
3
-
4
- before do
5
- @dispatcher = Elevate::Dispatcher.new
6
- end
7
-
8
- describe "#on_started=" do
9
- before do
10
- @dispatcher.on_started = lambda { @thread = NSThread.currentThread }
11
- end
12
-
13
- after do
14
- @thread = nil
15
- end
16
-
17
- it "invokes the callback on the main thread" do
18
- wait DELAY do
19
- @thread.should == NSThread.currentThread
20
- end
21
- end
22
-
23
- it "does not invoke the callback immediately" do
24
- @thread.should.be.nil
25
- end
26
- end
27
-
28
- describe "#on_finished=" do
29
- before do
30
- @dispatcher.on_finished = lambda { @thread = NSThread.currentThread }
31
- @dispatcher.invoke_finished_callback()
32
- end
33
-
34
- it "invokes the callback on the main thread" do
35
- wait DELAY do
36
- @thread.should == NSThread.currentThread
37
- end
38
- end
39
- end
40
- end
@@ -1,23 +0,0 @@
1
- class Target
2
- attr_reader :called
3
- attr_reader :io_coordinator
4
- attr_accessor :exception
5
- attr_accessor :result
6
-
7
- def initialize
8
- @called = 0
9
- @result = true
10
- end
11
-
12
- def execute
13
- @io_coordinator = Thread.current[:io_coordinator]
14
-
15
- @called += 1
16
-
17
- if @exception
18
- raise @exception
19
- end
20
-
21
- @result
22
- end
23
- end