active_collab-ruby-sdk 0.3.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 618cfa962f35bcae3a41e437b9aac7d77ae528f535f452355146ccacdb56885d
4
+ data.tar.gz: 3f370da2a5aeba7923bdc7cb44531a7c9d5f21eec0ded03bf4312194a0e2c6db
5
+ SHA512:
6
+ metadata.gz: 47597a3f5889d9abbb95699b3f1dffbe9a231c4eb28112b707af2fe0e9e9a06d56cefa88c1d87d00d52ed5d095fb4bc21694ea7f063996b0279fb7d8ff0dffef
7
+ data.tar.gz: f23a36519466b76297966bf02f319235113a76d7a3450612f8e4f8c00cd9fd9a40ef41970f4d1198c54cb56b9f109600450e9185a4ca70e1b1c6009db81ddf81
data/CHANGELOG.md ADDED
@@ -0,0 +1,88 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/), and
6
+ this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.3.2] - 2026-02-12
9
+
10
+ ### Added
11
+ - `bin/console` script for interactive development sessions.
12
+
13
+ ### Removed
14
+ - `Rakefile` in favor of calling `rspec` directly.
15
+
16
+ ## [0.3.1] - 2026-02-12
17
+
18
+ ### Added
19
+ - `Client#users` convenience method for accessing user endpoints directly.
20
+ - `ostruct` as an explicit runtime dependency for Ruby 4.0 compatibility.
21
+
22
+ ### Changed
23
+ - Cleaned up README code examples.
24
+
25
+ ## [0.3.0] - 2026-02-12
26
+
27
+ ### Added
28
+ - Custom exception hierarchy: `ActiveCollab::Error`, `APIError`, `AuthenticationError`, `NotFoundError`, `RateLimitError`, `ParseError`.
29
+ - HTTP error handling: 4xx/5xx responses now raise descriptive exceptions.
30
+ - JSON parse error handling: invalid response bodies raise `ParseError`.
31
+ - HTTP timeouts (30s open/read) to prevent hanging requests.
32
+ - Comprehensive test coverage for all public methods (63 examples).
33
+ - YARD documentation for all public methods.
34
+ - CHANGELOG, `.rspec`, and `Rakefile`.
35
+ - Gemspec metadata (`homepage_uri`, `source_code_uri`, `changelog_uri`, `rubygems_mfa_required`).
36
+
37
+ ### Changed
38
+ - **Breaking:** `Projects#list` renamed to `Projects#all`.
39
+ - **Breaking:** `Tasks#get` renamed to `Tasks#find`.
40
+ - **Breaking:** `TimeRecords#push` renamed to `TimeRecords#create`.
41
+ - **Breaking:** `Token#header` renamed to `Token#auth_header`.
42
+ - **Breaking:** `Token#raw` removed (use `Token#value` instead).
43
+ - `Response#to_json` renamed to `Response#to_json_string` to avoid shadowing `Object#to_json`. The `format: 'json'` parameter is unchanged.
44
+ - VERSION extracted to dedicated `lib/active_collab/version.rb`.
45
+ - Gemspec no longer loads the entire library at eval time.
46
+ - Development dependencies moved from gemspec to Gemfile.
47
+ - Narrowed ActiveSupport requires to only the extensions actually used.
48
+ - `json` dependency loosened from `~> 2.18` to `~> 2.10`.
49
+ - `activesupport` dependency widened to `>= 7, < 9`.
50
+ - Tasks and TaskLists source files moved from `lib/active_collab/projects/` to `lib/active_collab/` to match their namespace.
51
+
52
+ ### Fixed
53
+ - `Client#initialize` no longer mutates the caller's options hash.
54
+ - `Token#auth_header` now uses correct `AuthApiToken` casing (was `Authapitoken`).
55
+ - Mixed string/symbol key handling in `Tasks#archived` pagination.
56
+ - Added explicit `require 'ostruct'` for Ruby 3.3+ compatibility.
57
+ - `frozen_string_literal: true` added to all source files.
58
+
59
+ ## [0.2.2] - 2025-09-03
60
+
61
+ ### Changed
62
+ - Pinned `activesupport` to `~> 7` minimum.
63
+
64
+ ## [0.2.1] - 2025-06-28
65
+
66
+ ### Fixed
67
+ - Added missing `params` argument to `Projects#list`.
68
+
69
+ ## [0.2.0] - 2025-06-28
70
+
71
+ ### Added
72
+ - Format parameter support (`hash`, `json`, `object`) for all API calls.
73
+ - GET requests now correctly append params as query strings.
74
+
75
+ ## [0.1.0] - 2025-06-15
76
+
77
+ ### Added
78
+ - Initial release with Client, Token, Response, LoginResponse.
79
+ - Projects, Tasks, TaskLists, TimeRecords, and Users resources.
80
+ - Token-based and credential-based authentication.
81
+
82
+ [0.3.2]: https://github.com/davelens/active_collab-ruby-sdk/compare/0.3.1...0.3.2
83
+ [0.3.1]: https://github.com/davelens/active_collab-ruby-sdk/compare/0.3.0...0.3.1
84
+ [0.3.0]: https://github.com/davelens/active_collab-ruby-sdk/compare/0.2.2...0.3.0
85
+ [0.2.2]: https://github.com/davelens/active_collab-ruby-sdk/compare/0.2.1...0.2.2
86
+ [0.2.1]: https://github.com/davelens/active_collab-ruby-sdk/compare/0.2.0...0.2.1
87
+ [0.2.0]: https://github.com/davelens/active_collab-ruby-sdk/compare/0.1.0...0.2.0
88
+ [0.1.0]: https://github.com/davelens/active_collab-ruby-sdk/releases/tag/0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dave Lens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # ActiveCollab Ruby SDK
2
+
3
+ [![CI](https://github.com/davelens/active_collab-ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/davelens/active_collab-ruby-sdk/actions/workflows/ci.yml?version=1)
4
+
5
+ A Ruby SDK for the [ActiveCollab](https://activecollab.com/) API. Provides a clean interface for authentication, project management, task tracking, time records, and more.
6
+
7
+ ## Requirements
8
+ - Ruby >= 3.1
9
+ - An ActiveCollab account with API access
10
+
11
+ ## Installation
12
+ Add to your Gemfile:
13
+ ```ruby
14
+ gem 'active_collab-ruby-sdk', '~> 0.3'
15
+ ```
16
+
17
+ Or install directly:
18
+ ```
19
+ gem install active_collab-ruby-sdk
20
+ ```
21
+
22
+ ## Authentication
23
+ The SDK supports two authentication methods.
24
+
25
+ ### Token-based (if you already have a token)
26
+ ```ruby
27
+ require 'active_collab'
28
+
29
+ client = ActiveCollab::Client.new(
30
+ account_id: '12345',
31
+ token: 'your-api-token'
32
+ )
33
+ ```
34
+
35
+ ### Credential-based (username/password)
36
+ ```ruby
37
+ client = ActiveCollab::Client.new(
38
+ account_id: '12345',
39
+ username: 'user@example.com',
40
+ password: 'your-password',
41
+ client_vendor: 'YourCompany',
42
+ client_name: 'YourApp'
43
+ )
44
+
45
+ # Authenticates and stores the token on the client
46
+ client.request_token!
47
+ ```
48
+
49
+ The token is stored on the client instance and sent as the `X-Angie-AuthApiToken` header on all subsequent requests.
50
+
51
+ ## Usage
52
+ ### Projects
53
+ ```ruby
54
+ # List all projects (returns a Hash by default)
55
+ client.projects.all
56
+
57
+ # Get projects as a JSON string
58
+ client.projects.all(format: 'json')
59
+
60
+ # Get projects as an OpenStruct
61
+ client.projects.all(format: 'object')
62
+ ```
63
+
64
+ ### Tasks
65
+ ```ruby
66
+ tasks = client.projects.tasks(project_id)
67
+
68
+ # All tasks (active + archived, sorted by created_on desc)
69
+ tasks.all
70
+
71
+ # Active tasks only
72
+ tasks.active
73
+
74
+ # Archived tasks (auto-paginates through all pages)
75
+ tasks.archived
76
+
77
+ # Archived tasks for a specific page (no auto-pagination)
78
+ tasks.archived('page' => 2)
79
+
80
+ # Find a single task - includes comments!
81
+ tasks.find(task_id)
82
+
83
+ # Update a task
84
+ tasks.update(task_id, name: 'New name')
85
+
86
+ # Time records for a specific task
87
+ tasks.time_records(task_id)
88
+ ```
89
+
90
+ ### Task Lists
91
+ ```ruby
92
+ client.projects.task_lists(project_id).all
93
+ ```
94
+
95
+ ### Time Records
96
+ ```ruby
97
+ time_records = client.projects.time_records(project_id)
98
+
99
+ # List all time records for a project
100
+ time_records.all
101
+
102
+ # Create a new time record
103
+ time_records.create(value: 1.5, user_id: 10, job_type_id: 1)
104
+ ```
105
+
106
+ ### Users
107
+ ```ruby
108
+ # List all users
109
+ users = client.users.all
110
+ ```
111
+
112
+ ### Response Formats
113
+ All resource methods accept a `format` parameter:
114
+
115
+ | Format | Return type | Description |
116
+ |------------|-------------|------------------------------|
117
+ | `'hash'` | `Hash` | Parsed JSON (default) |
118
+ | `'json'` | `String` | Raw JSON response body |
119
+ | `'object'` | `OpenStruct`| Parsed JSON as OpenStruct |
120
+
121
+ ```ruby
122
+ client.projects.all(format: 'json') # => '{"projects": [...]}'
123
+ client.projects.all(format: 'object') # => #<OpenStruct projects=[...]>
124
+ ```
125
+
126
+ ### Token Object
127
+ ```ruby
128
+ token = client.token
129
+ token.value # => "your-api-token"
130
+ token.auth_header # => { "X-Angie-AuthApiToken": "your-api-token" }
131
+ ```
132
+
133
+ ### URL Helpers
134
+ ```ruby
135
+ # Build an app URL (for linking to the ActiveCollab web UI)
136
+ client.app_url('/projects/3') # => URI("https://next-app.activecollab.com/12345/projects/3")
137
+ ```
138
+
139
+ ## Error Handling
140
+ The SDK raises specific exceptions for API errors:
141
+
142
+ ```ruby
143
+ begin
144
+ client.projects.all
145
+ rescue ActiveCollab::AuthenticationError => e
146
+ # 401 - invalid or expired token
147
+ puts e.message # => "API request failed with status 401"
148
+ puts e.status # => 401
149
+ puts e.body # => raw response body
150
+ rescue ActiveCollab::NotFoundError => e
151
+ # 404
152
+ rescue ActiveCollab::RateLimitError => e
153
+ # 429
154
+ rescue ActiveCollab::APIError => e
155
+ # Any other 4xx/5xx
156
+ rescue ActiveCollab::ParseError => e
157
+ # Response body was not valid JSON
158
+ puts e.body # => the raw unparseable body
159
+ end
160
+ ```
161
+
162
+ ## Development
163
+ ```
164
+ git clone https://github.com/davelens/active_collab-ruby-sdk.git
165
+ cd active_collab-ruby-sdk
166
+ bundle install
167
+ ```
168
+
169
+ Running tests:
170
+ ```
171
+ rspec
172
+ ```
173
+
174
+ ## License
175
+ [MIT](LICENSE)
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ # HTTP client for the ActiveCollab API.
4
+ #
5
+ # Handles authentication, request construction, and response parsing.
6
+ #
7
+ # @see https://github.com/activecollab/activecollab-feather-sdk/issues/35
8
+ # Auth flow documentation
9
+ class ActiveCollab::Client
10
+
11
+ DEFAULTS = {
12
+ username: '',
13
+ password: '',
14
+ client_vendor: '',
15
+ client_name: '',
16
+ account_id: ''
17
+ }.freeze
18
+
19
+ HTTP_METHODS = {
20
+ 'Get' => Net::HTTP::Get,
21
+ 'Post' => Net::HTTP::Post,
22
+ 'Put' => Net::HTTP::Put
23
+ }.freeze
24
+
25
+ APP_URL = 'https://next-app.activecollab.com'
26
+ API_URL = 'https://app.activecollab.com'
27
+ AUTH_URL = 'https://activecollab.com/api/v1'
28
+
29
+ # @param options [Hash] client configuration
30
+ # @option options [String] :token pre-existing API token
31
+ # @option options [String] :account_id ActiveCollab account ID
32
+ # @option options [String] :username email address for authentication
33
+ # @option options [String] :password password for authentication
34
+ # @option options [String] :client_vendor vendor name for token issuance
35
+ # @option options [String] :client_name application name for token issuance
36
+ def initialize(options = {})
37
+ @token = options[:token]
38
+ @options = DEFAULTS.merge(options)
39
+ end
40
+
41
+ # Performs an HTTP request to the given URI.
42
+ #
43
+ # @param method [String] HTTP method ('Get', 'Post', or 'Put')
44
+ # @param uri [URI] the full request URI
45
+ # @param params [Hash] request parameters
46
+ # @option params [String] :format response format ('hash', 'json', or 'object')
47
+ # @return [Hash, String, OpenStruct] parsed response in the requested format
48
+ # @raise [ActiveCollab::APIError] on 4xx/5xx responses
49
+ def call(method, uri, params = {})
50
+ method = HTTP_METHODS.key?(method) ? method : 'Get'
51
+ data = params.except(:headers, :format)
52
+ format = if %w[hash json object].include?(params[:format])
53
+ params[:format]
54
+ else
55
+ 'hash'
56
+ end
57
+
58
+ if method == 'Get' && data.present?
59
+ uri.query = [uri.query, data.to_query].compact.join('&')
60
+ request = Net::HTTP::Get.new(uri)
61
+ else
62
+ request = HTTP_METHODS[method].new(uri)
63
+ request.set_form_data(data) unless method == 'Get'
64
+ end
65
+
66
+ if @token
67
+ request['X-Angie-AuthApiToken'] = @token
68
+ end
69
+
70
+ response = Net::HTTP.start(
71
+ request.uri.hostname,
72
+ request.uri.port,
73
+ use_ssl: request.uri.scheme == "https",
74
+ open_timeout: 30,
75
+ read_timeout: 30
76
+ ) do |http|
77
+ http.request(request)
78
+ end
79
+
80
+ handle_response_errors!(response)
81
+
82
+ format_method = { 'hash' => :to_hash, 'json' => :to_json_string, 'object' => :to_object }
83
+ ActiveCollab::Response
84
+ .new(response.body)
85
+ .send(format_method[format])
86
+ end
87
+
88
+ # Builds a URL for the ActiveCollab web application UI.
89
+ #
90
+ # @param uri [String] path to append (e.g., '/projects/3')
91
+ # @return [URI] the full app URL
92
+ def app_url(uri)
93
+ URI.parse("#{APP_URL}/#{@options[:account_id]}#{uri}")
94
+ end
95
+
96
+ # Builds an API endpoint URL for the given path.
97
+ #
98
+ # @param uri [String] API path (e.g., '/projects')
99
+ # @return [URI] the full API URL
100
+ def call_url(uri)
101
+ url = if uri.include?('/external/login')
102
+ "#{AUTH_URL}#{uri}"
103
+ else
104
+ "#{API_URL}/#{@options[:account_id]}/api/v1#{uri}"
105
+ end
106
+
107
+ URI.parse(url)
108
+ end
109
+
110
+ # Performs a GET request.
111
+ #
112
+ # @param uri [String] API path
113
+ # @param params [Hash] query parameters
114
+ # @return [Hash, String, OpenStruct]
115
+ def get(uri, params = {})
116
+ call('Get', call_url(uri), params)
117
+ end
118
+
119
+ # Performs a POST request.
120
+ #
121
+ # @param uri [String] API path
122
+ # @param params [Hash] form data parameters
123
+ # @return [Hash, String, OpenStruct]
124
+ def post(uri, params = {})
125
+ call('Post', call_url(uri), params)
126
+ end
127
+
128
+ # Performs a PUT request.
129
+ #
130
+ # @param uri [String] API path
131
+ # @param params [Hash] form data parameters
132
+ # @return [Hash, String, OpenStruct]
133
+ def put(uri, params = {})
134
+ call('Put', call_url(uri), params)
135
+ end
136
+
137
+ # Authenticates with username/password and stores the resulting token.
138
+ #
139
+ # @return [String] the issued API token
140
+ # @raise [ActiveCollab::AuthenticationError] if credentials are invalid
141
+ def request_token!
142
+ login_response = ActiveCollab::LoginResponse.new(
143
+ login,
144
+ account_id: @options[:account_id]
145
+ )
146
+
147
+ token = issue_token_intent(
148
+ client_vendor: @options[:client_vendor],
149
+ client_name: @options[:client_name],
150
+ intent: login_response.intent
151
+ )
152
+
153
+ @token = token.value
154
+ @token
155
+ end
156
+
157
+ # Returns the current token wrapped in a Token object.
158
+ #
159
+ # @return [ActiveCollab::Token]
160
+ def token
161
+ ActiveCollab::Token.new(@token)
162
+ end
163
+
164
+ # Returns a Projects resource for accessing project endpoints.
165
+ #
166
+ # @return [ActiveCollab::Projects]
167
+ def projects
168
+ ActiveCollab::Projects.new(self)
169
+ end
170
+
171
+ # Returns a Users resource for accessing user endpoints.
172
+ #
173
+ # @return [ActiveCollab::Users]
174
+ def users
175
+ ActiveCollab::Users.new(self)
176
+ end
177
+
178
+ private
179
+
180
+ def handle_response_errors!(response)
181
+ status = response.code.to_i
182
+ return if status < 400
183
+
184
+ body = response.body
185
+ message = "API request failed with status #{status}"
186
+
187
+ error_class = case status
188
+ when 401 then ActiveCollab::AuthenticationError
189
+ when 404 then ActiveCollab::NotFoundError
190
+ when 429 then ActiveCollab::RateLimitError
191
+ else ActiveCollab::APIError
192
+ end
193
+
194
+ raise error_class.new(message, status: status, body: body)
195
+ end
196
+
197
+ def issue_token_intent(opts = {})
198
+ response = post(
199
+ '/issue-token-intent',
200
+ client_vendor: opts[:client_vendor],
201
+ client_name: opts[:client_name],
202
+ intent: opts[:intent]
203
+ )
204
+
205
+ ActiveCollab::Token.new(response['token'])
206
+ end
207
+
208
+ def login
209
+ post(
210
+ '/external/login',
211
+ username: @options[:username],
212
+ password: @options[:password]
213
+ )
214
+ end
215
+
216
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCollab
4
+ # Base error class for all ActiveCollab errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the API returns an error HTTP response (4xx/5xx).
8
+ class APIError < Error
9
+ attr_reader :status, :body
10
+
11
+ def initialize(message = nil, status: nil, body: nil)
12
+ @status = status
13
+ @body = body
14
+ super(message || "API request failed with status #{status}")
15
+ end
16
+ end
17
+
18
+ # Raised on 401 Unauthorized responses.
19
+ class AuthenticationError < APIError; end
20
+
21
+ # Raised on 404 Not Found responses.
22
+ class NotFoundError < APIError; end
23
+
24
+ # Raised on 429 Too Many Requests responses.
25
+ class RateLimitError < APIError; end
26
+
27
+ # Raised when a response body cannot be parsed as JSON.
28
+ class ParseError < Error
29
+ attr_reader :body
30
+
31
+ def initialize(message = nil, body: nil)
32
+ @body = body
33
+ super(message || 'Failed to parse response body as JSON')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Wraps the response from the ActiveCollab login endpoint.
4
+ #
5
+ # Used internally by {Client#request_token!} to extract the intent
6
+ # token needed for API token issuance.
7
+ class ActiveCollab::LoginResponse
8
+ # @param values [Hash] parsed login response body
9
+ # @param account_id [String, nil] the account ID to match
10
+ def initialize(values, account_id: nil)
11
+ @values = values
12
+ @account_id = account_id
13
+ end
14
+
15
+ # Returns the list of accounts from the login response.
16
+ #
17
+ # @return [Array<Hash>]
18
+ def accounts
19
+ @accounts ||= @values['accounts']
20
+ end
21
+
22
+ # Returns the account matching the configured account ID.
23
+ #
24
+ # @return [Hash, nil] the matching account, or nil if not found
25
+ def account_info
26
+ return @account unless @account.nil?
27
+
28
+ @account = accounts.detect do |account|
29
+ account['name'].to_s == @account_id.to_s
30
+ end
31
+
32
+ @account
33
+ end
34
+
35
+ # Returns the API URL for the matching account.
36
+ #
37
+ # @return [String]
38
+ def call_url
39
+ account_info['url']
40
+ end
41
+
42
+ # Returns the user intent token from the login response.
43
+ #
44
+ # @return [String, nil]
45
+ def intent
46
+ @values.dig('user', 'intent')
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides access to project-related API endpoints.
4
+ class ActiveCollab::Projects
5
+ # @param client [ActiveCollab::Client]
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # Lists all projects.
11
+ #
12
+ # @param params [Hash] query parameters
13
+ # @option params [String] :format response format ('hash', 'json', or 'object')
14
+ # @return [Hash, String, OpenStruct]
15
+ def all(params = {})
16
+ @client.get('/projects', params)
17
+ end
18
+
19
+ # Returns a TaskLists resource scoped to the given project.
20
+ #
21
+ # @param project_id [Integer, String] the project ID
22
+ # @return [ActiveCollab::TaskLists]
23
+ def task_lists(project_id)
24
+ ActiveCollab::TaskLists.new(@client, project_id)
25
+ end
26
+
27
+ # Returns a Tasks resource scoped to the given project.
28
+ #
29
+ # @param project_id [Integer, String] the project ID
30
+ # @return [ActiveCollab::Tasks]
31
+ def tasks(project_id)
32
+ ActiveCollab::Tasks.new(@client, project_id)
33
+ end
34
+
35
+ # Returns a TimeRecords resource scoped to the given project.
36
+ #
37
+ # @param project_id [Integer, String] the project ID
38
+ # @return [ActiveCollab::TimeRecords]
39
+ def time_records(project_id)
40
+ ActiveCollab::TimeRecords.new(@client, project_id)
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ # Wraps a raw HTTP response body and provides format conversion.
6
+ class ActiveCollab::Response
7
+ # @return [String] the raw response body
8
+ attr_reader :raw_body
9
+
10
+ # @param raw_body [String, nil] the raw HTTP response body
11
+ def initialize(raw_body)
12
+ @raw_body = raw_body
13
+ @raw_body = '{}' if raw_body == '' || raw_body.nil?
14
+ end
15
+
16
+ # Returns the raw response body as a JSON string.
17
+ #
18
+ # @return [String]
19
+ def to_json_string
20
+ @raw_body
21
+ end
22
+
23
+ # Parses the response body as a Ruby Hash.
24
+ #
25
+ # @return [Hash]
26
+ # @raise [ActiveCollab::ParseError] if the body is not valid JSON
27
+ def to_hash
28
+ JSON.parse(@raw_body)
29
+ rescue JSON::ParserError => e
30
+ raise ActiveCollab::ParseError.new(
31
+ "Failed to parse response body as JSON: #{e.message}",
32
+ body: @raw_body
33
+ )
34
+ end
35
+
36
+ # Parses the response body into an OpenStruct.
37
+ #
38
+ # @return [OpenStruct]
39
+ # @raise [ActiveCollab::ParseError] if the body is not valid JSON
40
+ def to_object
41
+ OpenStruct.new(to_hash)
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides access to task list endpoints for a specific project.
4
+ class ActiveCollab::TaskLists
5
+ # @param client [ActiveCollab::Client]
6
+ # @param project_id [Integer, String] the project ID
7
+ def initialize(client, project_id)
8
+ @client = client
9
+ @project_id = project_id
10
+ end
11
+
12
+ # Lists all task lists for the project.
13
+ #
14
+ # @param params [Hash] query parameters
15
+ # @return [Hash, String, OpenStruct]
16
+ def all(params = {})
17
+ @client
18
+ .get("/projects/#{@project_id}/task-lists", params)
19
+ end
20
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides access to task-related API endpoints for a specific project.
4
+ class ActiveCollab::Tasks
5
+ # @param client [ActiveCollab::Client]
6
+ # @param project_id [Integer, String] the project ID
7
+ def initialize(client, project_id)
8
+ @client = client
9
+ @project_id = project_id
10
+ end
11
+
12
+ # Returns all tasks (active + archived), sorted by created_on descending.
13
+ #
14
+ # @param params [Hash] query parameters
15
+ # @option params [String] 'format' response format ('hash' or 'json')
16
+ # @return [Hash, String] a hash with a 'tasks' key, or JSON string
17
+ def all(params = {})
18
+ temp_params = params.merge('format' => 'hash')
19
+ all_tasks = [active(temp_params)['tasks'] + archived(temp_params)['tasks']]
20
+ result = {
21
+ 'tasks' => all_tasks
22
+ .flatten
23
+ .sort_by { |t| -t['created_on'] } || []
24
+ }
25
+
26
+ return JSON.generate(result) if params['format'] == 'json'
27
+ result
28
+ end
29
+
30
+ # Returns active (non-archived) tasks for the project.
31
+ #
32
+ # @param params [Hash] query parameters
33
+ # @return [Hash, String, OpenStruct]
34
+ def active(params = {})
35
+ @client
36
+ .get("/projects/#{@project_id}/tasks", params)
37
+ end
38
+
39
+ # Returns archived tasks for the project.
40
+ #
41
+ # Auto-paginates through all pages unless a specific page is requested.
42
+ #
43
+ # @param params [Hash] query parameters
44
+ # @option params [Integer] 'page' specific page to fetch (disables auto-pagination)
45
+ # @option params [String] 'format' response format ('hash' or 'json')
46
+ # @return [Hash, String] a hash with a 'tasks' key, or JSON string
47
+ def archived(params = {})
48
+ page = params['page'] || 1
49
+ all_tasks = []
50
+
51
+ loop do
52
+ response = @client
53
+ .get("/projects/#{@project_id}/tasks/archive", params.merge('page' => page))
54
+ tasks = response || []
55
+ all_tasks += tasks
56
+ break if tasks.empty? || params.key?('page')
57
+ page += 1
58
+ end
59
+
60
+ result = { 'tasks' => all_tasks.flatten.sort_by { |t| -t['created_on'] } }
61
+ return JSON.generate(result) if params['format'] == 'json'
62
+ result
63
+ end
64
+
65
+ # Fetches a single task by ID.
66
+ #
67
+ # @param id [Integer, String] the task ID
68
+ # @param params [Hash] query parameters
69
+ # @return [Hash, String, OpenStruct]
70
+ def find(id, params = {})
71
+ @client
72
+ .get("/projects/#{@project_id}/tasks/#{id}", params)
73
+ end
74
+
75
+ # Fetches time records for a specific task.
76
+ #
77
+ # @param id [Integer, String] the task ID
78
+ # @param params [Hash] query parameters
79
+ # @return [Hash, String, OpenStruct]
80
+ def time_records(id, params = {})
81
+ @client
82
+ .get("/projects/#{@project_id}/tasks/#{id}/time-records", params)
83
+ end
84
+
85
+ # Updates a task.
86
+ #
87
+ # @param id [Integer, String] the task ID
88
+ # @param params [Hash] fields to update
89
+ # @return [Hash, String, OpenStruct]
90
+ def update(id, params = {})
91
+ @client
92
+ .put("/projects/#{@project_id}/tasks/#{id}", params)
93
+ end
94
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides access to time record endpoints for a specific project.
4
+ class ActiveCollab::TimeRecords
5
+ # @param client [ActiveCollab::Client]
6
+ # @param project_id [Integer, String] the project ID
7
+ def initialize(client, project_id)
8
+ @client = client
9
+ @project_id = project_id
10
+ end
11
+
12
+ # Lists all time records for the project.
13
+ #
14
+ # @param params [Hash] query parameters
15
+ # @return [Hash, String, OpenStruct]
16
+ def all(params = {})
17
+ @client
18
+ .get("/projects/#{@project_id}/time-records", params)
19
+ end
20
+
21
+ # Creates a new time record for the project.
22
+ #
23
+ # @param params [Hash] time record attributes
24
+ # @return [Hash, String, OpenStruct]
25
+ def create(params = {})
26
+ @client
27
+ .post("/projects/#{@project_id}/time-records", params)
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents an ActiveCollab API authentication token.
4
+ class ActiveCollab::Token
5
+ # @return [String] the raw token string
6
+ attr_reader :value
7
+
8
+ # @param value [String] the API token
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ # Returns the token as an HTTP authentication header hash.
14
+ #
15
+ # @return [Hash{Symbol => String}]
16
+ def auth_header
17
+ {
18
+ 'X-Angie-AuthApiToken': @value
19
+ }
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides access to user-related API endpoints.
4
+ class ActiveCollab::Users
5
+ # @param client [ActiveCollab::Client]
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # Lists all users.
11
+ #
12
+ # @return [Hash, String, OpenStruct]
13
+ def all
14
+ @client.get('/users')
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCollab
4
+ VERSION = '0.3.2'
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'active_support/core_ext/object/blank'
7
+ require 'active_support/core_ext/object/to_query'
8
+
9
+ module ActiveCollab
10
+ require_relative 'active_collab/version'
11
+ require_relative 'active_collab/errors'
12
+ require_relative 'active_collab/client'
13
+ require_relative 'active_collab/response'
14
+ require_relative 'active_collab/login_response'
15
+ require_relative 'active_collab/token'
16
+ require_relative 'active_collab/projects'
17
+ require_relative 'active_collab/task_lists'
18
+ require_relative 'active_collab/tasks'
19
+ require_relative 'active_collab/time_records'
20
+ require_relative 'active_collab/users'
21
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_collab-ruby-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.2
5
+ platform: ruby
6
+ authors:
7
+ - Dave Lens
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.10'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.10'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ostruct
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activesupport
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7'
47
+ - - "<"
48
+ - !ruby/object:Gem::Version
49
+ version: '9'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '7'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '9'
60
+ description: A basic Ruby SDK to interact with the Active Collab API.
61
+ email:
62
+ - github@davelens.be
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - CHANGELOG.md
68
+ - LICENSE
69
+ - README.md
70
+ - lib/active_collab.rb
71
+ - lib/active_collab/client.rb
72
+ - lib/active_collab/errors.rb
73
+ - lib/active_collab/login_response.rb
74
+ - lib/active_collab/projects.rb
75
+ - lib/active_collab/response.rb
76
+ - lib/active_collab/task_lists.rb
77
+ - lib/active_collab/tasks.rb
78
+ - lib/active_collab/time_records.rb
79
+ - lib/active_collab/token.rb
80
+ - lib/active_collab/users.rb
81
+ - lib/active_collab/version.rb
82
+ homepage: https://github.com/davelens/active_collab-ruby-sdk
83
+ licenses:
84
+ - MIT
85
+ metadata:
86
+ homepage_uri: https://github.com/davelens/active_collab-ruby-sdk
87
+ source_code_uri: https://github.com/davelens/active_collab-ruby-sdk
88
+ changelog_uri: https://github.com/davelens/active_collab-ruby-sdk/blob/master/CHANGELOG.md
89
+ rubygems_mfa_required: 'true'
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '3.1'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 4.0.6
105
+ specification_version: 4
106
+ summary: Ruby SDK for Active Collab API
107
+ test_files: []