rumember 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +59 -0
- data/Rakefile +8 -0
- data/bin/ru +6 -0
- data/lib/rumember.rb +156 -0
- data/lib/rumember/abstract.rb +51 -0
- data/lib/rumember/account.rb +41 -0
- data/lib/rumember/list.rb +20 -0
- data/lib/rumember/location.rb +11 -0
- data/lib/rumember/task.rb +96 -0
- data/lib/rumember/timeline.rb +20 -0
- data/lib/rumember/transaction.rb +34 -0
- data/rumember.gemspec +18 -0
- data/spec/rumember/timeline_spec.rb +33 -0
- data/spec/rumember/transaction_spec.rb +51 -0
- data/spec/rumember_spec.rb +14 -0
- data/spec/spec_helper.rb +7 -0
- metadata +98 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Tim Pope
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
Rumember
|
2
|
+
========
|
3
|
+
|
4
|
+
Ruby API and command line client for [Remember The
|
5
|
+
Milk](http://www.rememberthemilk.com/).
|
6
|
+
|
7
|
+
## Command line usage
|
8
|
+
|
9
|
+
The sole motivation for this project was a quick way to capture to-dos
|
10
|
+
from the command line. As such, I've chosen a chosen a very short
|
11
|
+
command name of `ru` (something I'd normally never allow myself to do).
|
12
|
+
All arguments are joined with spaces and used to invoke Remember The
|
13
|
+
Milk's [Smart Add](http://www.rememberthemilk.com/services/smartadd/)
|
14
|
+
feature.
|
15
|
+
|
16
|
+
ru buy milk #errand
|
17
|
+
|
18
|
+
Browser based authentication is triggered the first time `ru` is run,
|
19
|
+
and the resulting token is cached in `~/.rtm.yml`.
|
20
|
+
|
21
|
+
I was originally planning to add support for the full range of
|
22
|
+
operations possible in Remember The Milk, but after pondering the
|
23
|
+
interface, this seems unlikely. I just can't imagine myself forgoing
|
24
|
+
the web interface in favor of something like:
|
25
|
+
|
26
|
+
ru --complete 142857 # Ain't gonna happen
|
27
|
+
|
28
|
+
## API Usage
|
29
|
+
|
30
|
+
The API is a bit more fleshed out than the command line interface, but
|
31
|
+
still incomplete, under-documented, and under-tested (I have additional
|
32
|
+
integration tests I won't publish because they are specific to my RTM
|
33
|
+
account). You'll need to familiarize yourself with [Remember The Milk's
|
34
|
+
API](http://www.rememberthemilk.com/services/api/). In particular, you
|
35
|
+
need to understand what a timeline is.
|
36
|
+
|
37
|
+
interface = Rumember.new(api_key, shared_secret)
|
38
|
+
interface = Rumember.new # Uses built in credentials
|
39
|
+
interface.dispatch('test.echo')
|
40
|
+
|
41
|
+
account = interface.account(auth_token)
|
42
|
+
account = interface.account # browser based and cached
|
43
|
+
account = Rumember.account # Rumember.new.account shortcut
|
44
|
+
|
45
|
+
timeline = account.timeline # cached
|
46
|
+
timeline = account.new_timeline # fresh each time
|
47
|
+
|
48
|
+
timeline.smart_add('buy milk #errand')
|
49
|
+
|
50
|
+
list = timeline.lists.first
|
51
|
+
task = list.tasks.first
|
52
|
+
transaction = task.complete
|
53
|
+
transaction.undo
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
Please follow [Git commit message best
|
58
|
+
practices](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
59
|
+
when submitting a pull request.
|
data/Rakefile
ADDED
data/bin/ru
ADDED
data/lib/rumember.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
class Rumember
|
2
|
+
|
3
|
+
class Error < RuntimeError
|
4
|
+
end
|
5
|
+
|
6
|
+
class ResponseError < Error
|
7
|
+
attr_accessor :code
|
8
|
+
end
|
9
|
+
|
10
|
+
API_KEY = '36f62f69fba7135e8049adbe307ff9ba'
|
11
|
+
SHARED_SECRET = '0c33513097c09be4'
|
12
|
+
|
13
|
+
module Dispatcher
|
14
|
+
|
15
|
+
def dispatch(method, params = {})
|
16
|
+
parent.dispatch(method, self.params.merge(params))
|
17
|
+
end
|
18
|
+
|
19
|
+
def transaction_dispatch(*args)
|
20
|
+
response = dispatch(*args)
|
21
|
+
yield response if block_given?
|
22
|
+
Transaction.new(self, response)
|
23
|
+
end
|
24
|
+
|
25
|
+
def lists
|
26
|
+
dispatch('lists.getList')['lists']['list'].map do |list|
|
27
|
+
List.new(self, list)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def locations
|
32
|
+
dispatch('locations.getList')['locations']['location'].map do |list|
|
33
|
+
Location.new(self, list)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.run(argv)
|
40
|
+
if argv.empty?
|
41
|
+
puts "Logged in as #{account.username}"
|
42
|
+
else
|
43
|
+
account.smart_add(argv.join(' '))
|
44
|
+
end
|
45
|
+
rescue Error
|
46
|
+
$stderr.puts "#$!"
|
47
|
+
exit 1
|
48
|
+
rescue Interrupt
|
49
|
+
$stderr.puts "Interrupted!"
|
50
|
+
exit 130
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :api_key, :shared_secret
|
54
|
+
|
55
|
+
def initialize(api_key = API_KEY, shared_secret = SHARED_SECRET)
|
56
|
+
@api_key = api_key
|
57
|
+
@shared_secret = shared_secret
|
58
|
+
end
|
59
|
+
|
60
|
+
def api_sig(params)
|
61
|
+
require 'digest/md5'
|
62
|
+
Digest::MD5.hexdigest(
|
63
|
+
shared_secret + params.sort_by {|k,v| k.to_s}.join
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def sign(params)
|
68
|
+
params = params.merge('api_key' => api_key)
|
69
|
+
params.update('api_sig' => api_sig(params))
|
70
|
+
end
|
71
|
+
|
72
|
+
def params(params)
|
73
|
+
require 'cgi'
|
74
|
+
sign(params).map do |k,v|
|
75
|
+
"#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
76
|
+
end.join('&')
|
77
|
+
end
|
78
|
+
|
79
|
+
def auth_url(perms = :delete, extra = {})
|
80
|
+
"http://rememberthemilk.com/services/auth?" +
|
81
|
+
params({'perms' => perms}.merge(extra))
|
82
|
+
end
|
83
|
+
|
84
|
+
def authenticate
|
85
|
+
require 'launchy'
|
86
|
+
frob = dispatch('auth.getFrob')['frob']
|
87
|
+
Launchy.open(auth_url(:delete, 'frob' => frob))
|
88
|
+
first = true
|
89
|
+
puts 'Press enter when authentication is complete'
|
90
|
+
$stdin.gets
|
91
|
+
dispatch('auth.getToken', 'frob' => frob)['auth']
|
92
|
+
end
|
93
|
+
|
94
|
+
def reconfigure
|
95
|
+
token = authenticate['token']
|
96
|
+
File.open(self.class.config_file,'w') do |f|
|
97
|
+
f.puts "auth_token: #{token}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.config_file
|
102
|
+
File.expand_path('~/.rtm.yml')
|
103
|
+
end
|
104
|
+
|
105
|
+
def account(auth_token = nil)
|
106
|
+
if auth_token
|
107
|
+
Account.new(self, auth_token)
|
108
|
+
else
|
109
|
+
require 'yaml'
|
110
|
+
@account ||=
|
111
|
+
begin
|
112
|
+
reconfigure unless File.exist?(self.class.config_file)
|
113
|
+
t = YAML.load(File.read(self.class.config_file))['auth_token']
|
114
|
+
account(t)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
alias autoconfigure account
|
120
|
+
|
121
|
+
def self.account
|
122
|
+
@account ||= new.account
|
123
|
+
end
|
124
|
+
|
125
|
+
def url(params)
|
126
|
+
"http://api.rememberthemilk.com/services/rest?#{params(params)}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def dispatch(method, params = {})
|
130
|
+
require 'json'
|
131
|
+
require 'open-uri'
|
132
|
+
raw = open(url(params.merge('method' => "rtm.#{method}", 'format' => 'json'))).read
|
133
|
+
rsp = JSON.parse(raw)['rsp']
|
134
|
+
case rsp['stat']
|
135
|
+
when 'fail'
|
136
|
+
error = ResponseError.new(rsp['err']['msg'])
|
137
|
+
error.code = rsp['err']['code']
|
138
|
+
error.set_backtrace caller
|
139
|
+
raise error
|
140
|
+
when 'ok'
|
141
|
+
rsp.delete('stat')
|
142
|
+
rsp
|
143
|
+
else
|
144
|
+
raise ResponseError.new(rsp.inspect)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
autoload :Abstract, 'rumember/abstract'
|
149
|
+
autoload :Account, 'rumember/account'
|
150
|
+
autoload :Timeline, 'rumember/timeline'
|
151
|
+
autoload :Transaction, 'rumember/transaction'
|
152
|
+
autoload :List, 'rumember/list'
|
153
|
+
autoload :Location, 'rumember/location'
|
154
|
+
autoload :Task, 'rumember/task'
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class Rumember
|
2
|
+
class Abstract
|
3
|
+
|
4
|
+
include Dispatcher
|
5
|
+
attr_reader :parent
|
6
|
+
|
7
|
+
def initialize(parent, attributes)
|
8
|
+
@parent = parent
|
9
|
+
@attributes = attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
def params
|
13
|
+
{}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.reader(*methods, &block)
|
17
|
+
methods.each do |method|
|
18
|
+
define_method(method) do
|
19
|
+
value = @attributes[method.to_s]
|
20
|
+
if block && !value.nil?
|
21
|
+
yield value
|
22
|
+
else
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.integer_reader(*methods)
|
30
|
+
reader(*methods) do |value|
|
31
|
+
Integer(value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.boolean_reader(*methods)
|
36
|
+
reader(*methods) do |value|
|
37
|
+
value == '1' ? true : false
|
38
|
+
end
|
39
|
+
methods.each do |method|
|
40
|
+
alias_method "#{method}?", method
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.time_reader(*methods)
|
45
|
+
reader(*methods) do |value|
|
46
|
+
Time.parse(value) unless value.empty?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Rumember
|
2
|
+
class Account
|
3
|
+
|
4
|
+
include Dispatcher
|
5
|
+
attr_reader :auth_token
|
6
|
+
|
7
|
+
def initialize(interface, auth_token)
|
8
|
+
@interface = interface
|
9
|
+
@auth_token = auth_token
|
10
|
+
end
|
11
|
+
|
12
|
+
def parent
|
13
|
+
@interface
|
14
|
+
end
|
15
|
+
|
16
|
+
def params
|
17
|
+
{'auth_token' => auth_token}
|
18
|
+
end
|
19
|
+
|
20
|
+
def new_timeline
|
21
|
+
Timeline.new(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def timeline
|
25
|
+
@timeline ||= new_timeline
|
26
|
+
end
|
27
|
+
|
28
|
+
def smart_add(name)
|
29
|
+
timeline.smart_add(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def username
|
33
|
+
@username ||= dispatch('auth.checkToken')['auth']['user']['username']
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
"#<#{self.class.inspect}: #{username}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Rumember
|
2
|
+
class List < Abstract
|
3
|
+
|
4
|
+
reader :name, :filter
|
5
|
+
boolean_reader :archived, :deleted, :locked, :smart
|
6
|
+
integer_reader :id, :position
|
7
|
+
alias to_s name
|
8
|
+
|
9
|
+
def params
|
10
|
+
{'list_id' => id}
|
11
|
+
end
|
12
|
+
|
13
|
+
def tasks
|
14
|
+
dispatch('tasks.getList')['tasks']['list']['taskseries'].map do |ts|
|
15
|
+
Task.new(self, ts)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
class Rumember
|
2
|
+
class Task < Abstract
|
3
|
+
integer_reader :id, :location_id
|
4
|
+
time_reader :created, :modified
|
5
|
+
reader :name, :source, :url
|
6
|
+
alias taskseries_id id
|
7
|
+
|
8
|
+
def task_id
|
9
|
+
Integer(@attributes['task']['id'])
|
10
|
+
end
|
11
|
+
|
12
|
+
def tags
|
13
|
+
if @attributes['tags'].empty?
|
14
|
+
[]
|
15
|
+
else
|
16
|
+
Array(@attributes['tags']['tag'])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def params
|
21
|
+
{'taskseries_id' => taskseries_id, 'task_id' => task_id}
|
22
|
+
end
|
23
|
+
|
24
|
+
def transaction_dispatch(*args)
|
25
|
+
super(*args) do |response|
|
26
|
+
@attributes = response['list']['taskseries']
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete
|
31
|
+
transaction_dispatch('tasks.delete')
|
32
|
+
end
|
33
|
+
|
34
|
+
def complete
|
35
|
+
transaction_dispatch('tasks.complete')
|
36
|
+
end
|
37
|
+
|
38
|
+
def uncomplete
|
39
|
+
transaction_dispatch('tasks.uncomplete')
|
40
|
+
end
|
41
|
+
|
42
|
+
def postpone
|
43
|
+
transaction_dispatch('tasks.postpone')
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_tags(tags)
|
47
|
+
transaction_dispatch('tasks.addTags', 'tags' => tags)
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_tags(tags)
|
51
|
+
transaction_dispatch('tasks.removeTags', 'tags' => tags)
|
52
|
+
end
|
53
|
+
|
54
|
+
def raise_priority
|
55
|
+
transaction_dispatch('tasks.movePriority', 'direction' => 'up')
|
56
|
+
end
|
57
|
+
|
58
|
+
def lower_priority
|
59
|
+
transaction_dispatch('tasks.movePriority', 'direction' => 'down')
|
60
|
+
end
|
61
|
+
|
62
|
+
def move_to(list)
|
63
|
+
transaction_dispatch(
|
64
|
+
'tasks.moveTo',
|
65
|
+
'from_list_id' => parent.id,
|
66
|
+
'to_list_id' => list.id
|
67
|
+
).tap do
|
68
|
+
@parent = list
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
%w(Estimate Name Priority Recurrence URL).each do |attr|
|
73
|
+
define_method("set_#{attr.downcase}") do |value|
|
74
|
+
transaction_dispatch("tasks.set#{attr}", attr.downcase => value)
|
75
|
+
end
|
76
|
+
alias_method "#{attr.downcase}=", "set_#{attr.downcase}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_tags(tags)
|
80
|
+
transaction_dispatch('tasks.setTags', 'tags' => Array(tags).join(' '))
|
81
|
+
end
|
82
|
+
alias tags= set_tags
|
83
|
+
|
84
|
+
def set_due_date(url)
|
85
|
+
transaction_dispatch('tasks.setDueDate', 'url' => url)
|
86
|
+
end
|
87
|
+
alias due_date= set_due_date
|
88
|
+
|
89
|
+
def set_location(id)
|
90
|
+
id = id.location_id if id.respond_to?(:location_id)
|
91
|
+
transaction_dispatch('tasks.setLocation', id)
|
92
|
+
end
|
93
|
+
alias location= set_location
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Rumember
|
2
|
+
class Timeline
|
3
|
+
|
4
|
+
include Dispatcher
|
5
|
+
attr_reader :id, :parent
|
6
|
+
|
7
|
+
def initialize(parent, id = nil)
|
8
|
+
@parent = parent
|
9
|
+
@id = (id || parent.dispatch('timelines.create').fetch('timeline')).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def params
|
13
|
+
{'timeline' => id}
|
14
|
+
end
|
15
|
+
|
16
|
+
def smart_add(name)
|
17
|
+
dispatch('tasks.add', 'parse' => 1, 'name' => name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Rumember
|
2
|
+
class Transaction < Abstract
|
3
|
+
|
4
|
+
def id
|
5
|
+
Integer(@attributes['transaction']['id'])
|
6
|
+
end
|
7
|
+
|
8
|
+
def undoable?
|
9
|
+
@attributes['transaction']['undoable'] == '1'
|
10
|
+
end
|
11
|
+
|
12
|
+
def undone?
|
13
|
+
!!@undone
|
14
|
+
end
|
15
|
+
|
16
|
+
def response
|
17
|
+
@attributes
|
18
|
+
end
|
19
|
+
|
20
|
+
def params
|
21
|
+
{'transaction_id' => id}
|
22
|
+
end
|
23
|
+
|
24
|
+
def undo
|
25
|
+
if undone? || !undoable?
|
26
|
+
false
|
27
|
+
else
|
28
|
+
dispatch('transactions.undo')
|
29
|
+
@undone = true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/rumember.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "rumember"
|
3
|
+
s.version = "1.0.0"
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.authors = ["Tim Pope"]
|
6
|
+
s.email = ["code@tpope.n"+'et']
|
7
|
+
s.homepage = "http://github.com/tpope/rumember"
|
8
|
+
s.summary = "Remember The Milk Ruby API and command line client"
|
9
|
+
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
12
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
|
15
|
+
s.add_runtime_dependency("json", ["~> 1.4.0"])
|
16
|
+
s.add_runtime_dependency("launchy", ["~> 0.3.0"])
|
17
|
+
s.add_development_dependency("rspec", ["~> 2.5"])
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec/spec_helper')
|
2
|
+
|
3
|
+
describe Rumember::Timeline do
|
4
|
+
|
5
|
+
let :interface do
|
6
|
+
stub.as_null_object
|
7
|
+
end
|
8
|
+
|
9
|
+
let :account do
|
10
|
+
Rumember::Account.new(interface, 'key')
|
11
|
+
end
|
12
|
+
|
13
|
+
subject do
|
14
|
+
Rumember::Timeline.new(account, 3)
|
15
|
+
end
|
16
|
+
|
17
|
+
its(:parent) { should == account }
|
18
|
+
its(:params) { should == { 'timeline' => 3 } }
|
19
|
+
|
20
|
+
describe '#smart_add' do
|
21
|
+
it 'should dispatch rtm.tasks.add' do
|
22
|
+
interface.should_receive(:dispatch).with(
|
23
|
+
'tasks.add',
|
24
|
+
'auth_token' => 'key',
|
25
|
+
'name' => 'buy milk',
|
26
|
+
'parse' => 1,
|
27
|
+
'timeline' => 3
|
28
|
+
)
|
29
|
+
subject.smart_add('buy milk')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec/spec_helper')
|
2
|
+
|
3
|
+
describe Rumember::Transaction do
|
4
|
+
|
5
|
+
let :response do
|
6
|
+
{ 'transaction' => { 'id' => '1', 'undoable' => '1' } }
|
7
|
+
end
|
8
|
+
|
9
|
+
let :parent do
|
10
|
+
stub.as_null_object
|
11
|
+
end
|
12
|
+
|
13
|
+
subject do
|
14
|
+
Rumember::Transaction.new(parent, response)
|
15
|
+
end
|
16
|
+
|
17
|
+
its(:parent) { should == parent }
|
18
|
+
its(:response) { should == response }
|
19
|
+
its(:id) { should == 1 }
|
20
|
+
its(:undoable?) { should be_true }
|
21
|
+
its(:params) { should == {'transaction_id' => 1} }
|
22
|
+
|
23
|
+
describe '#undo' do
|
24
|
+
context 'when the transaction is not undoable' do
|
25
|
+
let :response do
|
26
|
+
{ 'transaction' => { 'id' => '1', 'undoable' => '0' } }
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should not dispatch rtm.transactions.undo' do
|
30
|
+
subject.should_not_receive(:dispatch).with('transactions.undo')
|
31
|
+
subject.undo
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should return false' do
|
35
|
+
subject.undo.should be_false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when the transaction is undoable' do
|
40
|
+
it 'should dispatch rtm.transactions.undo' do
|
41
|
+
subject.should_receive(:dispatch).with('transactions.undo')
|
42
|
+
subject.undo
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should return true' do
|
46
|
+
subject.undo.should be_true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec/spec_helper')
|
2
|
+
|
3
|
+
describe Rumember do
|
4
|
+
subject do
|
5
|
+
Rumember.new('abc123', 'BANANAS')
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#api_sig' do
|
9
|
+
it 'should MD5 the concatenated sorted parameters' do
|
10
|
+
subject.api_sig(:yxz => 'foo', :feg => 'bar', :abc => 'baz').should ==
|
11
|
+
'82044aae4dd676094f23f1ec152159ba'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rumember
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tim Pope
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-14 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: &75735360 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.4.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *75735360
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: launchy
|
27
|
+
requirement: &75735060 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.3.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *75735060
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &75734750 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '2.5'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *75734750
|
47
|
+
description:
|
48
|
+
email:
|
49
|
+
- code@tpope.net
|
50
|
+
executables:
|
51
|
+
- ru
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- .gitignore
|
56
|
+
- Gemfile
|
57
|
+
- MIT-LICENSE
|
58
|
+
- README.markdown
|
59
|
+
- Rakefile
|
60
|
+
- bin/ru
|
61
|
+
- lib/rumember.rb
|
62
|
+
- lib/rumember/abstract.rb
|
63
|
+
- lib/rumember/account.rb
|
64
|
+
- lib/rumember/list.rb
|
65
|
+
- lib/rumember/location.rb
|
66
|
+
- lib/rumember/task.rb
|
67
|
+
- lib/rumember/timeline.rb
|
68
|
+
- lib/rumember/transaction.rb
|
69
|
+
- rumember.gemspec
|
70
|
+
- spec/rumember/timeline_spec.rb
|
71
|
+
- spec/rumember/transaction_spec.rb
|
72
|
+
- spec/rumember_spec.rb
|
73
|
+
- spec/spec_helper.rb
|
74
|
+
homepage: http://github.com/tpope/rumember
|
75
|
+
licenses: []
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 1.8.6
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: Remember The Milk Ruby API and command line client
|
98
|
+
test_files: []
|