rumember 1.0.0
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 +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: []
|