gradesfirst 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +1 -2
  3. data/Gemfile.lock +2 -6
  4. data/gradesfirst.gemspec +2 -2
  5. data/lib/gradesfirst.rb +1 -0
  6. data/lib/gradesfirst/cli.rb +7 -29
  7. data/lib/gradesfirst/cli_helper.rb +33 -0
  8. data/lib/gradesfirst/command.rb +7 -0
  9. data/lib/gradesfirst/commit_message_command.rb +1 -1
  10. data/lib/gradesfirst/task_add_command.rb +51 -0
  11. data/lib/gradesfirst/task_cli.rb +48 -0
  12. data/lib/gradesfirst/task_command.rb +38 -30
  13. data/lib/gradesfirst/task_delete_command.rb +64 -0
  14. data/lib/gradesfirst/task_list_command.rb +40 -0
  15. data/lib/gradesfirst/task_move_command.rb +64 -0
  16. data/lib/gradesfirst/task_toggle_command.rb +65 -0
  17. data/lib/http_magic.rb +2 -258
  18. data/lib/http_magic/api.rb +233 -0
  19. data/lib/http_magic/request.rb +93 -0
  20. data/lib/http_magic/uri.rb +74 -0
  21. data/lib/pivotal_tracker.rb +1 -1
  22. data/test/branch_command_test.rb +8 -21
  23. data/test/cli_test.rb +51 -34
  24. data/test/command_test.rb +5 -7
  25. data/test/commit_message_command_test.rb +37 -45
  26. data/test/fixtures/task.json +10 -0
  27. data/test/fixtures/task_added.txt +6 -0
  28. data/test/fixtures/task_deleted.txt +6 -0
  29. data/test/fixtures/task_moved.txt +6 -0
  30. data/test/fixtures/task_toggled.txt +6 -0
  31. data/test/fixtures/tasks.txt +2 -2
  32. data/test/http_magic/{get_test.rb → api/get_test.rb} +1 -1
  33. data/test/http_magic/api/post_test.rb +34 -0
  34. data/test/http_magic/request_test.rb +63 -0
  35. data/test/http_magic/uri_test.rb +28 -55
  36. data/test/support/pivotal_test_helper.rb +110 -0
  37. data/test/support/request_expectation.rb +33 -0
  38. data/test/task_add_command_test.rb +54 -0
  39. data/test/task_delete_command_test.rb +57 -0
  40. data/test/task_list_command_test.rb +18 -0
  41. data/test/task_move_command_test.rb +66 -0
  42. data/test/task_toggle_command_test.rb +58 -0
  43. data/test/test_helper.rb +35 -19
  44. metadata +29 -7
  45. data/test/pivotal_tracker_test.rb +0 -46
  46. data/test/task_command_test.rb +0 -52
