clubhouse.io-ruby 0.1.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.
- 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
|