wip-ruby 0.2.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/.simplecov +36 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +846 -0
- data/Rakefile +45 -0
- data/lib/wip/client.rb +84 -0
- data/lib/wip/configuration.rb +89 -0
- data/lib/wip/error.rb +76 -0
- data/lib/wip/http_client.rb +221 -0
- data/lib/wip/models/base.rb +160 -0
- data/lib/wip/models/collection.rb +81 -0
- data/lib/wip/models/comment.rb +28 -0
- data/lib/wip/models/concerns/reactable.rb +25 -0
- data/lib/wip/models/project.rb +48 -0
- data/lib/wip/models/reaction.rb +32 -0
- data/lib/wip/models/todo.rb +57 -0
- data/lib/wip/models/user.rb +51 -0
- data/lib/wip/resources/base.rb +80 -0
- data/lib/wip/resources/comments.rb +93 -0
- data/lib/wip/resources/projects.rb +42 -0
- data/lib/wip/resources/reactions.rb +74 -0
- data/lib/wip/resources/todos.rb +47 -0
- data/lib/wip/resources/uploads.rb +111 -0
- data/lib/wip/resources/users.rb +61 -0
- data/lib/wip/resources/viewer.rb +52 -0
- data/lib/wip/version.rb +5 -0
- data/lib/wip-ruby.rb +1 -0
- data/lib/wip.rb +51 -0
- data/sig/wip/ruby.rbs +6 -0
- data/test_examples.rb +435 -0
- metadata +119 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wip
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a paginated collection of items
|
|
6
|
+
class Collection
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
# @return [Array<Base>] The items in the collection
|
|
10
|
+
attr_reader :data
|
|
11
|
+
|
|
12
|
+
# @return [Boolean] Whether there are more items
|
|
13
|
+
attr_reader :has_more
|
|
14
|
+
|
|
15
|
+
# @return [Integer] The total count of items
|
|
16
|
+
attr_reader :total_count
|
|
17
|
+
|
|
18
|
+
# Initialize a new collection
|
|
19
|
+
# @param data [Array<Base>] The items in the collection
|
|
20
|
+
# @param has_more [Boolean] Whether there are more items
|
|
21
|
+
# @param total_count [Integer] The total count of items
|
|
22
|
+
def initialize(data:, has_more:, total_count:)
|
|
23
|
+
@data = data
|
|
24
|
+
@has_more = has_more
|
|
25
|
+
@total_count = total_count
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create a new collection from a JSON response
|
|
29
|
+
# @param json [Hash] The JSON response
|
|
30
|
+
# @param item_class [Class] The class to instantiate items as
|
|
31
|
+
# @return [Collection] The new collection
|
|
32
|
+
# @raise [ArgumentError] If json is nil or not a Hash
|
|
33
|
+
def self.from_json(json, item_class:)
|
|
34
|
+
raise ArgumentError, "Expected Hash, got #{json.class}" unless json.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
data = json["data"]
|
|
37
|
+
raise ArgumentError, "Missing 'data' key in response" unless data.is_a?(Array)
|
|
38
|
+
|
|
39
|
+
new(
|
|
40
|
+
data: data.map { |item| item_class.from_json(item) },
|
|
41
|
+
has_more: json.fetch("has_more", false),
|
|
42
|
+
total_count: json["total_count"]
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Iterate over the items in the collection
|
|
47
|
+
# @yield [item] Each item in the collection
|
|
48
|
+
# @return [Enumerator] If no block is given
|
|
49
|
+
def each(&block)
|
|
50
|
+
data.each(&block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Integer] The number of items in the collection
|
|
54
|
+
def size
|
|
55
|
+
data.size
|
|
56
|
+
end
|
|
57
|
+
alias length size
|
|
58
|
+
alias count size
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] Whether the collection is empty
|
|
61
|
+
def empty?
|
|
62
|
+
data.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Base, nil] The first item in the collection
|
|
66
|
+
def first
|
|
67
|
+
data.first
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Base, nil] The last item in the collection
|
|
71
|
+
def last
|
|
72
|
+
data.last
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [String, nil] The ID of the last item (for pagination)
|
|
76
|
+
def last_id
|
|
77
|
+
last&.id
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "user"
|
|
5
|
+
require_relative "concerns/reactable"
|
|
6
|
+
|
|
7
|
+
module Wip
|
|
8
|
+
module Models
|
|
9
|
+
# Represents a comment on a todo
|
|
10
|
+
class Comment < Base
|
|
11
|
+
include Concerns::Reactable
|
|
12
|
+
|
|
13
|
+
attribute :id, type: :string
|
|
14
|
+
attribute :body, type: :string
|
|
15
|
+
attribute :created_at, type: :time
|
|
16
|
+
attribute :updated_at, type: :time
|
|
17
|
+
attribute :url, type: :string
|
|
18
|
+
attribute :reactions_count, type: :integer
|
|
19
|
+
attribute :viewer, type: Hash # Hash with reactions array
|
|
20
|
+
attribute :creator, type: User
|
|
21
|
+
|
|
22
|
+
# @return [String] String representation of the comment
|
|
23
|
+
def to_s
|
|
24
|
+
body
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wip
|
|
4
|
+
module Models
|
|
5
|
+
module Concerns
|
|
6
|
+
# Shared behavior for models that can have reactions (Todo, Comment)
|
|
7
|
+
module Reactable
|
|
8
|
+
# @return [Boolean] Whether this resource has any reactions
|
|
9
|
+
def reactions?
|
|
10
|
+
reactions_count&.positive?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [Array<Hash>] The viewer's reactions on this resource (empty if none)
|
|
14
|
+
def viewer_reactions
|
|
15
|
+
viewer&.dig("reactions") || []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Boolean] Whether the authenticated user has reacted to this resource
|
|
19
|
+
def reacted_by_viewer?
|
|
20
|
+
viewer_reactions.any?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "user"
|
|
5
|
+
|
|
6
|
+
module Wip
|
|
7
|
+
module Models
|
|
8
|
+
# Represents a project
|
|
9
|
+
class Project < Base
|
|
10
|
+
attribute :id, type: :string
|
|
11
|
+
attribute :slug, type: :string
|
|
12
|
+
attribute :name, type: :string
|
|
13
|
+
attribute :pitch, type: :string
|
|
14
|
+
attribute :description, type: :string
|
|
15
|
+
attribute :created_at, type: :time
|
|
16
|
+
attribute :updated_at, type: :time
|
|
17
|
+
attribute :hashtag, type: :string
|
|
18
|
+
attribute :website_url, type: :string
|
|
19
|
+
attribute :protected, type: :boolean
|
|
20
|
+
attribute :archived, type: :boolean
|
|
21
|
+
attribute :url, type: :string
|
|
22
|
+
attribute :logo, type: Hash # Hash with small, medium, large URLs
|
|
23
|
+
attribute :owner, type: User
|
|
24
|
+
attribute :makers, type: [User]
|
|
25
|
+
|
|
26
|
+
# @return [String] String representation of the project
|
|
27
|
+
def to_s
|
|
28
|
+
name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] Whether this project has a logo
|
|
32
|
+
def logo?
|
|
33
|
+
logo&.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Boolean] Whether this project has multiple makers
|
|
37
|
+
def team_project?
|
|
38
|
+
makers&.length.to_i > 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [String, nil] The logo URL for the given size
|
|
42
|
+
# @param size [Symbol] The size of the logo (:small, :medium, :large)
|
|
43
|
+
def logo_url(size = :medium)
|
|
44
|
+
logo&.dig(size.to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "user"
|
|
5
|
+
|
|
6
|
+
module Wip
|
|
7
|
+
module Models
|
|
8
|
+
# Represents a reaction (like) on a todo or comment
|
|
9
|
+
class Reaction < Base
|
|
10
|
+
attribute :id, type: :string
|
|
11
|
+
attribute :created_at, type: :time
|
|
12
|
+
attribute :reactable_type, type: :string
|
|
13
|
+
attribute :reactable_id, type: :string
|
|
14
|
+
attribute :reactor, type: User
|
|
15
|
+
|
|
16
|
+
# @return [String] String representation of the reaction
|
|
17
|
+
def to_s
|
|
18
|
+
"Reaction by #{reactor} on #{reactable_type} #{reactable_id}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Boolean] Whether this reaction is on a todo
|
|
22
|
+
def on_todo?
|
|
23
|
+
reactable_type == "Todo"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] Whether this reaction is on a comment
|
|
27
|
+
def on_comment?
|
|
28
|
+
reactable_type == "Comment"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "project"
|
|
5
|
+
require_relative "concerns/reactable"
|
|
6
|
+
|
|
7
|
+
module Wip
|
|
8
|
+
module Models
|
|
9
|
+
# Represents a todo item
|
|
10
|
+
class Todo < Base
|
|
11
|
+
include Concerns::Reactable
|
|
12
|
+
|
|
13
|
+
attribute :id, type: :string
|
|
14
|
+
attribute :created_at, type: :time
|
|
15
|
+
attribute :updated_at, type: :time
|
|
16
|
+
attribute :body, type: :string
|
|
17
|
+
attribute :url, type: :string
|
|
18
|
+
attribute :attachments, type: [Hash] # Array of attachment objects
|
|
19
|
+
attribute :creator_id, type: :string
|
|
20
|
+
attribute :user_id, type: :string # @deprecated Use creator_id instead
|
|
21
|
+
attribute :projects, type: [Project]
|
|
22
|
+
attribute :reactions_count, type: :integer
|
|
23
|
+
attribute :comments_count, type: :integer
|
|
24
|
+
attribute :viewer, type: Hash # Hash with reactions and comments arrays
|
|
25
|
+
|
|
26
|
+
# @return [String] String representation of the todo
|
|
27
|
+
def to_s
|
|
28
|
+
body
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] Whether this todo has any attachments
|
|
32
|
+
def attachments?
|
|
33
|
+
attachments&.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Boolean] Whether this todo belongs to any projects
|
|
37
|
+
def projects?
|
|
38
|
+
projects&.any?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean] Whether this todo has any comments
|
|
42
|
+
def comments?
|
|
43
|
+
comments_count&.positive?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Array<Hash>] The viewer's comments on this todo (empty if none)
|
|
47
|
+
def viewer_comments
|
|
48
|
+
viewer&.dig("comments") || []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Boolean] Whether the authenticated user has commented on this todo
|
|
52
|
+
def commented_by_viewer?
|
|
53
|
+
viewer_comments.any?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
module Models
|
|
7
|
+
# Represents a user
|
|
8
|
+
class User < Base
|
|
9
|
+
attribute :id, type: :string
|
|
10
|
+
attribute :username, type: :string
|
|
11
|
+
attribute :streak, type: :integer
|
|
12
|
+
attribute :created_at, type: :time
|
|
13
|
+
attribute :updated_at, type: :time
|
|
14
|
+
attribute :protected, type: :boolean
|
|
15
|
+
attribute :first_name, type: :string
|
|
16
|
+
attribute :last_name, type: :string
|
|
17
|
+
attribute :todos_count, type: :integer
|
|
18
|
+
attribute :time_zone, type: :string
|
|
19
|
+
attribute :url, type: :string
|
|
20
|
+
attribute :avatar, type: Hash # Hash with small, medium, large URLs
|
|
21
|
+
attribute :best_streak, type: :integer
|
|
22
|
+
attribute :streaking, type: :boolean
|
|
23
|
+
|
|
24
|
+
# @return [String] The user's full name
|
|
25
|
+
def full_name
|
|
26
|
+
[first_name, last_name].compact.join(" ").strip
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String] String representation of the user
|
|
30
|
+
def to_s
|
|
31
|
+
full_name.empty? ? username : full_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [String, nil] The avatar URL for the given size
|
|
35
|
+
# @param size [Symbol] The size of the avatar (:small, :medium, :large)
|
|
36
|
+
def avatar_url(size = :medium)
|
|
37
|
+
avatar&.dig(size.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] Whether the user has completed any todos
|
|
41
|
+
def todos?
|
|
42
|
+
todos_count&.positive?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Boolean] Whether the user is currently on a streak
|
|
46
|
+
def on_streak?
|
|
47
|
+
streak&.positive? && streaking?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
module Resources
|
|
7
|
+
# Base class for all API resources
|
|
8
|
+
# @api private
|
|
9
|
+
class Base
|
|
10
|
+
# @return [HTTPClient] The HTTP client instance
|
|
11
|
+
attr_reader :client
|
|
12
|
+
|
|
13
|
+
# Initialize a new resource
|
|
14
|
+
# @param client [HTTPClient] The HTTP client instance
|
|
15
|
+
def initialize(client)
|
|
16
|
+
@client = client
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Build a path with parameters
|
|
22
|
+
# @param path [String] The path template
|
|
23
|
+
# @param params [Hash] The parameters to interpolate
|
|
24
|
+
# @return [String] The built path
|
|
25
|
+
def build_path(path, params = {})
|
|
26
|
+
path.gsub(/\{([^}]+)\}/) do |match|
|
|
27
|
+
key = Regexp.last_match(1)
|
|
28
|
+
value = params[key.to_sym] || params[key.to_s]
|
|
29
|
+
raise ArgumentError, "Missing parameter: #{key}" unless value
|
|
30
|
+
value.to_s
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Extract pagination parameters from options
|
|
35
|
+
# @param options [Hash] The options hash
|
|
36
|
+
# @return [Hash] The pagination parameters
|
|
37
|
+
def extract_pagination_params(options)
|
|
38
|
+
{
|
|
39
|
+
limit: options[:limit],
|
|
40
|
+
starting_after: options[:starting_after]
|
|
41
|
+
}.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Extract pagination and date filter parameters for todo endpoints
|
|
45
|
+
# @param options [Hash] The options hash
|
|
46
|
+
# @option options [Integer] :limit Number of items to return
|
|
47
|
+
# @option options [String] :starting_after Cursor for pagination
|
|
48
|
+
# @option options [Time, Date, String, Integer] :since Filter todos created since this date
|
|
49
|
+
# @option options [Time, Date, String, Integer] :before Filter todos created before this date
|
|
50
|
+
# @return [Hash] The query parameters
|
|
51
|
+
def extract_todo_params(options)
|
|
52
|
+
params = extract_pagination_params(options)
|
|
53
|
+
params[:since] = format_date_param(options[:since]) if options[:since]
|
|
54
|
+
params[:until] = format_date_param(options[:before]) if options[:before]
|
|
55
|
+
params
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Format a date parameter for the API
|
|
59
|
+
# Accepts: Time, Date, String (ISO 8601, YYYY, YYYY-MM, YYYY-MM-DD), or Integer (Unix timestamp)
|
|
60
|
+
# @param value [Time, Date, String, Integer] The date value
|
|
61
|
+
# @return [String] The formatted date string
|
|
62
|
+
def format_date_param(value)
|
|
63
|
+
case value
|
|
64
|
+
when Time
|
|
65
|
+
value.utc.iso8601
|
|
66
|
+
when Date
|
|
67
|
+
value.iso8601
|
|
68
|
+
when Integer
|
|
69
|
+
# Unix timestamp
|
|
70
|
+
value.to_s
|
|
71
|
+
when String
|
|
72
|
+
# Pass through strings as-is (API accepts multiple formats)
|
|
73
|
+
value
|
|
74
|
+
else
|
|
75
|
+
raise ArgumentError, "Invalid date value: #{value.inspect}. Expected Time, Date, String, or Integer (Unix timestamp)"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../models/comment"
|
|
5
|
+
require_relative "../models/collection"
|
|
6
|
+
|
|
7
|
+
module Wip
|
|
8
|
+
module Resources
|
|
9
|
+
# Resource for interacting with comments
|
|
10
|
+
#
|
|
11
|
+
# Comments can be added to todos. The authenticated user can create, update,
|
|
12
|
+
# and delete their own comments.
|
|
13
|
+
#
|
|
14
|
+
# @example Create a comment on a todo
|
|
15
|
+
# client.comments.create(
|
|
16
|
+
# commentable_type: "Todo",
|
|
17
|
+
# commentable_id: "todo_123",
|
|
18
|
+
# body: "Great progress!"
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example List comments for a todo
|
|
22
|
+
# client.comments.for_todo("todo_123")
|
|
23
|
+
class Comments < Base
|
|
24
|
+
# Valid commentable types
|
|
25
|
+
COMMENTABLE_TYPES = %w[Todo].freeze
|
|
26
|
+
|
|
27
|
+
# List comments for a todo
|
|
28
|
+
# @param todo_id [String] The todo ID
|
|
29
|
+
# @param limit [Integer] Number of items to return (default: 25)
|
|
30
|
+
# @param starting_after [String] Cursor for pagination
|
|
31
|
+
# @return [Models::Collection<Models::Comment>] The paginated comments
|
|
32
|
+
def for_todo(todo_id, limit: nil, starting_after: nil)
|
|
33
|
+
path = build_path("/v1/todos/{todo_id}/comments", todo_id: todo_id)
|
|
34
|
+
params = extract_pagination_params(limit: limit, starting_after: starting_after)
|
|
35
|
+
response = client.get(path, params)
|
|
36
|
+
Models::Collection.from_json(response.extract_resource, item_class: Models::Comment)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Create a comment on a resource
|
|
40
|
+
# @param commentable_type [String] The type of resource to comment on ("Todo")
|
|
41
|
+
# @param commentable_id [String] The ID of the resource to comment on
|
|
42
|
+
# @param body [String] The content of the comment
|
|
43
|
+
# @return [Models::Comment] The created comment
|
|
44
|
+
# @raise [ArgumentError] If commentable_type is invalid
|
|
45
|
+
def create(commentable_type:, commentable_id:, body:)
|
|
46
|
+
validate_commentable_type!(commentable_type)
|
|
47
|
+
validate_body!(body)
|
|
48
|
+
|
|
49
|
+
response = client.post("/v1/comments", {
|
|
50
|
+
commentable_type: commentable_type,
|
|
51
|
+
commentable_id: commentable_id,
|
|
52
|
+
body: body
|
|
53
|
+
})
|
|
54
|
+
Models::Comment.from_json(response.extract_resource)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Update a comment
|
|
58
|
+
# @param comment_id [String] The comment ID
|
|
59
|
+
# @param body [String] The new content of the comment
|
|
60
|
+
# @return [Models::Comment] The updated comment
|
|
61
|
+
# @note You can only update your own comments
|
|
62
|
+
def update(comment_id, body:)
|
|
63
|
+
validate_body!(body)
|
|
64
|
+
|
|
65
|
+
path = build_path("/v1/comments/{comment_id}", comment_id: comment_id)
|
|
66
|
+
response = client.patch(path, { body: body })
|
|
67
|
+
Models::Comment.from_json(response.extract_resource)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Delete a comment
|
|
71
|
+
# @param comment_id [String] The comment ID
|
|
72
|
+
# @return [Boolean] True if deletion was successful
|
|
73
|
+
# @note You can only delete your own comments
|
|
74
|
+
def delete(comment_id)
|
|
75
|
+
path = build_path("/v1/comments/{comment_id}", comment_id: comment_id)
|
|
76
|
+
client.delete(path)
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def validate_commentable_type!(type)
|
|
83
|
+
return if COMMENTABLE_TYPES.include?(type)
|
|
84
|
+
|
|
85
|
+
raise ArgumentError, "Invalid commentable_type '#{type}'. Must be one of: #{COMMENTABLE_TYPES.join(', ')}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def validate_body!(body)
|
|
89
|
+
raise ArgumentError, "Comment body cannot be empty" if body.nil? || body.strip.empty?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../models/project"
|
|
5
|
+
require_relative "../models/todo"
|
|
6
|
+
require_relative "../models/collection"
|
|
7
|
+
|
|
8
|
+
module Wip
|
|
9
|
+
module Resources
|
|
10
|
+
# Resource for interacting with projects
|
|
11
|
+
#
|
|
12
|
+
# @example Get a project
|
|
13
|
+
# client.projects.find("project_123")
|
|
14
|
+
#
|
|
15
|
+
# @example List todos for a project
|
|
16
|
+
# client.projects.todos("project_123", limit: 10)
|
|
17
|
+
class Projects < Base
|
|
18
|
+
# Get a single project by ID
|
|
19
|
+
# @param project_id [String] The project ID
|
|
20
|
+
# @return [Models::Project] The project
|
|
21
|
+
def find(project_id)
|
|
22
|
+
path = build_path("/v1/projects/{project_id}", project_id: project_id)
|
|
23
|
+
response = client.get(path)
|
|
24
|
+
Models::Project.from_json(response.extract_resource)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# List todos for a project
|
|
28
|
+
# @param project_id [String] The project ID
|
|
29
|
+
# @param limit [Integer] Number of items to return (default: 25)
|
|
30
|
+
# @param starting_after [String] Cursor for pagination
|
|
31
|
+
# @param since [Time, Date, String, Integer] Filter todos created since this date
|
|
32
|
+
# @param before [Time, Date, String, Integer] Filter todos created before this date (maps to API's `until` param)
|
|
33
|
+
# @return [Models::Collection<Models::Todo>] The paginated todos
|
|
34
|
+
def todos(project_id, limit: nil, starting_after: nil, since: nil, before: nil)
|
|
35
|
+
path = build_path("/v1/projects/{project_id}/todos", project_id: project_id)
|
|
36
|
+
params = extract_todo_params(limit: limit, starting_after: starting_after, since: since, before: before)
|
|
37
|
+
response = client.get(path, params)
|
|
38
|
+
Models::Collection.from_json(response.extract_resource, item_class: Models::Todo)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../models/reaction"
|
|
5
|
+
|
|
6
|
+
module Wip
|
|
7
|
+
module Resources
|
|
8
|
+
# Resource for interacting with reactions (likes)
|
|
9
|
+
#
|
|
10
|
+
# Reactions can be added to todos and comments. Each user can only have one
|
|
11
|
+
# reaction per resource. If the user has already reacted, the existing
|
|
12
|
+
# reaction is returned.
|
|
13
|
+
#
|
|
14
|
+
# @example React to a todo
|
|
15
|
+
# client.reactions.create(
|
|
16
|
+
# reactable_type: "Todo",
|
|
17
|
+
# reactable_id: "todo_123"
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @example Remove a reaction
|
|
21
|
+
# client.reactions.delete("reaction_123")
|
|
22
|
+
class Reactions < Base
|
|
23
|
+
# Valid reactable types
|
|
24
|
+
REACTABLE_TYPES = %w[Todo Comment].freeze
|
|
25
|
+
|
|
26
|
+
# Create a reaction on a resource
|
|
27
|
+
# @param reactable_type [String] The type of resource to react to ("Todo" or "Comment")
|
|
28
|
+
# @param reactable_id [String] The ID of the resource to react to
|
|
29
|
+
# @return [Models::Reaction] The created reaction (or existing if already reacted)
|
|
30
|
+
# @raise [ArgumentError] If reactable_type is invalid
|
|
31
|
+
def create(reactable_type:, reactable_id:)
|
|
32
|
+
validate_reactable_type!(reactable_type)
|
|
33
|
+
|
|
34
|
+
response = client.post("/v1/reactions", {
|
|
35
|
+
reactable_type: reactable_type,
|
|
36
|
+
reactable_id: reactable_id
|
|
37
|
+
})
|
|
38
|
+
Models::Reaction.from_json(response.extract_resource)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Delete a reaction
|
|
42
|
+
# @param reaction_id [String] The reaction ID
|
|
43
|
+
# @return [Boolean] True if deletion was successful
|
|
44
|
+
# @note You can only delete your own reactions
|
|
45
|
+
def delete(reaction_id)
|
|
46
|
+
path = build_path("/v1/reactions/{reaction_id}", reaction_id: reaction_id)
|
|
47
|
+
client.delete(path)
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Convenience method to react to a todo
|
|
52
|
+
# @param todo_id [String] The todo ID
|
|
53
|
+
# @return [Models::Reaction] The created reaction
|
|
54
|
+
def react_to_todo(todo_id)
|
|
55
|
+
create(reactable_type: "Todo", reactable_id: todo_id)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Convenience method to react to a comment
|
|
59
|
+
# @param comment_id [String] The comment ID
|
|
60
|
+
# @return [Models::Reaction] The created reaction
|
|
61
|
+
def react_to_comment(comment_id)
|
|
62
|
+
create(reactable_type: "Comment", reactable_id: comment_id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def validate_reactable_type!(type)
|
|
68
|
+
return if REACTABLE_TYPES.include?(type)
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, "Invalid reactable_type '#{type}'. Must be one of: #{REACTABLE_TYPES.join(', ')}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../models/todo"
|
|
5
|
+
require_relative "../models/collection"
|
|
6
|
+
|
|
7
|
+
module Wip
|
|
8
|
+
module Resources
|
|
9
|
+
# Resource for interacting with todos
|
|
10
|
+
#
|
|
11
|
+
# @example Create a todo
|
|
12
|
+
# client.todos.create(body: "Shipped new feature!")
|
|
13
|
+
#
|
|
14
|
+
# @example Get a specific todo
|
|
15
|
+
# client.todos.find("todo_123")
|
|
16
|
+
#
|
|
17
|
+
# @note To list todos, use the appropriate resource:
|
|
18
|
+
# - `client.users.todos("username")` for a user's todos
|
|
19
|
+
# - `client.projects.todos("project_id")` for a project's todos
|
|
20
|
+
# - `client.viewer.todos` for your own todos
|
|
21
|
+
class Todos < Base
|
|
22
|
+
# Create a new completed todo
|
|
23
|
+
# @param body [String] The content of the todo
|
|
24
|
+
# @param attachments [Array<String>] List of attachment signed IDs
|
|
25
|
+
# @return [Models::Todo] The created todo
|
|
26
|
+
# @raise [ArgumentError] If body is empty
|
|
27
|
+
def create(body:, attachments: [])
|
|
28
|
+
raise ArgumentError, "Todo body cannot be empty" if body.nil? || body.strip.empty?
|
|
29
|
+
|
|
30
|
+
response = client.post("/v1/todos", {
|
|
31
|
+
body: body,
|
|
32
|
+
attachments: attachments
|
|
33
|
+
})
|
|
34
|
+
Models::Todo.from_json(response.extract_resource(:todo))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get a single todo by ID
|
|
38
|
+
# @param todo_id [String] The todo ID
|
|
39
|
+
# @return [Models::Todo] The todo
|
|
40
|
+
def find(todo_id)
|
|
41
|
+
path = build_path("/v1/todos/{todo_id}", todo_id: todo_id)
|
|
42
|
+
response = client.get(path)
|
|
43
|
+
Models::Todo.from_json(response.extract_resource)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|