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 +7 -0
- data/CHANGELOG.md +88 -0
- data/LICENSE +21 -0
- data/README.md +175 -0
- data/lib/active_collab/client.rb +216 -0
- data/lib/active_collab/errors.rb +36 -0
- data/lib/active_collab/login_response.rb +48 -0
- data/lib/active_collab/projects.rb +42 -0
- data/lib/active_collab/response.rb +43 -0
- data/lib/active_collab/task_lists.rb +20 -0
- data/lib/active_collab/tasks.rb +94 -0
- data/lib/active_collab/time_records.rb +29 -0
- data/lib/active_collab/token.rb +21 -0
- data/lib/active_collab/users.rb +16 -0
- data/lib/active_collab/version.rb +5 -0
- data/lib/active_collab.rb +21 -0
- metadata +107 -0
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
|
+
[](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,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: []
|