@@ -0,0 +1,40 @@
1
+ require 'gradesfirst/task_command'
2
+
3
+ module GradesFirst
4
+
5
+ # Implementation of a Thor command for listing tasks related to a story.
6
+ class TaskListCommand < GradesFirst::TaskCommand
7
+
8
+ # Description of the gf task list Thor command that will be used in the
9
+ # command line help.
10
+ def self.description
11
+ 'List the tasks related to a PivotalTracker story.'
12
+ end
13
+
14
+ # Performs the gf task list Thor command.
15
+ def execute
16
+ @story = current_story
17
+ if @story
18
+ @tasks = get_tasks(@story)
19
+ end
20
+ end
21
+
22
+ # Generates the comand line output response. The output of the task command
23
+ # is a list of the tasks associated with the PivotalTracker story associated
24
+ # with the current branch.
25
+ def response
26
+ if @tasks.nil?
27
+ story_error_message
28
+ else
29
+ task_list_response(@story, @tasks)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def story_error_message
36
+ 'Tasks cannot be retrieved for this branch.'
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,64 @@
1
+ require 'gradesfirst/task_command'
2
+
3
+ module GradesFirst
4
+
5
+ # Implementation of a Thor command for moving tasks on a PivotalTracker story
6
+ # from one priority position to another in the list.
7
+ class TaskMoveCommand < GradesFirst::TaskCommand
8
+ # Description of the "gf task move" Thor command that will be sued in the
9
+ # commandline help.
10
+ def self.description
11
+ 'Move a task to a new position in the list for a PivotalTracker story.'
12
+ end
13
+
14
+ # Performs the gf task move Thor command.
15
+ def execute(from, to)
16
+ @story = current_story
17
+ if @story
18
+ task_id = get_task_id_by_position(@story, from)
19
+ if task_id
20
+ @success = task_move(@story, task_id, to)
21
+ else
22
+ @task_position_invalid = true
23
+ end
24
+ end
25
+ end
26
+
27
+ # Generates the command line output response. The output of the task move
28
+ # command is a completion status message which may be followed by the new
29
+ # list of tasks if the task was moved successfully.
30
+ def response
31
+ if @task_position_invalid
32
+ position_invalid_message
33
+ else
34
+ task_action_response(@story, @success)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def position_invalid_message
41
+ 'Task "from" position given does not exist.'
42
+ end
43
+
44
+ def story_error_message
45
+ 'Tasks cannnot be moved for this branch.'
46
+ end
47
+
48
+ def task_error_message
49
+ 'Moving the task failed.'
50
+ end
51
+
52
+ def task_move(story, task_id, to)
53
+ PivotalTracker.
54
+ projects[story['project_id']].
55
+ stories[story['id']].
56
+ tasks[task_id].
57
+ put(position: to.to_i)
58
+ end
59
+
60
+ def task_success_message
61
+ 'Task was successfully moved.'
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,65 @@
1
+ require 'gradesfirst/task_command'
2
+
3
+ module GradesFirst
4
+
5
+ # Implementation of a Thor command for toggling whether or not a task is
6
+ # complete.
7
+ class TaskToggleCommand < GradesFirst::TaskCommand
8
+ # Description of the "gf task toggle" Thor command that will be used in
9
+ # the commandline help.
10
+ def self.description
11
+ 'Toggle completion status of a PivotalTracker story task.'
12
+ end
13
+
14
+ # Performs the gf task toggle POSITION Thor command.
15
+ def execute(position)
16
+ @story = current_story
17
+ if @story
18
+ task = get_task_by_position(@story, position)
19
+ if task
20
+ @success = task_toggle(@story, task)
21
+ else
22
+ @task_position_invalid = true
23
+ end
24
+ end
25
+ end
26
+
27
+ # Generates the command line output response. The output of the task toggle
28
+ # command is a completion status message which may be followed by the new
29
+ # list of tasks if the task was moved successfully.
30
+ def response
31
+ if @task_position_invalid
32
+ position_invalid_message
33
+ else
34
+ task_action_response(@story, @success)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def position_invalid_message
41
+ 'Task position given does not exist.'
42
+ end
43
+
44
+ def story_error_message
45
+ 'Tasks cannot be toggled for this branch.'
46
+ end
47
+
48
+ def task_error_message
49
+ 'Toggling of the task completion status failed.'
50
+ end
51
+
52
+ def task_success_message
53
+ 'Task completion status was successfully toggled.'
54
+ end
55
+
56
+ def task_toggle(story, task)
57
+ PivotalTracker.
58
+ projects[story['project_id']].
59
+ stories[story['id']].
60
+ tasks[task['id']].
61
+ put(complete: !task['complete'])
62
+ end
63
+ end
64
+
65
+ end
data/lib/http_magic.rb CHANGED
@@ -1,260 +1,4 @@
1
- require 'net/http'
2
- require 'json'
1
+ require 'http_magic/api'
3
2
 
