├── VERSION ├── .gitignore ├── lib ├── clubhouse2 │ ├── story_link.rb │ ├── profile.rb │ ├── branch.rb │ ├── category.rb │ ├── commit.rb │ ├── repository.rb │ ├── pull_request.rb │ ├── team.rb │ ├── state.rb │ ├── linked_file.rb │ ├── file.rb │ ├── epic_comment.rb │ ├── story_comment.rb │ ├── label.rb │ ├── task.rb │ ├── milestone.rb │ ├── workflow.rb │ ├── member.rb │ ├── project.rb │ ├── exceptions.rb │ ├── epic.rb │ ├── clubhouse_resource.rb │ ├── client.rb │ └── story.rb └── clubhouse2.rb ├── clubhouse2.gemspec └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.10 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | bin/ 3 | Gemfile.lock 4 | Gemfile 5 | 6 | -------------------------------------------------------------------------------- /lib/clubhouse2/story_link.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Storylink < ClubhouseResource 3 | def self.properties 4 | [ :object_id, :subject_id, :verb ] 5 | end 6 | 7 | def self.api_url 8 | 'story-links' 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/clubhouse2/profile.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Profile < ClubhouseResource 3 | def self.properties 4 | [ :deactivated, :name, :mention_name, :email_address, :gravatar_hash, :display_icon, :two_factor_auth_activated ] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/clubhouse2/branch.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Branch < ClubhouseResource 3 | def self.properties 4 | [ :created_at, :deleted, :entity_type, :merged_branch_ids, :id, :name, :persistent, :pull_requests, :repository_id, :updated_at, :url ] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/clubhouse2/category.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Category < ClubhouseResource 3 | attr_reader :archived, :color, :created_at, :entity_type, :external_id, :id, :name, :type, :updated_at 4 | 5 | def self.api_url 6 | 'categories' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/clubhouse2/commit.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Commit < ClubhouseResource 3 | def self.properties 4 | [ :author_email, :author_id, :author_identity, :created_at, :entity_type, :hash, :id, :merged_branch_ids, :message, :repository_id, :timestamp, :updated_at, :url ] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/clubhouse2/repository.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Repository < ClubhouseResource 3 | def self.properties 4 | [ :created_at, :entity_type, :external_id, :full_name, :id, :name, :type, :updated_at, :url ] 5 | end 6 | 7 | def self.api_url 8 | 'repositories' 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/clubhouse2/pull_request.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class PullRequest < ClubhouseResource 3 | def self.properties 4 | [ :branch_id, :closed, :entity_type, :created_at, :id, :num_added, :num_commits, :num_modified, :num_removed, :number, :target_branch_id, :title, :updated_at, :url ] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/clubhouse2/team.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Team < ClubhouseResource 3 | def self.properties 4 | [ 5 | :created_at, :description, :entity_type, :id, :name, :position, :project_ids, :updated_at, :workflow, 6 | :team_id, :updated_at 7 | ] 8 | end 9 | 10 | def self.api_url 11 | 'teams' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/clubhouse2/state.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class State < ClubhouseResource 3 | def self.properties 4 | [ 5 | :categories, :completed, :completed_at, :completed_at_override, :created_at, :description, :entity_type, 6 | :id, :name, :position, :started, :started_at, :started_at_override, :state, :updated_at 7 | ] 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/clubhouse2/linked_file.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Linkedfile < ClubhouseResource 3 | def self.properties 4 | [ 5 | :content_type, :created_at, :description, :entity_type, :id, :mention_ids, :name, 6 | :size, :story_ids, :thumbnail_url, :type, :updated_at, :uploader_id 7 | ] 8 | end 9 | 10 | def self.api_url 11 | 'linkedfiles' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/clubhouse2/file.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class File < ClubhouseResource 3 | def self.properties 4 | [ 5 | :content_type, :created_at, :description, :entity_type, :external_id, :filename, :id, :mention_ids, :name, 6 | :size, :story_ids, :thumbnail_url, :updated_at, :uploader_id, :url 7 | ] 8 | end 9 | 10 | def self.api_url 11 | 'files' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/clubhouse2/epic_comment.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Epiccomment < ClubhouseResource 3 | def self.properties 4 | [ :author_id, :comments, :created_at, :entity_type, :external_id, :id, :mention_ids, :position, :text, :updated_at, :epic_id ] 5 | end 6 | 7 | def self.api_url 8 | 'comments' 9 | end 10 | 11 | def api_url 12 | "#{self.api_url}/#{@epic_id}/#{id}" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/clubhouse2/story_comment.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Storycomment < ClubhouseResource 3 | def self.properties 4 | [ :author_id, :comments, :created_at, :entity_type, :external_id, :id, :mention_ids, :position, :story_id, :text, :updated_at ] 5 | end 6 | 7 | def self.api_url 8 | 'comments' 9 | end 10 | 11 | def api_url 12 | "#{self.api_url}/#{@story_id}/#{id}" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/clubhouse2/label.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Label < ClubhouseResource 3 | def self.properties 4 | [ :archived, :color, :created_at, :entity_type, :external_id, :id, :name, :stats, :updated_at ] 5 | end 6 | 7 | def self.api_url 8 | 'labels' 9 | end 10 | 11 | def stories 12 | @client.projects.collect(&:stories).reduce(:+).select { |s| s.labels.collect(&:id).include? @id } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/clubhouse2/task.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Task < ClubhouseResource 3 | def self.properties 4 | [ 5 | :complete, :completed_at, :created_at, :description, :entity_type, :external_id, :id, :mention_ids, :owner_ids, 6 | :position, :story_id, :updated_at 7 | ] 8 | end 9 | 10 | def self.api_url 11 | 'tasks' 12 | end 13 | 14 | def to_h 15 | super.reject { |k, v| [ :story_id ].include? k } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/clubhouse2/milestone.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Milestone < ClubhouseResource 3 | def self.properties 4 | [ 5 | :categories, :completed, :completed_at, :completed_at_override, :created_at, :description, :entity_type, 6 | :id, :name, :position, :started, :started_at, :started_at_override, :state, :updated_at 7 | ] 8 | end 9 | 10 | def self.api_url 11 | 'milestones' 12 | end 13 | 14 | def epics 15 | @client.epics.select { |e| e.milestone_id == @id } 16 | end 17 | 18 | def stories 19 | epics.collect(&:stories).reduce(:+) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/clubhouse2/workflow.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Workflow < ClubhouseResource 3 | def self.properties 4 | [ :created_at, :default_state_id, :description, :entity_type, :id, :name, :team_id, :updated_at ] 5 | end 6 | 7 | def initialize(client:, object:) 8 | super 9 | @states = [] 10 | object['states'].each do |this_state| 11 | this_state[:workflow_id] = @id 12 | @states << State.new(client: client, object: this_state) 13 | end 14 | end 15 | 16 | def self.api_url 17 | 'workflows' 18 | end 19 | 20 | def states(**args) 21 | @states.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) } 22 | end 23 | 24 | def state(**args); states(args).first; end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/clubhouse2/member.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Member < ClubhouseResource 3 | def self.properties 4 | [ :created_at, :disabled, :id, :profile, :role, :updated_at ] 5 | end 6 | 7 | def initialize(client:, object:) 8 | super 9 | @profile = Profile.new(client: client, object: @profile) 10 | 11 | # Create accessors for profile properties 12 | Profile.properties.each do |property| 13 | self.class.send(:define_method, (property.to_sym)) { @profile.send(property) } 14 | end 15 | end 16 | 17 | def self.api_url 18 | 'members' 19 | end 20 | 21 | def stories_requested 22 | @client.projects.collect(&:stories).reduce(:+).select { |s| s.requested_by_id == @id } 23 | end 24 | 25 | def stories_following 26 | @client.projects.collect(&:stories).reduce(:+).select { |s| s.follower_ids.include? @id } 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/clubhouse2.rb: -------------------------------------------------------------------------------- 1 | require 'clubhouse2/clubhouse_resource.rb' 2 | require 'clubhouse2/story_comment.rb' 3 | require 'clubhouse2/epic_comment.rb' 4 | require 'clubhouse2/linked_file.rb' 5 | require 'clubhouse2/story_link.rb' 6 | require 'clubhouse2/exceptions.rb' 7 | require 'clubhouse2/milestone.rb' 8 | require 'clubhouse2/category.rb' 9 | require 'clubhouse2/workflow.rb' 10 | require 'clubhouse2/project.rb' 11 | require 'clubhouse2/profile.rb' 12 | require 'clubhouse2/member.rb' 13 | require 'clubhouse2/client.rb' 14 | require 'clubhouse2/story.rb' 15 | require 'clubhouse2/label.rb' 16 | require 'clubhouse2/state.rb' 17 | require 'clubhouse2/team.rb' 18 | require 'clubhouse2/file.rb' 19 | require 'clubhouse2/task.rb' 20 | require 'clubhouse2/epic.rb' 21 | require 'clubhouse2/branch.rb' 22 | require 'clubhouse2/pull_request.rb' 23 | require 'clubhouse2/commit.rb' 24 | 25 | module Clubhouse 26 | end 27 | -------------------------------------------------------------------------------- /lib/clubhouse2/project.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Project < ClubhouseResource 3 | def self.properties 4 | [ 5 | :abbreviation, :archived, :color, :created_at, :days_to_thermometer, :description, :entity_type, :external_id, 6 | :follower_ids, :id, :iteration_length, :name, :show_thermometer, :start_time, :stats, :updated_at 7 | ] 8 | end 9 | 10 | def self.api_url 11 | 'projects' 12 | end 13 | 14 | def stories(**args) 15 | @stories ||= JSON.parse(@client.api_request(:get, @client.url("#{api_url}/stories"))).collect { |story| Story.new(client: @client, object: story) } 16 | @stories.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) } 17 | end 18 | 19 | def create_story(**args) 20 | @stories = nil 21 | args[:project_id] = @id 22 | Story.validate(**args) 23 | @client.create_object(:story, args) 24 | end 25 | 26 | def story(**args); stories(args).first; end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/clubhouse2/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class ClubhouseValidationError < StandardError 3 | def initialize(message) 4 | super('validation error: %s' % message) 5 | end 6 | end 7 | 8 | class ClubhouseAPIError < StandardError 9 | def initialize(response) 10 | super('api error (%d): %s' % [ response.code, response.to_s ]) 11 | end 12 | end 13 | 14 | class NoSuchMember < ClubhouseValidationError 15 | def initialize(member) 16 | super('no such member (%s)' % member) 17 | end 18 | end 19 | 20 | class NoSuchFile < ClubhouseValidationError 21 | def initialize(file) 22 | super('no such file (%s)' % file) 23 | end 24 | end 25 | 26 | class NoSuchLinkedFile < ClubhouseValidationError 27 | def initialize(file) 28 | super('no such linked file (%s)' % file) 29 | end 30 | end 31 | 32 | class NoSuchTeam < ClubhouseValidationError 33 | def initialize(team) 34 | super('no such team (%s)' % team) 35 | end 36 | end 37 | 38 | class NoSuchProject < ClubhouseValidationError 39 | def initialize(project) 40 | super('no such project (%s)' % project) 41 | end 42 | end 43 | 44 | class NoSuchMilestone < ClubhouseValidationError 45 | def initialize(milestone) 46 | super('no such milestone (%s)' % milestone) 47 | end 48 | end 49 | 50 | class NoSuchEpic < ClubhouseValidationError 51 | def initialize(epic) 52 | super('no such epic (%s)' % epic) 53 | end 54 | end 55 | end -------------------------------------------------------------------------------- /clubhouse2.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "clubhouse2" 7 | spec.version = File.read(File.expand_path(File.dirname(__FILE__)) + '/VERSION') 8 | spec.authors = ["James Denness"] 9 | spec.email = ["jd@masabi.com"] 10 | 11 | spec.summary = %q{Clubhouse library for API version 2} 12 | spec.description = %q{A resource-oriented library for working with the Cloubhouse API (v2)} 13 | spec.homepage = "https://github.com/Masabi/clubhouse2-ruby" 14 | spec.license = "GPL-3.0" 15 | 16 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 17 | # delete this section to allow pushing this gem to any host. 18 | if spec.respond_to?(:metadata) 19 | else 20 | raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 21 | end 22 | 23 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 24 | spec.bindir = 'bin' 25 | spec.require_paths = [ 'lib' ] 26 | spec.required_ruby_version = '>= 2.3.0' 27 | spec.add_dependency 'http', '~> 3' 28 | spec.add_dependency 'pry', '~> 0.10.4' 29 | spec.requirements << 'A clubhouse account (https://clubhouse.io)' 30 | 31 | spec.add_development_dependency "bundler", "~> 1.10" 32 | end 33 | -------------------------------------------------------------------------------- /lib/clubhouse2/epic.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Epic < ClubhouseResource 3 | def self.properties 4 | [ 5 | :archived, :comments, :completed, :completed_at, :completed_at_override, :created_at, :deadline, :description, 6 | :entity_type, :external_id, :follower_ids, :id, :labels, :milestone_id, :name, :owner_ids, :position, :project_ids, 7 | :started, :started_at, :started_at_override, :state, :stats, :updated_at 8 | ] 9 | end 10 | 11 | def self.api_url 12 | 'epics' 13 | end 14 | 15 | def stories 16 | @client.projects.collect(&:stories).reduce(:+).select { |s| s.epic_id == @id } 17 | end 18 | 19 | def validate(args) 20 | raise NoSuchTeam.NoSuchTeam(args[:team_id]) unless @client.get_team(id: args[:team_id]) 21 | 22 | (args[:follower_ids] || []).each do |this_member| 23 | raise NoSuchMember.NoSuchMember(this_member) unless @client.get_member(id: this_member) 24 | end 25 | 26 | (args[:owner_ids] || []).each do |this_member| 27 | raise NoSuchMember.NoSuchMember(this_member) unless @client.get_member(id: this_member) 28 | end 29 | end 30 | 31 | def comments(**args) 32 | # The API is missing a parent epic ID property, so we need to fake it here 33 | args[:epic_id] = @id 34 | @comments ||= JSON.parse(@client.api_request(:get, @client.url("#{api_url}/#{Epiccomment.api_url}"))).collect { |task| Epiccomment.new(client: @client, object: comment) } 35 | @comments.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) } 36 | end 37 | 38 | def to_h 39 | super.merge({ 40 | comments: @comments.collect(&:to_h) 41 | }) 42 | end 43 | 44 | def create_comment(**args) 45 | Task.validate(args) 46 | response = JSON.parse(@client.api_request(:post, @client.url("#{api_url}/#{Epiccomment.api_url}"), :json => args)) 47 | raise ClubhouseAPIError.new(response) unless response.code == 201 48 | @comments = nil 49 | response 50 | end 51 | 52 | def comment(**args); comments(args).first; end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/clubhouse2/clubhouse_resource.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class ClubhouseResource 3 | @@subclasses = [] 4 | 5 | def self.inherited(other) 6 | @@subclasses << other 7 | end 8 | 9 | # A list of properties to exlude from any create request 10 | def self.property_filter_create 11 | [ 12 | :archived, :days_to_thermometer, :entity_type, :id, :show_thermometer, :stats, :created_at, :updated_at, 13 | :started_at, :completed_at, :comments, :position, :started, :project_ids, :completed, :blocker, :moved_at, 14 | :task_ids, :files, :comment_ids, :workflow_state_id, :story_links, :mention_ids, :file_ids, :linked_file_ids, 15 | :tasks 16 | ] 17 | end 18 | 19 | # A list of properties to exlude from any update request 20 | def self.property_filter_update 21 | self.property_filter_create 22 | end 23 | 24 | def self.subclass(sub_class) 25 | @@subclasses.find { |s| s.name == 'Clubhouse::%s' % sub_class.capitalize } 26 | end 27 | 28 | def api_url 29 | self.class.api_url + "/#{@id}" 30 | end 31 | 32 | def self.validate(args); end 33 | 34 | def initialize(client:, object:) 35 | @client = client 36 | 37 | self.class.properties.each do |this_property| 38 | self.class.class_eval { attr_accessor(this_property.to_sym) } 39 | self.class.send(:define_method, (this_property.to_s + '=').to_sym) do |value| 40 | update({ this_property => resolve_to_ids(value) }) 41 | instance_variable_set('@' + this_property.to_s, resolve_to_ids(value)) 42 | end 43 | end 44 | 45 | set_properties(object) 46 | self 47 | end 48 | 49 | def resolve_to_ids(object) 50 | return object.collect { |o| resolve_to_ids(o) } if object.is_a? Array 51 | (object.respond_to?(:id) ? object.id : object) 52 | end 53 | 54 | def set_properties(object) 55 | object.each_pair do |k, v| 56 | instance_variable_set('@' + k.to_s, value_format(k, v)) 57 | end 58 | end 59 | 60 | def value_format(key, value) 61 | DateTime.strptime(value+'+0000', '%Y-%m-%dT%H:%M:%SZ%z') rescue value 62 | end 63 | 64 | # Empties resource cache 65 | def flush 66 | @client.flush(self.class) 67 | end 68 | 69 | def update(args = {}) 70 | new_params = args.reject { |k, v| self.class.property_filter_update.include? k.to_sym } 71 | validate(new_params) 72 | flush 73 | @client.api_request(:put, @client.url(api_url), :json => new_params) 74 | end 75 | 76 | def delete! 77 | flush 78 | @client.api_request(:delete, @client.url(api_url)) 79 | end 80 | 81 | def to_h 82 | Hash[ (self.class.properties - self.class.property_filter_create).map { |name| [ name, instance_variable_get('@' + name.to_s) ] } ].compact 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/clubhouse2/client.rb: -------------------------------------------------------------------------------- 1 | require 'http' 2 | require 'uri' 3 | require 'json' 4 | 5 | module Clubhouse 6 | class Client 7 | def initialize(api_key:, base_url: 'https://api.clubhouse.io/api/v2/') 8 | @api_key = api_key 9 | @base_url = base_url 10 | @resources = {} 11 | end 12 | 13 | def url(endpoint) 14 | URI.join(@base_url, endpoint,'?token=%s' % @api_key) 15 | end 16 | 17 | def api_request(method, *params) 18 | response = HTTP.headers(content_type: 'application/json').send(method, *params) 19 | case response.code 20 | when 429 21 | sleep 30 22 | api_request(method, *params) 23 | when 200 24 | when 201 25 | else 26 | raise ClubhouseAPIError.new(response) 27 | end 28 | 29 | response 30 | end 31 | 32 | def flush(resource_class) 33 | @resources[resource_class] = nil 34 | end 35 | 36 | # Take all the provided properties, and filter out any resources that don't match. 37 | # If the value of a property is an object with an ID, match on that ID instead (makes for tidier queries) 38 | def filter(object_array, args) 39 | object_array.reject { |s| args.collect { |k, v| not resolve_to_ids([ *s.send(k) ]).include? resolve_to_ids(v) }.reduce(:|) } 40 | end 41 | 42 | def resolve_to_ids(object) 43 | return object.collect { |o| resolve_to_ids(o) } if object.is_a? Array 44 | (object.respond_to?(:id) ? object.id : object) 45 | end 46 | 47 | # or v.empty? if v.respond_to?(:empty?) 48 | def create_object(resource_class, args) 49 | this_class = Clubhouse::ClubhouseResource.subclass(resource_class) 50 | this_class.validate(args) 51 | flush(this_class) 52 | new_params = args.compact.reject { |k, v| this_class.property_filter_create.include? k.to_sym } 53 | response = api_request(:post, url(this_class.api_url), :json => new_params) 54 | JSON.parse(response.to_s) 55 | end 56 | 57 | def get_objects(resource_class, args = {}) 58 | this_class = Clubhouse::ClubhouseResource.subclass(resource_class) 59 | unless @resources[this_class] 60 | response = api_request(:get, url(this_class.api_url)) 61 | @resources[this_class] = JSON.parse(response.to_s).collect do |resource| 62 | this_class.new(client: self, object: resource) 63 | end 64 | end 65 | 66 | filter(@resources[this_class], args) 67 | end 68 | 69 | def get_object(resource_class, args = {}) 70 | get_objects(resource_class, args).first 71 | end 72 | 73 | # --- 74 | 75 | def create_milestone(**args); create_object(:milestone, args); end 76 | def milestones(**args); get_objects(:milestone, args); end 77 | def milestone(**args); get_object(:milestone, args); end 78 | 79 | def create_project(**args); create_object(:project, args); end 80 | def projects(**args); get_objects(:project, args); end 81 | def project(**args); get_object(:project, args); end 82 | 83 | def create_story(**args); create_object(:story, args); end 84 | def stories(**args) 85 | filter(get_objects(:project).collect(&:stories).flatten, args) 86 | end 87 | 88 | def story(**args); stories(**args).first; end 89 | 90 | def create_story_link(**args); create_object(:storylink, args); end 91 | def story_links(**args) 92 | filter(stories.collect(&:story_links).flatten, args) 93 | end 94 | 95 | def story_link(**args); story_links(**args).first; end 96 | 97 | def create_member(**args); create_object(:member, args); end 98 | def members(**args); get_objects(:member, args); end 99 | def member(**args); get_object(:member, args); end 100 | 101 | def create_team(**args); create_object(:team, args); end 102 | def teams(**args); get_objects(:team, args); end 103 | def team(**args); get_object(:team, args); end 104 | 105 | def create_epic(**args); create_object(:epic, args); end 106 | def epics(**args); get_objects(:epic, args); end 107 | def epic(**args); get_object(:epic, args); end 108 | 109 | def create_category(**args); create_object(:category, args); end 110 | def categories(**args); get_objects(:category, args); end 111 | def category(**args); get_object(:category, args); end 112 | 113 | def create_label(**args); create_object(:label, args); end 114 | def update_label(**args); update_object(:label, args); end 115 | def labels(**args); get_objects(:label, args); end 116 | def label(**args); get_object(:label, args); end 117 | 118 | def create_file(**args); create_object(:file, args); end 119 | def files(**args); get_objects(:file, args); end 120 | def file(**args); get_object(:file, args); end 121 | 122 | def create_linked_file(**args); create_object(:linkedfile, args); end 123 | def linked_files(**args); get_objects(:linkedfile, args); end 124 | def linked_file(**args); get_object(:linkedfile, args); end 125 | 126 | def create_workflow(**args); create_object(:workflow, args); end 127 | def workflows(**args); get_objects(:workflow, args); end 128 | def workflow(**args); get_object(:workflow, args); end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/clubhouse2/story.rb: -------------------------------------------------------------------------------- 1 | module Clubhouse 2 | class Story < ClubhouseResource 3 | def self.properties 4 | [ 5 | :archived, :blocker, :blocked, :comment_ids, :completed, :completed_at, :completed_at_override, :created_at, 6 | :deadline, :entity_type, :epic_id, :estimate, :external_id, :file_ids, :follower_ids, :id, 7 | :linked_file_ids, :moved_at, :name, :owner_ids, :position, :project_id, :requested_by_id, :started, 8 | :started_at, :started_at_override, :story_type, :task_ids, :updated_at, :workflow_state_id, :app_url 9 | ] 10 | end 11 | 12 | def self.api_url 13 | 'stories' 14 | end 15 | 16 | def validate(**args) 17 | raise NoSuchEpic.new(args[:epic_id]) unless @client.epic(id: args[:epic_id]) if args[:epic_id] 18 | raise NoSuchProject.new(args[:project_id]) unless @client.project(id: args[:project_id]) if args[:project_id] 19 | raise NoSuchMember.new(args[:requested_by_id]) unless @client.member(id: args[:requested_by_id]) if args[:requested_by_id] 20 | 21 | (args[:follower_ids] || []).each do |this_member| 22 | raise NoSuchMember.new(this_member) unless @client.member(id: this_member) 23 | end 24 | 25 | (args[:owner_ids] || []).each do |this_member| 26 | raise NoSuchMember.new(this_member) unless @client.member(id: this_member) 27 | end 28 | 29 | (args[:file_ids] || []).each do |this_file| 30 | raise NoSuchFile.new(this_file) unless @client.file(id: this_file) 31 | end 32 | 33 | (args[:linked_file_ids] || []).each do |this_linked_file| 34 | raise NoSuchLinkedFile.new(this_linked_file) unless @client.linked_file(id: this_linked_file) 35 | end 36 | 37 | (args[:story_links] || []).each do |this_linked_story| 38 | raise NoSuchLinkedStory.new(this_linked_story) unless @client.story(id: this_linked_story['subject_id']) 39 | end 40 | 41 | (args[:labels] || []).collect! do |this_label| 42 | this_label.is_a?(Label) ? this_label : @client.label(id: this_label['name']) 43 | end 44 | end 45 | 46 | def update(**args) 47 | # The API won't let us try to update the project ID without changing it 48 | args.delete(:project_id) if args[:project_id] == @project_id 49 | super 50 | end 51 | 52 | def create_task(**args) 53 | Task.validate(**args) 54 | @tasks = nil 55 | JSON.parse(@client.api_request(:post, @client.url("#{api_url}/#{Task.api_url}"), :json => args)) 56 | end 57 | 58 | def comments(**args) 59 | @comments ||= @comment_ids.collect do |this_comment_id| 60 | comment_data = JSON.parse(@client.api_request(:get, @client.url("#{api_url}/comments/#{this_comment_id}"))) 61 | Storycomment.new(client: @client, object: comment_data) 62 | end 63 | 64 | @comments.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) } 65 | end 66 | 67 | def story_links(**args) 68 | @story_link_objects ||= @story_links.collect do |this_story_link| 69 | link_data = JSON.parse(@client.api_request(:get, @client.url("#{Storylink.api_url}/#{this_story_link['id']}"))) 70 | link_data.reject { |k, v| v == @id} 71 | Storylink.new(client: @client, object: link_data) 72 | end 73 | 74 | @story_link_objects.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) } 75 | end 76 | 77 | def tasks(**args) 78 | @tasks ||= @task_ids.collect do |this_task_id| 79 | task_data = JSON.parse(@client.api_request(:get, @client.url("#{api_url}/tasks/#{this_task_id}"))) 80 | Task.new(client: @client, object: task_data) 81 | end 82 | 83 | @tasks.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) } 84 | end 85 | 86 | def linked_files(**args) 87 | @client.linked_files(story_ids: @id, **args) 88 | end 89 | 90 | def files(**args) 91 | @client.files(story_ids: @id, **args) 92 | end 93 | 94 | def labels(**args) 95 | @labels.collect { |l| @client.label(id: l['id'], **args) } 96 | end 97 | 98 | def to_h 99 | super.merge({ 100 | comments: [ *comments ].collect(&:to_h), 101 | tasks: [ *tasks ].collect(&:to_h), 102 | files: [ *files ].collect(&:to_h), 103 | story_links: [ *story_links ].collect(&:to_h), 104 | labels: [ *labels ].collect(&:to_h), 105 | branches: [ *branches ].collect(&:to_h), 106 | commits: [ *commits ].collect(&:to_h), 107 | }) 108 | end 109 | 110 | def full_story(**args) 111 | @full_story ||= begin 112 | JSON.parse(@client.api_request(:get, @client.url("#{Story.api_url}/#{id}"))) 113 | end 114 | end 115 | 116 | def commits(**args) 117 | @commits ||= begin 118 | full_story['commits'].collect do |commit_data| 119 | Commit.new(client: @client, object: commit_data) 120 | end 121 | end 122 | end 123 | 124 | def branches(**args) 125 | @branches ||= begin 126 | full_story['branches'].collect do |branch_data| 127 | Branch.new(client: @client, object: branch_data) 128 | end 129 | end 130 | end 131 | 132 | def create_comment(**args) 133 | Task.validate(**args) 134 | @comments = nil 135 | JSON.parse(@client.api_request(:post, @client.url("#{api_url}/#{Storycomment.api_url}"), :json => args)) 136 | end 137 | 138 | def comment(**args); comments(args).first; end 139 | def story_link(**args); story_links(args).first; end 140 | def task(**args); tasks(args).first; end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clubhouse API v2 Ruby Gem 2 | 3 | This is a resource-oriented ruby library for interacting with the Clubhouse v2 API. 4 | 5 | ## How to use 6 | 7 | ### Initializing the client 8 | ```ruby 9 | require 'clubhouse2' 10 | client = Clubhouse::Client.new(api_key: 'your_api_key') 11 | ``` 12 | 13 | ### Quick-start 14 | #### Queries 15 | Get all stories being followed by a user called 'James'. 16 | ```ruby 17 | client.stories(follower_ids: client.member(name: 'James')) 18 | ``` 19 | 20 | Get all stories in the 'Testing' project in the 'Completed' state. 21 | ```ruby 22 | client.project(name: 'Testing').stories(workflow_state_id: client.workflow.state(name: 'Completed')) 23 | ``` 24 | 25 | Get the names of all stories in the 'Testing' project 26 | ```ruby 27 | client.project(name: 'Testing').stories.collect(&:name) 28 | ``` 29 | 30 | Get all non-archived stories with the label 'Out Of Hours' 31 | ```ruby 32 | client.stories(archived: false, labels: client.label(name: 'Out Of Hours')) 33 | ``` 34 | 35 | Get all stories last updated more than 30 days ago 36 | ```ruby 37 | client.stories.select { |story| story.updated_at < Date.today - 30 } 38 | ``` 39 | 40 | Get a list of all story states in the default workflow 41 | ```ruby 42 | client.workflow.states 43 | ``` 44 | 45 | #### Creating resources 46 | See the official Clubhouse API documentation for valid properties to use here: 47 | https://clubhouse.io/api/rest/v2/ 48 | 49 | Create a new story in the 'Testing' project 50 | ```ruby 51 | client.project(name: 'Testing').create_story( **...** ) 52 | client.create_story(project_id: client.project(name: 'Testing'), **...** ) 53 | ``` 54 | 55 | #### Updating resources 56 | Updating a property of a resource can be achieved simply by using assignment operators, as shown in the examples below. 57 | 58 | See the official Clubhouse API documentation for valid properties to use here: 59 | https://clubhouse.io/api/rest/v2/ 60 | 61 | Change the name of a story 62 | ```ruby 63 | client.story(name: 'Old name').name = 'New name' 64 | client.story(id: 123).name = 'New name' 65 | ``` 66 | 67 | Add a new follower to a story 68 | ```ruby 69 | client.story(id: 123).follower_ids += [ client.member(name: 'Jeff') ] 70 | ``` 71 | 72 | Assign a story to an epic 73 | ```ruby 74 | client.story(id: 123).epic_id = client.epic(name: 'Awesome') 75 | ``` 76 | 77 | #### Deleting resources 78 | Deletion is possible by using the `delete!` method, which is available on most resources. Some resources can only be deleted from the web interface. 79 | 80 | Delete an epic 81 | ```ruby 82 | client.epic(id: 123).delete! 83 | ``` 84 | 85 | Delete all stories in the 'Testing' project 86 | ```ruby 87 | client.project(name: 'Testing').stories.each(&:delete!) 88 | ``` 89 | 90 | ### Methods returning arrays of resources 91 | ```ruby 92 | client.projects # list all projects 93 | client.milestones # list all milestones 94 | client.members # list all members (users) 95 | client.epics # list all epics 96 | client.stories # list all stories, comments and tasks [WARNING: slow!] 97 | client.categories # list all categories 98 | client.workflows # list all workflows and states 99 | client.labels # list all labels 100 | client.teams # list all teams 101 | client.story_links # list all story links 102 | ``` 103 | ### Methods returning single resources 104 | ```ruby 105 | client.project # list the first matching project 106 | client.milestone # list the first matching milestone 107 | client.member # list the first matching member (user) 108 | client.epic # list the first matching epic 109 | client.story # list the first matching story [WARNING: slow!] 110 | client.category # list the first matching category 111 | client.workflow # list the first matching workflow (usually Default) 112 | client.label # list the first matching label 113 | client.team # list the first matching team 114 | client.story_link # list the first matching story link 115 | ``` 116 | 117 | ### Creation methods 118 | ```ruby 119 | client.create_project # create a project 120 | client.create_milestone # create a milestone 121 | client.create_member # create a member 122 | client.create_epic # create an epic 123 | client.create_story # create a story 124 | client.create_category # create a category 125 | client.create_workflow # create a workflow 126 | client.create_label # create a label 127 | client.create_team # create a team 128 | client.create_story_link # create a story link 129 | client.story.create_comment # create a comment for a story 130 | client.story.create_task # create a task for a story 131 | client.epic.create_comment # create a comment for an epic 132 | ``` 133 | ### Update methods 134 | ```ruby 135 | client.update_project # update a project 136 | client.update_milestone # update a milestone 137 | client.update_member # update a member 138 | client.update_epic # update an epic 139 | client.update_story # update a story 140 | client.update_category # update a category 141 | client.update_workflow # update a workflow 142 | client.update_label # update a label 143 | client.update_team # update a team 144 | client.update_story_link # update a story link 145 | ``` 146 | 147 | ### Filtering 148 | It's possible to filter by any resource property provided by the API. Multiple property filters can be specified. Filters match any member of an array, for example you can filter `stories` by `follower_ids`, which will match any stories for which the given member, or members, are followers. 149 | ```ruby 150 | client.project(id: 123) # get a specific project 151 | client.project(name: 'blah') # get a project by name 152 | client.projects(archived: true) # get all archived projects 153 | client.project(id: 123).stories # get stories belonging to a project 154 | client.story(archived: false) # get all non-archived stories 155 | ``` 156 | ### Notes 157 | Note that querying for stories is quicker when performed on a Project, rather than using the `client.projects` method. This is because stories are only available as children of a project, so building the global story array requires making an API call to every project. --------------------------------------------------------------------------------