clubhouse.io-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/clubhouse.io-ruby.gemspec +26 -0
- data/docs/comments.md +64 -0
- data/docs/epics.md +64 -0
- data/docs/files.md +48 -0
- data/docs/labels.md +49 -0
- data/docs/linked_files.md +64 -0
- data/docs/projects.md +74 -0
- data/docs/stories.md +96 -0
- data/docs/story_links.md +39 -0
- data/docs/tasks.md +66 -0
- data/docs/users.md +26 -0
- data/docs/workflows.md +17 -0
- data/lib/clubhouse.rb +34 -0
- data/lib/clubhouse/api_actions.rb +10 -0
- data/lib/clubhouse/base_resource.rb +108 -0
- data/lib/clubhouse/client.rb +83 -0
- data/lib/clubhouse/comment.rb +45 -0
- data/lib/clubhouse/epic.rb +38 -0
- data/lib/clubhouse/ext/string.rb +9 -0
- data/lib/clubhouse/file.rb +19 -0
- data/lib/clubhouse/label.rb +18 -0
- data/lib/clubhouse/linked_file.rb +13 -0
- data/lib/clubhouse/project.rb +20 -0
- data/lib/clubhouse/story.rb +70 -0
- data/lib/clubhouse/story_link.rb +17 -0
- data/lib/clubhouse/task.rb +45 -0
- data/lib/clubhouse/user.rb +34 -0
- data/lib/clubhouse/version.rb +3 -0
- data/lib/clubhouse/workflow.rb +42 -0
- metadata +137 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class BadRequestError < StandardError; end
|
3
|
+
class UnauthorizedError < StandardError; end
|
4
|
+
class ResourceNotFoundError < StandardError; end
|
5
|
+
class UnexpectedError < StandardError; end
|
6
|
+
class UnprocessableError < StandardError; end
|
7
|
+
|
8
|
+
class Client
|
9
|
+
include APIActions
|
10
|
+
|
11
|
+
API_VERSION='v1'.freeze
|
12
|
+
|
13
|
+
def initialize(token)
|
14
|
+
@token = token
|
15
|
+
end
|
16
|
+
|
17
|
+
def basepath
|
18
|
+
@basepath ||= "https://api.clubhouse.io/api/#{API_VERSION}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(resource)
|
22
|
+
req = Net::HTTP::Get.new(build_uri(resource))
|
23
|
+
do_request(req)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(resource)
|
27
|
+
req = Net::HTTP::Delete.new(build_uri(resource))
|
28
|
+
do_request(req)
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(resource, body = {})
|
32
|
+
req = Net::HTTP::Post.new(build_uri(resource))
|
33
|
+
req['Content-Type'] = 'application/json'
|
34
|
+
req.body = body.to_json
|
35
|
+
|
36
|
+
do_request(req)
|
37
|
+
end
|
38
|
+
|
39
|
+
def put(resource, body = {})
|
40
|
+
req = Net::HTTP::Put.new(build_uri(resource))
|
41
|
+
req['Content-Type'] = 'application/json'
|
42
|
+
req.body = body.to_json
|
43
|
+
|
44
|
+
do_request(req)
|
45
|
+
end
|
46
|
+
|
47
|
+
def raise_error_to_user(response)
|
48
|
+
code = response.code.to_i
|
49
|
+
|
50
|
+
err = case code
|
51
|
+
when 400 then BadRequestError
|
52
|
+
when 401, 403 then UnauthorizedError
|
53
|
+
when 404 then ResourceNotFoundError
|
54
|
+
when 422 then UnprocessableError
|
55
|
+
else UnexpectedError
|
56
|
+
end
|
57
|
+
|
58
|
+
raise err, response.body
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def build_uri(resource)
|
64
|
+
uri = URI.parse("#{basepath}/#{resource}")
|
65
|
+
uri.query = URI.encode_www_form(token: @token)
|
66
|
+
uri
|
67
|
+
end
|
68
|
+
|
69
|
+
def do_request(request)
|
70
|
+
http = Net::HTTP.new(request.uri.host, request.uri.port)
|
71
|
+
http.use_ssl = true
|
72
|
+
response = http.request(request)
|
73
|
+
|
74
|
+
if [200,201,204].include?(response.code.to_i)
|
75
|
+
# Clubhouse API returns content type as application/json for DELETE request with no body :(
|
76
|
+
return {} if response.body.nil?
|
77
|
+
JSON.parse(response.body)
|
78
|
+
else
|
79
|
+
raise_error_to_user(response)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Comment < BaseResource
|
3
|
+
resource :comments
|
4
|
+
|
5
|
+
attributes :author_id, :created_at, :external_id, :story_id, :text, :updated_at,
|
6
|
+
readonly: [:id, :mention_ids, :position]
|
7
|
+
|
8
|
+
attributes_for_create :author_id, :created_at, :external_id, :text, :updated_at
|
9
|
+
attributes_for_update :text
|
10
|
+
|
11
|
+
def save
|
12
|
+
raise MissingStoryIDError, 'story_id is required to create/update comments' unless story_id
|
13
|
+
raise ClientNotSetup, "A default client or instance client is not setup" unless client
|
14
|
+
|
15
|
+
payload = if id
|
16
|
+
client.put("stories/#{story_id}/#{self.class.endpoint}/#{id}", update_attributes)
|
17
|
+
else
|
18
|
+
client.post("stories/#{story_id}/#{self.class.endpoint}", create_attributes)
|
19
|
+
end
|
20
|
+
|
21
|
+
update_object_from_payload(payload)
|
22
|
+
end
|
23
|
+
|
24
|
+
def reload
|
25
|
+
payload = client.get("stories/#{story_id}/#{self.class.endpoint}/#{id}")
|
26
|
+
update_object_from_payload(payload)
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def find(story_id, comment_id)
|
31
|
+
payload = client.get("stories/#{story_id}/#{endpoint}/#{comment_id}")
|
32
|
+
new.update_object_from_payload(payload)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(story_id, comment_id)
|
36
|
+
client.delete("stories/#{story_id}/#{endpoint}/#{comment_id}")
|
37
|
+
end
|
38
|
+
|
39
|
+
def all
|
40
|
+
raise NotSupportedByAPIError,
|
41
|
+
"You can get all comments associated directly from the story model"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Epic < BaseResource
|
3
|
+
resource :epics
|
4
|
+
|
5
|
+
attributes :archived, :created_at, :deadline, :description, :external_id,
|
6
|
+
:follower_ids, :name, :owner_ids, :state, :updated_at, readonly: [:id, :comments, :position]
|
7
|
+
|
8
|
+
attributes_for_create :created_at, :deadline, :description, :external_id,
|
9
|
+
:follower_ids, :name, :owner_ids, :state, :updated_at
|
10
|
+
|
11
|
+
attributes_for_update :after_id, :archived, :before_id, :deadline,
|
12
|
+
:description, :follower_ids, :name, :owner_ids, :state
|
13
|
+
|
14
|
+
def comments
|
15
|
+
@_comments ||= Array(@comments).collect{|c| Comment.new.update_object_from_payload(c) }
|
16
|
+
end
|
17
|
+
|
18
|
+
class Comment < BaseResource
|
19
|
+
attributes :text, readonly: [:author_id, :created_at, :updated_at, :deleted, :id, :comments]
|
20
|
+
|
21
|
+
def comments
|
22
|
+
@_comments ||= Array(@comments).collect {|c| Comment.new.update_object_from_payload(c) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def save
|
26
|
+
raise NotImplemented
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.find(id = nil)
|
30
|
+
raise NotImplemented
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.all
|
34
|
+
raise NotImplemented
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class File < BaseResource
|
3
|
+
resource :files
|
4
|
+
|
5
|
+
attributes :created_at, :description, :external_id, :name, :updated_at, :uploader_id,
|
6
|
+
readonly: [:content_type, :filename, :id, :mention_ids, :size, :story_ids, :thumbnail_url, :url]
|
7
|
+
|
8
|
+
attributes_for_update :description, :external_id, :name, :updated_at, :uploader_id
|
9
|
+
|
10
|
+
def save
|
11
|
+
if id.nil?
|
12
|
+
raise NotSupportedByAPIError,
|
13
|
+
"You can't create a direct file upload, but can create linked files over the API"
|
14
|
+
end
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Label < BaseResource
|
3
|
+
resource :labels
|
4
|
+
|
5
|
+
attributes :name, :external_id, readonly: [
|
6
|
+
:id,
|
7
|
+
:num_stories_in_progress,
|
8
|
+
:num_stories_total,
|
9
|
+
:num_stories_completed,
|
10
|
+
:created_at,
|
11
|
+
:updated_at,
|
12
|
+
:num_stories_completed
|
13
|
+
]
|
14
|
+
|
15
|
+
attributes_for_create :name, :external_id
|
16
|
+
attributes_for_update :name
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class LinkedFile < BaseResource
|
3
|
+
resource 'linked-files'
|
4
|
+
|
5
|
+
attributes :content_type, :description, :name, :size, :story_id, :thumbnail_url,
|
6
|
+
:uploader_id, :url, :type, readonly: [:created_at, :id, :mention_ids, :story_ids, :updated_at]
|
7
|
+
|
8
|
+
attributes_for_create :content_type, :description, :name, :size, :story_id,
|
9
|
+
:thumbnail_url, :type, :uploader_id, :url
|
10
|
+
|
11
|
+
attributes_for_update :description, :name, :size, :thumbnail_url, :type, :uploader_id, :url
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Project < BaseResource
|
3
|
+
resource :projects
|
4
|
+
|
5
|
+
attributes :abbreviation, :archived, :color, :created_at, :description, :external_id,
|
6
|
+
:follower_ids, :name, :updated_at, readonly: [:archived, :id, :num_points, :num_stories]
|
7
|
+
|
8
|
+
attributes_for_create :abbreviation, :color, :created_at, :description,
|
9
|
+
:external_id, :follower_ids, :name, :updated_at
|
10
|
+
|
11
|
+
attributes_for_update :abbreviation, :archived, :color, :description, :follower_ids, :name
|
12
|
+
|
13
|
+
def stories
|
14
|
+
return [] if id.nil?
|
15
|
+
|
16
|
+
payload = client.get("projects/#{id}/stories")
|
17
|
+
Array(payload).collect{|s| Story.new.update_object_from_payload(s) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Story < BaseResource
|
3
|
+
resource :stories
|
4
|
+
|
5
|
+
attributes :archived, :comments, :created_at, :deadline, :description, :epic_id, :estimate,
|
6
|
+
:file_ids, :follower_ids, :labels, :linked_file_ids, :name, :owner_ids,
|
7
|
+
:position, :project_id, :requested_by_id, :story_links, :story_type, :tasks,
|
8
|
+
:updated_at, :workflow_state_id, readonly: :id
|
9
|
+
|
10
|
+
attributes_for_create :comments, :created_at, :deadline, :description, :epic_id, :estimate,
|
11
|
+
:external_id, :file_ids, :labels, :linked_file_ids, :name, :owner_ids,
|
12
|
+
:project_id, :requested_by_id, :story_links, :story_type, :tasks,
|
13
|
+
:updated_at, :workflow_state_id
|
14
|
+
|
15
|
+
attributes_for_update :after_id, :archived, :before_id, :deadline, :description, :epic_id,
|
16
|
+
:estimate, :file_ids, :follower_ids, :labels, :linked_file_ids, :name,
|
17
|
+
:owner_ids, :project_id, :requested_by_id, :story_type, :workflow_state_id
|
18
|
+
|
19
|
+
|
20
|
+
def comments
|
21
|
+
@_comments ||= Array(@comments).collect {|c| Comment.new.update_object_from_payload(c) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def tasks
|
25
|
+
@_tasks ||= Array(@tasks).collect {|t| Task.new.update_object_from_payload(t) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_comment(text)
|
29
|
+
add_resource(:comment) do
|
30
|
+
comment = Comment.new(text: text, story_id: id)
|
31
|
+
comment.save
|
32
|
+
comments << comment
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_task(desc)
|
37
|
+
add_resource(:task) do
|
38
|
+
task = Task.new(description: desc, story_id: id)
|
39
|
+
task.save
|
40
|
+
tasks << task
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def update_object_from_payload(attr = {})
|
45
|
+
super
|
46
|
+
instance_variable_set("@_comments", nil)
|
47
|
+
instance_variable_set("@_tasks", nil)
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
class << self
|
52
|
+
def all
|
53
|
+
raise NotSupportedByAPIError,
|
54
|
+
'Use Story.search(..) to return stories matching your search query'
|
55
|
+
end
|
56
|
+
|
57
|
+
def search(attr = {})
|
58
|
+
payload = client.post("#{endpoint}/search", attr)
|
59
|
+
payload.collect {|s| new.update_object_from_payload(s) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def add_resource(type)
|
66
|
+
raise StoryNotSavedError, "Please save the story to use the add #{type} method" unless id
|
67
|
+
yield
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class StoryLink < BaseResource
|
3
|
+
resource 'story-links'
|
4
|
+
|
5
|
+
attributes :object_id, :subject_id, :verb, readonly: [ :created_at, :id, :updated_at ]
|
6
|
+
attributes_for_create :object_id, :subject_id, :verb
|
7
|
+
|
8
|
+
def save
|
9
|
+
raise NotSupportedByAPIError, "You can't update a story link" if id
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.all
|
14
|
+
raise NotSupportedByAPIError, "Not supported by the API"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Task < BaseResource
|
3
|
+
resource :tasks
|
4
|
+
|
5
|
+
attributes :complete, :created_at, :description, :external_id, :owner_ids,
|
6
|
+
:updated_at, :story_id, readonly: [:completed_at, :id, :mention_ids, :position]
|
7
|
+
|
8
|
+
attributes_for_create :complete, :created_at, :description, :external_id, :owner_ids, :updated_at
|
9
|
+
attributes_for_update :after_id, :before_id, :complete, :description, :owner_ids
|
10
|
+
|
11
|
+
|
12
|
+
def save
|
13
|
+
raise MissingStoryIDError, 'story_id is required to create/update tasks' unless story_id
|
14
|
+
raise ClientNotSetup, "A default client or instance client is not setup" unless client
|
15
|
+
|
16
|
+
payload = if id
|
17
|
+
client.put("stories/#{story_id}/#{self.class.endpoint}/#{id}", update_attributes)
|
18
|
+
else
|
19
|
+
client.post("stories/#{story_id}/#{self.class.endpoint}", create_attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
update_object_from_payload(payload)
|
23
|
+
end
|
24
|
+
|
25
|
+
def reload
|
26
|
+
payload = client.get("stories/#{story_id}/#{self.class.endpoint}/#{id}")
|
27
|
+
update_object_from_payload(payload)
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def find(story_id, task_id)
|
32
|
+
payload = client.get("stories/#{story_id}/#{endpoint}/#{task_id}")
|
33
|
+
new.update_object_from_payload(payload)
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete(story_id, task_id)
|
37
|
+
client.delete("stories/#{story_id}/tasks/#{task_id}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def all
|
41
|
+
raise NotSupportedByAPIError, "You can get all tasks associated directly from the story model"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class User < BaseResource
|
3
|
+
resource :users
|
4
|
+
|
5
|
+
attributes :name, readonly: [ :deactivated, :id, :name, :permissions,
|
6
|
+
:two_factor_auth_activated, :username]
|
7
|
+
|
8
|
+
Permission = Struct.new(:created_at, :disabled, :email_address,
|
9
|
+
:gravatar_hash, :id, :initials, :role, :updated_at)
|
10
|
+
|
11
|
+
def save
|
12
|
+
raise NotSupportedByAPIError, "You can't create users over the API, please use clubhouse web"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.delete(id = nil)
|
16
|
+
raise NotSupportedByAPIError, "You can't delete users over the API, please use clubhouse web"
|
17
|
+
end
|
18
|
+
|
19
|
+
def permissions
|
20
|
+
@_permissions ||= Array(@permissions).collect do |p|
|
21
|
+
Permission.new(
|
22
|
+
p['created_at'],
|
23
|
+
p['disabled'],
|
24
|
+
p['email_address'],
|
25
|
+
p['gravatar_hash'],
|
26
|
+
p['id'],
|
27
|
+
p['initials'],
|
28
|
+
p['role'],
|
29
|
+
p['updated_at']
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Workflow < BaseResource
|
3
|
+
resource :workflows
|
4
|
+
|
5
|
+
attributes :default_state_id, readonly: [:id, :created_at, :states, :updated_at]
|
6
|
+
|
7
|
+
State = Struct.new(:color, :created_at, :description, :id, :name,
|
8
|
+
:num_stories, :position, :type, :updated_at, :verb)
|
9
|
+
|
10
|
+
def save
|
11
|
+
raise NotSupportedByAPIError,
|
12
|
+
"You can't manage workflows over the API, please use clubhouse web"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.delete(id = nil)
|
16
|
+
raise NotSupportedByAPIError,
|
17
|
+
"You can't delete workflows over the API, please use clubhouse web"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.find(id = nil)
|
21
|
+
raise NotSupportedByAPIError,
|
22
|
+
"You can only list all workflows, please use Clubhouse::Workflow.all"
|
23
|
+
end
|
24
|
+
|
25
|
+
def states
|
26
|
+
@_states ||= Array(@states).collect do |s|
|
27
|
+
State.new(
|
28
|
+
s['color'],
|
29
|
+
s['created_at'],
|
30
|
+
s['description'],
|
31
|
+
s['id'],
|
32
|
+
s['name'],
|
33
|
+
s['num_stories'],
|
34
|
+
s['position'],
|
35
|
+
s['type'],
|
36
|
+
s['updated_at'],
|
37
|
+
s['verb']
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|