4
- # A magical class that interacts with HTTP resources with a minimal amount of
5
- # configuration.
6
- #
7
- # Assuming an api where the url http://www.example.com/foo/99 responds with
8
- # the following.
9
- #
10
- # Header:
11
- #
12
- # Content-Type: application/json
13
- #
14
- # Body:
15
- #
16
- # {
17
- # "name": "Foo Bar"
18
- # }
19
- #
20
- # == Example
21
- #
22
- # class ExampleApi < HttpMagic
23
- # url 'www.example.com'
24
- # end
25
- #
26
- # ExampleApi.foo[99].get['name']
27
- # => "Foo Bar"
28
- #
29
- class HttpMagic < BasicObject
30
- # Makes the new method private so that instances of this class and it's
31
- # children can only be instantiated through the class method method_missing.
32
- # This is needed to enforce the intended usage of HttpMagic where the class
33
- # is configured to represent an http location. Once it is configured, the
34
- # location is interacted with dynamically with chained methods that correspond
35
- # with parts of URNs found at the location.
36
- #
37
- # == Example
38
- #
39
- # class ExampleApi < HttpMagic
40
- # url 'www.example.com'
41
- # namespace 'api/v1'
42
- # headers({'X-AuthToken' => 'token'})
43
- # end
44
- #
45
- # ExampleApi.new.uri
46
- # => "http://www.example.com/api/v1/new"
47
- #
48
- # ExampleApi.foo[99].uri
49
- # => "http://www.example.com/api/v1/foo/99"
50
- class << self
51
- private :new
52
- end
53
-
54
- # Sets or returns the request headers for an HttpMagic based class. The
55
- # headers will be passed along to each resource request being made.
56
- #
57
- # == Example
58
- #
59
- # class ExampleApi < HttpMagic
60
- # url 'www.example.com'
61
- # namespace 'api/v1'
62
- # headers({'X-AuthToken' => 'token'})
63
- # end
64
- def self.headers(value = :not_provided)
65
- unless value == :not_provided
66
- @headers = value
67
- end
68
- @headers
69
- end
70
-
71
- # Sets or returns the namespace for an HttpMagic based class. The namespace
72
- # will be prefixed to the urn for each request made to the url.
73
- #
74
- # Assuming an api where each resource is namespaced with 'api/v1' and where
75
- # the url http://www.example.com/api/v1/foo/99 responds with the following.
76
- #
77
- # Header:
78
- #
79
- # Content-Type: application/json
80
- #
81
- # Body:
82
- #
83
- # {
84
- # "name": "Foo Bar"
85
- # }
86
- #
87
- # == Example
88
- #
89
- # class ExampleApi < HttpMagic
90
- # url 'www.example.com'
91
- # namespace 'api/v1'
92
- # end
93
- #
94
- # ExampleApi.foo[99].get['name']
95
- # => "Foo Bar"
96
- def self.namespace(value = :not_provided)
97
- unless value == :not_provided
98
- @namespace = value
99
- end
100
- @namespace
101
- end
102
-
103
- # Sets or returns the uniform resource locator for the HTTP resource.
104
- #
105
- # == Example
106
- #
107
- # class ExampleApi < HttpMagic
108
- # url 'www.example.com'
109
- # end
110
- #
111
- # ExampleApi.url
112
- # => 'www.example.com'
113
- def self.url(value = :not_provided)
114
- unless value == :not_provided
115
- @url = value
116
- end
117
- @url
118
- end
119
-
120
- # Class scoped method_missing that starts the magic of creating urns
121
- # through meta programming by instantiating an instance of the class
122
- # and delegating the first part of the urn to that instance. This method
123
- # is an implementation of the Factory Method design pattern.
124
- def self.method_missing(name, *args, &block)
125
- new(@url, @namespace, @headers).__send__(name, *args)
126
- end
127
-
128
- def initialize(url, namespace, headers)
129
- @url = url
130
- @namespace = namespace
131
- @headers = headers
132
-
133
- @urn_parts = []
134
- end
135
-
136
- # Resource index reference intended to allow for the use of numbers in a urn
137
- # such as the following 'foo/99' being referenced by ExampleApi.foo[99]. It
138
- # can also be used to allow for HttpMagic methods to be specified for a urn
139
- # such as 'foo/get' being referenced by ExampleApi.foo[:get]. Finally, it can
140
- # be used for urn parts that are not valid Ruby methods such as 'foo/%5B%5D'
141
- # being referenced by ExampleApi.foo["%5B%5D"].
142
- #
143
- # * part - a part of a urn such that 'foo' and 'bar' would be parts of the urn
144
- # 'foo/bar'.
145
- #
146
- # Returns a reference to its instance so that urn parts can be chained
147
- # together.
148
- def [](part)
149
- @urn_parts << part.to_s
150
- self
151
- end
152
-
153
- # Gets a resource from the URI and returns it based on its content type. JSON
154
- # content will be parsed and returned as equivalent Ruby objects. All other
155
- # content types will be returned as text.
156
- #
157
- # Assuming an api where each resource is namespaced with 'api/v1' and where
158
- # the url http://www.example.com/api/v1/foo/99 responds with the following.
159
- #
160
- # Header:
161
- #
162
- # Content-Type: application/json
163
- #
164
- # Body:
165
- #
166
- # {
167
- # "name": "Foo Bar"
168
- # }
169
- #
170
- # == Example
171
- #
172
- # class ExampleApi < HttpMagic
173
- # url 'www.example.com'
174
- # namespace 'api/v1'
175
- # end
176
- #
177
- # ExampleApi.foo[99].get
178
- # => { "name" => "Foo Bar" }
179
- def get
180
- http = ::Net::HTTP.new(url, 443)
181
- http.use_ssl = true
182
-
183
- response = http.request_get(urn, @headers || {})
184
- if response && response.is_a?(::Net::HTTPSuccess)
185
- if response.content_type == 'application/json'
186
- ::JSON.parse(response.body)
187
- else
188
- response.body
189
- end
190
- else
191
- nil
192
- end
193
- end
194
-
195
- # Uniform resource identifier for a specified request.
196
- #
197
- # == Example
198
- #
199
- # class ExampleApi < HttpMagic
200
- # url 'www.example.com'
201
- # namespace 'api/v1'
202
- # end
203
- #
204
- # ExampleApi.foo[99].uri
205
- # => "www.example.com/api/v1/foo/99"
206
- def uri
207
- "#{url}#{urn}"
208
- end
209
-
210
- # Uniform resource locator for all the resources.
211
- #
212
- # == Example
213
- #
214
- # class ExampleApi < HttpMagic
215
- # url 'www.example.com'
216
- # namespace 'api/v1'
217
- # end
218
- #
219
- # ExampleApi.foo[99].url
220
- # => "www.example.com"
221
- def url
222
- @url
223
- end
224
-
225
- # Uniform resource name for a resource.
226
- #
227
- # == Example
228
- #
229
- # class ExampleApi < HttpMagic
230
- # url 'www.example.com'
231
- # namespace 'api/v1'
232
- # end
233
- #
234
- # ExampleApi.foo[99].urn
235
- # => "api/v1/foo/99"
236
- def urn
237
- resource_name = [@namespace, @urn_parts].flatten.compact.join('/')
238
- "/#{resource_name}"
239
- end
240
-
241
- # Instance scoped method_missing that accumulates the URN parts used in
242
- # forming the URI. It returns a reference to the current instance so that
243
- # method and [] based parts can be chained together to from the URN. In the
244
- # case of ExampleApi.foo[99] the 'foo' method is accumlated as a URN part
245
- # that will form the resulting URI.
246
- #
247
- # == Example
248
- #
249
- # class ExampleApi < HttpMagic
250
- # url 'www.example.com'
251
- # namespace 'api/v1'
252
- # end
253
- #
254
- # ExampleApi.foo[99].uri
255
- # => "www.example.com/api/v1/foo/99"
256
- def method_missing(part, *args, &block)
257
- @urn_parts << part.to_s
258
- self
259
- end
3
+ module HttpMagic
260
4
  end
