gradesfirst 0.2.1 → 0.3.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.
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