@@ -0,0 +1,233 @@
1
+ require 'json'
2
+ require 'http_magic/uri'
3
+ require 'http_magic/request'
4
+
5
+ module HttpMagic
6
+ # A magical class that interacts with HTTP resources with a minimal amount of
7
+ # configuration.
8
+ #
9
+ # Assuming an api where the url http://www.example.com/api/v1/foo/99 responds
10
+ # with the following.
11
+ #
12
+ # Header:
13
+ #
14
+ # Content-Type: application/json
15
+ #
16
+ # Body:
17
+ #
18
+ # {
19
+ # "name": "Foo Bar"
20
+ # }
21
+ #
22
+ # == Example
23
+ #
24
+ # class ExampleApi < HttpMagic::Api
25
+ # url 'www.example.com'
26
+ # namespace 'api/v1'
27
+ # headers({'X-AuthToken' => 'token'})
28
+ # end
29
+ #
30
+ # ExampleApi.foo[99].get['name']
31
+ # => "Foo Bar"
32
+ #
33
+ # ExampleApi.foo.create.post(name: 'New Foo')
34
+ # => { 'name' => 'New Foo' }
35
+ class Api < BasicObject
36
+ # Makes the new method private so that instances of this class and it's
37
+ # children can only be instantiated through the class method method_missing.
38
+ # This is needed to enforce the intended usage of HttpMagic where the class
39
+ # is configured to represent an http location. Once it is configured, the
40
+ # location is interacted with dynamically with chained methods that correspond
41
+ # with parts of URNs found at the location.
42
+ class << self
43
+ private :new
44
+ end
45
+
46
+ # Sets or returns the request headers for an HttpMagic based class. The
47
+ # headers will be passed along to each resource request being made.
48
+ #
49
+ # == Example
50
+ #
51
+ # class ExampleApi < HttpMagic:Api
52
+ # url 'www.example.com'
53
+ # headers({'X-AuthToken' => 'token'})
54
+ # end
55
+ def self.headers(value = :not_provided)
56
+ unless value == :not_provided
57
+ @headers = value
58
+ end
59
+ @headers
60
+ end
61
+
62
+ # Sets or returns the namespace for an HttpMagic based class. The namespace
63
+ # will be prefixed to the urn for each request made to the url.
64
+ #
65
+ # Assuming an api where each resource is namespaced with 'api/v1' and where
66
+ # the url http://www.example.com/api/v1/foo/99 responds with the following.
67
+ #
68
+ # Header:
69
+ #
70
+ # Content-Type: application/json
71
+ #
72
+ # Body:
73
+ #
74
+ # {
75
+ # "name": "Foo Bar"
76
+ # }
77
+ #
78
+ # == Example
79
+ #
80
+ # class ExampleApi < HttpMagic:Api
81
+ # url 'www.example.com'
82
+ # namespace 'api/v1'
83
+ # end
84
+ #
85
+ # ExampleApi.foo[99].get['name']
86
+ # => "Foo Bar"
87
+ #
88
+ def self.namespace(value = :not_provided)
89
+ unless value == :not_provided
90
+ @namespace = value
91
+ end
92
+ @namespace
93
+ end
94
+
95
+ # Sets or returns the uniform resource locator for the HTTP resource.
96
+ #
97
+ # == Example
98
+ #
99
+ # class ExampleApi < HttpMagic::Api
100
+ # url 'www.example.com'
101
+ # end
102
+ def self.url(value = :not_provided)
103
+ unless value == :not_provided
104
+ @url = value
105
+ end
106
+ @url
107
+ end
108
+
109
+ # Class scoped method_missing that starts the magic of creating urns
110
+ # through meta programming by instantiating an instance of the class
111
+ # and delegating the first part of the urn to that instance. This method
112
+ # is an implementation of the Factory Method design pattern.
113
+ def self.method_missing(name, *args, &block)
114
+ new(@url, @namespace, @headers).__send__(name, *args)
115
+ end
116
+
117
+ def initialize(url, namespace, headers)
118
+ @uri = Uri.new(url)
119
+ @uri.namespace = namespace
120
+ @headers = headers
121
+ end
122
+
123
+ # Resource index reference intended to allow for the use of numbers in a urn
124
+ # such as the following 'foo/99' being referenced by ExampleApi.foo[99]. It
125
+ # can also be used to allow for HttpMagic methods to be specified for a urn
126
+ # such as 'foo/get' being referenced by ExampleApi.foo[:get]. Finally, it can
127
+ # be used for urn parts that are not valid Ruby methods such as 'foo/%5B%5D'
128
+ # being referenced by ExampleApi.foo["%5B%5D"].
129
+ #
130
+ # * part - a part of a urn such that 'foo' and 'bar' would be parts of the urn
131
+ # 'foo/bar'.
132
+ #
133
+ # Returns a reference to its instance so that urn parts can be chained
134
+ # together.
135
+ def [](part)
136
+ @uri.parts << part.to_s
137
+ self
138
+ end
139
+
140
+ def delete
141
+ request = Request.new(@uri,
142
+ headers: @headers
143
+ )
144
+ request.delete
145
+ end
146
+
147
+ # Gets a resource from the URI and returns it based on its content type.
148
+ # JSON content will be parsed and returned as equivalent Ruby objects. All
149
+ # other content types will be returned as text.
150
+ #
151
+ # Assuming an api where each resource is namespaced with 'api/v1' and where
152
+ # the url http://www.example.com/api/v1/foo/99 responds with the following.
153
+ #
154
+ # Header:
155
+ #
156
+ # Content-Type: application/json
157
+ #
158
+ # Body:
159
+ #
160
+ # {
161
+ # "name": "Foo Bar"
162
+ # }
163
+ #
164
+ # == Example
165
+ #
166
+ # class ExampleApi < HttpMagic::Api
167
+ # url 'www.example.com'
168
+ # namespace 'api/v1'
169
+ # end
170
+ #
171
+ # ExampleApi.foo[99].get
172
+ # => { "name" => "Foo Bar" }
173
+ def get
174
+ request = Request.new(@uri,
175
+ headers: @headers
176
+ )
177
+ request.get
178
+ end
179
+
180
+ # POST's a resource from the URI and returns the result based on its content
181
+ # type. JSON content will be parsed and returned as equivalent Ruby objects.
182
+ # All other content types will be returned as text.
183
+ #
184
+ # Assuming an api where each resource is namespaced with 'api/v1' and where
185
+ # the url http://www.example.com/api/v1/foo/create responds with the
186
+ # following when a 'name' is sent with the request.
187
+ #
188
+ # Header:
189
+ #
190
+ # Content-Type: application/json
191
+ #
192
+ # Body:
193
+ #
194
+ # {
195
+ # "name": "New Foo"
196
+ # }
197
+ #
198
+ # == Example
199
+ #
200
+ # class ExampleApi < HttpMagic::Api
201
+ # url 'www.example.com'
202
+ # namespace 'api/v1'
203
+ # end
204
+ #
205
+ # ExampleApi.foo.create.post(name: 'New Foo')
206
+ # => { "name" => "New Foo" }
207
+ def post(data = {})
208
+ request = Request.new(@uri,
209
+ headers: @headers,
210
+ data: data,
211
+ )
212
+ request.post
213
+ end
214
+
215
+ def put(data = {})
216
+ request = Request.new(@uri,
217
+ headers: @headers,
218
+ data: data,
219
+ )
220
+ request.put
221
+ end
222
+
223
+ # Instance scoped method_missing that accumulates the URN parts used in
224
+ # forming the URI. It returns a reference to the current instance so that
225
+ # method and [] based parts can be chained together to from the URN. In the
226
+ # case of ExampleApi.foo[99] the 'foo' method is accumlated as a URN part
227
+ # that will form the resulting URI.
228
+ def method_missing(part, *args, &block)
229
+ @uri.parts << part.to_s
230
+ self
231
+ end
232
+ end
233
+ end