d4h_api 2.0.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
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +136 -0
- data/README.md +785 -0
- data/d4h_api.gemspec +28 -0
- data/lib/d4h/api/client.rb +216 -0
- data/lib/d4h/api/collection.rb +55 -0
- data/lib/d4h/api/error.rb +31 -0
- data/lib/d4h/api/model.rb +57 -0
- data/lib/d4h/api/models/animal.rb +8 -0
- data/lib/d4h/api/models/animal_group.rb +8 -0
- data/lib/d4h/api/models/animal_group_membership.rb +8 -0
- data/lib/d4h/api/models/animal_qualification.rb +8 -0
- data/lib/d4h/api/models/attendance.rb +8 -0
- data/lib/d4h/api/models/custom_field.rb +8 -0
- data/lib/d4h/api/models/custom_field_for_entity.rb +8 -0
- data/lib/d4h/api/models/customer_identifier.rb +8 -0
- data/lib/d4h/api/models/d4h_module.rb +8 -0
- data/lib/d4h/api/models/d4h_task.rb +8 -0
- data/lib/d4h/api/models/document.rb +8 -0
- data/lib/d4h/api/models/duty.rb +8 -0
- data/lib/d4h/api/models/equipment.rb +8 -0
- data/lib/d4h/api/models/equipment_brand.rb +8 -0
- data/lib/d4h/api/models/equipment_category.rb +8 -0
- data/lib/d4h/api/models/equipment_fund.rb +8 -0
- data/lib/d4h/api/models/equipment_inspection.rb +8 -0
- data/lib/d4h/api/models/equipment_inspection_result.rb +8 -0
- data/lib/d4h/api/models/equipment_inspection_step.rb +8 -0
- data/lib/d4h/api/models/equipment_inspection_step_result.rb +8 -0
- data/lib/d4h/api/models/equipment_kind.rb +8 -0
- data/lib/d4h/api/models/equipment_location.rb +8 -0
- data/lib/d4h/api/models/equipment_model.rb +8 -0
- data/lib/d4h/api/models/equipment_retired_reason.rb +8 -0
- data/lib/d4h/api/models/equipment_supplier.rb +8 -0
- data/lib/d4h/api/models/equipment_supplier_ref.rb +8 -0
- data/lib/d4h/api/models/equipment_usage.rb +8 -0
- data/lib/d4h/api/models/event.rb +8 -0
- data/lib/d4h/api/models/exercise.rb +8 -0
- data/lib/d4h/api/models/handler_group.rb +8 -0
- data/lib/d4h/api/models/handler_group_membership.rb +8 -0
- data/lib/d4h/api/models/handler_qualification.rb +8 -0
- data/lib/d4h/api/models/health_safety_category.rb +8 -0
- data/lib/d4h/api/models/health_safety_report.rb +8 -0
- data/lib/d4h/api/models/health_safety_severity.rb +8 -0
- data/lib/d4h/api/models/incident.rb +8 -0
- data/lib/d4h/api/models/incident_involved_injury.rb +8 -0
- data/lib/d4h/api/models/incident_involved_metadata.rb +8 -0
- data/lib/d4h/api/models/incident_involved_person.rb +8 -0
- data/lib/d4h/api/models/location_bookmark.rb +8 -0
- data/lib/d4h/api/models/member.rb +8 -0
- data/lib/d4h/api/models/member_custom_status.rb +8 -0
- data/lib/d4h/api/models/member_group.rb +8 -0
- data/lib/d4h/api/models/member_group_membership.rb +8 -0
- data/lib/d4h/api/models/member_qualification.rb +8 -0
- data/lib/d4h/api/models/member_qualification_award.rb +8 -0
- data/lib/d4h/api/models/member_retired_reason.rb +8 -0
- data/lib/d4h/api/models/organisation.rb +8 -0
- data/lib/d4h/api/models/repair.rb +8 -0
- data/lib/d4h/api/models/resource_bundle.rb +8 -0
- data/lib/d4h/api/models/role.rb +8 -0
- data/lib/d4h/api/models/search_result.rb +8 -0
- data/lib/d4h/api/models/tag.rb +8 -0
- data/lib/d4h/api/models/team.rb +8 -0
- data/lib/d4h/api/models/whiteboard.rb +8 -0
- data/lib/d4h/api/models/whoami.rb +8 -0
- data/lib/d4h/api/resource.rb +171 -0
- data/lib/d4h/api/resources/animal_group_membership_resource.rb +21 -0
- data/lib/d4h/api/resources/animal_group_resource.rb +33 -0
- data/lib/d4h/api/resources/animal_qualification_resource.rb +21 -0
- data/lib/d4h/api/resources/animal_resource.rb +21 -0
- data/lib/d4h/api/resources/attendance_resource.rb +25 -0
- data/lib/d4h/api/resources/custom_field_for_entity_resource.rb +17 -0
- data/lib/d4h/api/resources/custom_field_resource.rb +33 -0
- data/lib/d4h/api/resources/customer_identifier_resource.rb +17 -0
- data/lib/d4h/api/resources/d4h_module_resource.rb +17 -0
- data/lib/d4h/api/resources/d4h_task_resource.rb +17 -0
- data/lib/d4h/api/resources/document_resource.rb +33 -0
- data/lib/d4h/api/resources/duty_resource.rb +21 -0
- data/lib/d4h/api/resources/equipment_brand_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_category_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_fund_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_inspection_resource.rb +21 -0
- data/lib/d4h/api/resources/equipment_inspection_result_resource.rb +29 -0
- data/lib/d4h/api/resources/equipment_inspection_step_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_inspection_step_result_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_kind_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_location_resource.rb +21 -0
- data/lib/d4h/api/resources/equipment_model_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_retired_reason_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_supplier_ref_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_supplier_resource.rb +33 -0
- data/lib/d4h/api/resources/equipment_usage_resource.rb +33 -0
- data/lib/d4h/api/resources/event_resource.rb +29 -0
- data/lib/d4h/api/resources/exercise_resource.rb +33 -0
- data/lib/d4h/api/resources/handler_group_membership_resource.rb +21 -0
- data/lib/d4h/api/resources/handler_group_resource.rb +33 -0
- data/lib/d4h/api/resources/handler_qualification_resource.rb +21 -0
- data/lib/d4h/api/resources/health_safety_category_resource.rb +33 -0
- data/lib/d4h/api/resources/health_safety_report_resource.rb +21 -0
- data/lib/d4h/api/resources/health_safety_severity_resource.rb +33 -0
- data/lib/d4h/api/resources/incident_involved_injury_resource.rb +21 -0
- data/lib/d4h/api/resources/incident_involved_metadata_resource.rb +17 -0
- data/lib/d4h/api/resources/incident_involved_person_resource.rb +21 -0
- data/lib/d4h/api/resources/incident_resource.rb +29 -0
- data/lib/d4h/api/resources/location_bookmark_resource.rb +21 -0
- data/lib/d4h/api/resources/member_custom_status_resource.rb +17 -0
- data/lib/d4h/api/resources/member_group_membership_resource.rb +21 -0
- data/lib/d4h/api/resources/member_group_resource.rb +33 -0
- data/lib/d4h/api/resources/member_qualification_award_resource.rb +21 -0
- data/lib/d4h/api/resources/member_qualification_resource.rb +21 -0
- data/lib/d4h/api/resources/member_resource.rb +21 -0
- data/lib/d4h/api/resources/member_retired_reason_resource.rb +17 -0
- data/lib/d4h/api/resources/organisation_resource.rb +13 -0
- data/lib/d4h/api/resources/repair_resource.rb +33 -0
- data/lib/d4h/api/resources/resource_bundle_resource.rb +21 -0
- data/lib/d4h/api/resources/role_resource.rb +21 -0
- data/lib/d4h/api/resources/search_result_resource.rb +17 -0
- data/lib/d4h/api/resources/tag_resource.rb +33 -0
- data/lib/d4h/api/resources/team_resource.rb +13 -0
- data/lib/d4h/api/resources/whiteboard_resource.rb +33 -0
- data/lib/d4h/api/resources/whoami_resource.rb +13 -0
- data/lib/d4h.rb +156 -0
- data.tar.gz.sig +0 -0
- metadata +264 -0
- metadata.gz.sig +0 -0
data/README.md
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
# D4H API
|
|
2
|
+
|
|
3
|
+
A Ruby gem wrapping the [D4H Developer API v3](https://api.d4h.com/v3/documentation) with a thin, idiomatic interface. Every API resource is mapped to a Ruby object with dot-notation attribute access, paginated collections, and full CRUD where the API allows it.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby >= 4.0
|
|
8
|
+
- A D4H API token ([generate one in D4H](https://support.d4h.com/en/articles/2334703-api-access))
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add to your Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "d4h_api", github: "rockymountainrescue/d4h_api"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then run:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install directly:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gem install d4h_api
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
require "d4h"
|
|
34
|
+
|
|
35
|
+
client = D4H::API::Client.new(
|
|
36
|
+
api_key: ENV.fetch("D4H_TOKEN"),
|
|
37
|
+
context_id: ENV.fetch("D4H_TEAM_ID").to_i,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Who am I?
|
|
41
|
+
me = client.whoami.show
|
|
42
|
+
puts "#{me.name} (#{me.email})"
|
|
43
|
+
|
|
44
|
+
# List all operational members
|
|
45
|
+
members = client.member.list(status: "OPERATIONAL")
|
|
46
|
+
members.each { |m| puts m.name }
|
|
47
|
+
|
|
48
|
+
# Show team info
|
|
49
|
+
team = client.team.show(id: 42)
|
|
50
|
+
puts "#{team.title} — #{team.country}"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
### Client Initialization
|
|
56
|
+
|
|
57
|
+
The client requires an `api_key` and a `context_id` (your D4H team ID). The `context` defaults to `"team"` but can be set to `"organisation"` for organisation-scoped API calls.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Team context (default)
|
|
61
|
+
client = D4H::API::Client.new(
|
|
62
|
+
api_key: ENV.fetch("D4H_TOKEN"),
|
|
63
|
+
context_id: ENV.fetch("D4H_TEAM_ID").to_i,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Organisation context
|
|
67
|
+
client = D4H::API::Client.new(
|
|
68
|
+
api_key: ENV.fetch("D4H_TOKEN"),
|
|
69
|
+
context: "organisation",
|
|
70
|
+
context_id: ENV.fetch("D4H_ORG_ID").to_i,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# EU or other regional endpoint
|
|
74
|
+
client = D4H::API::Client.new(
|
|
75
|
+
api_key: ENV.fetch("D4H_TOKEN"),
|
|
76
|
+
context_id: ENV.fetch("D4H_TEAM_ID").to_i,
|
|
77
|
+
base_url: "https://api.team-manager.eu.d4h.com",
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Environment Variables
|
|
82
|
+
|
|
83
|
+
The client reads the following environment variables as defaults. All can be overridden via constructor arguments.
|
|
84
|
+
|
|
85
|
+
| Variable | Default | Constructor param | Description |
|
|
86
|
+
|----------------|---------------------------------------|-------------------|---------------------------------------------------------------------------|
|
|
87
|
+
| `D4H_TOKEN` | *(required)* | `api_key:` | Your D4H API Bearer token. Generate one in your [D4H account settings](https://support.d4h.com/en/articles/2334703-api-access). |
|
|
88
|
+
| `D4H_TEAM_ID` | *(required)* | `context_id:` | Your D4H team (or organisation) numeric ID. Find it in your D4H URL or via the API. |
|
|
89
|
+
| `D4H_BASE_URL` | `https://api.team-manager.us.d4h.com` | `base_url:` | Base URL for the D4H API. Change for EU (`https://api.team-manager.eu.d4h.com`) or other regional endpoints. |
|
|
90
|
+
|
|
91
|
+
A typical `.env` file:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
D4H_TOKEN="your-api-token-here"
|
|
95
|
+
D4H_TEAM_ID="42"
|
|
96
|
+
# D4H_BASE_URL="https://api.team-manager.eu.d4h.com" # uncomment for EU
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Architecture
|
|
100
|
+
|
|
101
|
+
The gem is built around four core classes that work together in a simple pipeline:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Client ──▶ Resource ──▶ Model / Collection
|
|
105
|
+
│ │ │
|
|
106
|
+
│ Faraday │ HTTP verbs │ Dot-notation
|
|
107
|
+
│ connection │ + URL routing │ attribute access
|
|
108
|
+
│ + auth │ + pagination │ + Enumerable
|
|
109
|
+
│ + retry │ + error check │
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### How a request flows
|
|
113
|
+
|
|
114
|
+
When you call `client.member.list(status: "OPERATIONAL")`, here's what happens:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
1. client.member → creates a MemberResource bound to the client
|
|
118
|
+
2. .list(status: "...") → MemberResource calls get_request on the resource URL
|
|
119
|
+
3. Resource builds the URL → "v3/team/42/members" (from base_path + SUB_URL)
|
|
120
|
+
4. Resource adds auth → Authorization: Bearer <token>
|
|
121
|
+
5. Faraday sends GET → GET https://api.team-manager.us.d4h.com/v3/team/42/members?status=OPERATIONAL
|
|
122
|
+
6. Retry middleware → 429/5xx? → exponential backoff and retry (up to 3 times)
|
|
123
|
+
7. Resource checks status → 2xx → continue, otherwise raise D4H::API::Error
|
|
124
|
+
8. Response body parsed → Collection wraps the JSON envelope, each result becomes a Member model
|
|
125
|
+
9. You get back a → Collection (Enumerable) of Member objects with dot-notation access
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### The four core classes
|
|
129
|
+
|
|
130
|
+
**`D4H::API::Client`** is the entry point. It holds your API credentials, builds the Faraday HTTP connection, and exposes 56 resource accessor methods. Each accessor returns a fresh Resource instance bound to the client:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
client = D4H::API::Client.new(api_key: "token", context_id: 42)
|
|
134
|
+
|
|
135
|
+
client.member # => MemberResource.new(client)
|
|
136
|
+
client.equipment # => EquipmentResource.new(client)
|
|
137
|
+
client.event # => EventResource.new(client)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The client builds a **base path** from the context — `v3/team/42` for team context, `v3/organisation/99` for organisation context — which all resources prepend to their endpoint URLs.
|
|
141
|
+
|
|
142
|
+
**`D4H::API::Resource`** is the base class for all 56 resource endpoints. It provides five HTTP verb methods (`get_request`, `post_request`, `put_request`, `patch_request`, `delete_request`), each of which injects the Bearer token header and checks the response status. Every subclass defines a `SUB_URL` constant and implements only the CRUD methods the D4H API supports for that resource:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class TagResource < Resource
|
|
146
|
+
SUB_URL = "tags" # → URL becomes "v3/team/42/tags"
|
|
147
|
+
|
|
148
|
+
def list(**params) # GET /v3/team/42/tags
|
|
149
|
+
def show(id:) # GET /v3/team/42/tags/{id}
|
|
150
|
+
def create(data) # POST /v3/team/42/tags
|
|
151
|
+
def update(id:, **params) # PATCH /v3/team/42/tags/{id}
|
|
152
|
+
def destroy(id:) # DELETE /v3/team/42/tags/{id}
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Resource also provides a private `paginate_all` helper used by `list_all` methods — it fetches pages in a loop until all results are collected, then returns a single Collection.
|
|
157
|
+
|
|
158
|
+
**`D4H::API::Model`** wraps a JSON response hash in an OpenStruct with recursive conversion, so nested hashes and arrays become dot-accessible objects all the way down:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# The API returns: {"id" => 10, "brand" => {"id" => 3, "title" => "Petzl"}}
|
|
162
|
+
# Model gives you: item.brand.title # => "Petzl"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Each API resource has a corresponding thin Model subclass (e.g. `Member`, `Event`, `Equipment`) that inherits from Model. These exist for type identification — `item.is_a?(D4H::API::Equipment)` — but add no extra behavior. The original JSON hash is preserved in `#to_json`.
|
|
166
|
+
|
|
167
|
+
**`D4H::API::Collection`** wraps the D4H v3 list envelope (`results`, `page`, `pageSize`, `totalSize`). It converts each result into the appropriate Model subclass and includes `Enumerable`, so you can use `map`, `select`, `first`, `count`, and all other Enumerable methods directly:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
collection = client.member.list # Collection of Member models
|
|
171
|
+
collection.total_size # pagination metadata
|
|
172
|
+
collection.map(&:name) # Enumerable — iterate the results
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### File layout
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
lib/
|
|
179
|
+
d4h.rb # Gem entry point — Zeitwerk setup + autoloads
|
|
180
|
+
d4h/api/
|
|
181
|
+
client.rb # Client — connection + 56 resource accessors
|
|
182
|
+
resource.rb # Resource — HTTP verbs, auth, pagination
|
|
183
|
+
model.rb # Model — recursive OpenStruct wrapper
|
|
184
|
+
collection.rb # Collection — Enumerable list envelope
|
|
185
|
+
error.rb # Error + RetriableError — raised on failures
|
|
186
|
+
models/ # 56 thin Model subclasses (member.rb, event.rb, ...)
|
|
187
|
+
resources/ # 56 Resource subclasses (member_resource.rb, ...)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Method signatures
|
|
191
|
+
|
|
192
|
+
Resources follow consistent method signatures depending on the operation:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
# List — returns a Collection, accepts filter params
|
|
196
|
+
client.member.list(status: "OPERATIONAL", size: 10)
|
|
197
|
+
|
|
198
|
+
# List all — auto-paginates, same params as list
|
|
199
|
+
client.member.list_all(status: "OPERATIONAL")
|
|
200
|
+
|
|
201
|
+
# Show — returns a single Model, requires id:
|
|
202
|
+
client.event.show(id: 1)
|
|
203
|
+
|
|
204
|
+
# Create — returns the created Model, accepts a Hash body
|
|
205
|
+
client.event.create({"title" => "Training", "startsAt" => "2026-03-09T08:00:00Z"})
|
|
206
|
+
|
|
207
|
+
# Update — returns the updated Model, requires id: plus keyword params
|
|
208
|
+
client.event.update(id: 1, title: "Updated Training")
|
|
209
|
+
|
|
210
|
+
# Destroy — returns the raw response, requires id:
|
|
211
|
+
client.tag.destroy(id: 5)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Two special cases: `whoami.show` takes no arguments (it returns the authenticated user), and `document.update` uses HTTP PUT instead of PATCH per the D4H API contract.
|
|
215
|
+
|
|
216
|
+
## Usage
|
|
217
|
+
|
|
218
|
+
### Response Objects
|
|
219
|
+
|
|
220
|
+
Every API call returns a **Model** — an OpenStruct with recursive dot-notation access to all attributes, including nested hashes and arrays.
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
event = client.event.show(id: 1)
|
|
224
|
+
|
|
225
|
+
event.id # => 1
|
|
226
|
+
event.reference # => "EVT-001"
|
|
227
|
+
event.description # => "Monthly training drill"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Nested data is automatically accessible:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
item = client.equipment.show(id: 10)
|
|
234
|
+
|
|
235
|
+
item.ref # => "E010"
|
|
236
|
+
item.brand.title # => "Petzl"
|
|
237
|
+
item.owner.id # => 42
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The original JSON hash is always available via `#to_json`:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
event.to_json
|
|
244
|
+
# => {"id" => 1, "reference" => "EVT-001", "description" => "Monthly training drill"}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Collections
|
|
248
|
+
|
|
249
|
+
List endpoints return a **Collection** — an Enumerable wrapper around paginated results.
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
members = client.member.list
|
|
253
|
+
|
|
254
|
+
members.results # => Array of Member models
|
|
255
|
+
members.total_size # => 90
|
|
256
|
+
members.page # => 0
|
|
257
|
+
members.page_size # => 25
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Collections include `Enumerable`, so you can use `each`, `map`, `select`, `first`, and more:
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
# Get all member names
|
|
264
|
+
names = client.member.list.map(&:name)
|
|
265
|
+
|
|
266
|
+
# Find operational members
|
|
267
|
+
ops = client.member.list(status: "OPERATIONAL").select { |m| m.status == "OPERATIONAL" }
|
|
268
|
+
|
|
269
|
+
# Grab the first result
|
|
270
|
+
leader = client.role.list.first
|
|
271
|
+
puts leader.title # => "Team Leader"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Pagination
|
|
275
|
+
|
|
276
|
+
Single-page results use `list`. To automatically fetch **all pages**, use `list_all`:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# Fetch first page (default 25 results)
|
|
280
|
+
page = client.member.list
|
|
281
|
+
|
|
282
|
+
# Fetch ALL members across all pages (250 per page by default)
|
|
283
|
+
everyone = client.member.list_all
|
|
284
|
+
everyone.total_size # => 90
|
|
285
|
+
everyone.count # => 90
|
|
286
|
+
|
|
287
|
+
# Custom page size
|
|
288
|
+
everyone = client.member.list_all(size: 50)
|
|
289
|
+
|
|
290
|
+
# Combine with filters
|
|
291
|
+
operational = client.member.list_all(status: "OPERATIONAL")
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Error Handling
|
|
295
|
+
|
|
296
|
+
Non-2xx responses raise `D4H::API::Error` with the API's error message:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
begin
|
|
300
|
+
client.equipment.show(id: 999_999)
|
|
301
|
+
rescue D4H::API::Error => e
|
|
302
|
+
puts e.message # => "Not Found: Equipment not found"
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Transient errors (429 rate limit, 500, 502, 503, 504) raise `D4H::API::RetriableError`, a subclass of `Error`. You can rescue either:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Catch only transient failures (after retries are exhausted)
|
|
310
|
+
rescue D4H::API::RetriableError => e
|
|
311
|
+
puts "Server is overloaded: #{e.message}"
|
|
312
|
+
|
|
313
|
+
# Catch all API errors (including transient)
|
|
314
|
+
rescue D4H::API::Error => e
|
|
315
|
+
puts "Something went wrong: #{e.message}"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Retry & Rate Limiting
|
|
319
|
+
|
|
320
|
+
The client automatically retries transient errors with exponential backoff. This handles the D4H API's [sliding-window rate limiting](https://api.d4h.com/v3/documentation) — when request frequency exceeds the limit, the API returns 429 and the client backs off and retries.
|
|
321
|
+
|
|
322
|
+
**Default behavior:**
|
|
323
|
+
- Retries up to **3 times** on 429, 500, 502, 503, and 504 responses
|
|
324
|
+
- Exponential backoff: **1s, 2s, 4s** (doubles each retry), capped at 30s
|
|
325
|
+
- Respects the D4H API's `ratelimit` response headers for wait times
|
|
326
|
+
- Retries **all HTTP methods** (GET, POST, PATCH, PUT, DELETE)
|
|
327
|
+
- Logs each retry to stderr: `[D4H] Retry 1/3 for GET .../members ...`
|
|
328
|
+
|
|
329
|
+
**Customize retry behavior:**
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# More retries for batch scripts
|
|
333
|
+
client = D4H::API::Client.new(
|
|
334
|
+
api_key: "your-token",
|
|
335
|
+
context_id: 42,
|
|
336
|
+
max_retries: 5,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Disable retries entirely
|
|
340
|
+
client = D4H::API::Client.new(
|
|
341
|
+
api_key: "your-token",
|
|
342
|
+
context_id: 42,
|
|
343
|
+
max_retries: 0,
|
|
344
|
+
)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
If all retries are exhausted, the `RetriableError` propagates to your code so you can handle it as needed.
|
|
348
|
+
|
|
349
|
+
## API Resources
|
|
350
|
+
|
|
351
|
+
### Team & Identity
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
# Show your team's info (requires the team's own ID)
|
|
355
|
+
team = client.team.show(id: 42)
|
|
356
|
+
team.title # => "Rocky Mountain Rescue"
|
|
357
|
+
team.timezone # => "America/Denver"
|
|
358
|
+
team.memberCounts.total # => 90
|
|
359
|
+
team.memberCounts.operational # => 85
|
|
360
|
+
|
|
361
|
+
# Show your own profile
|
|
362
|
+
me = client.whoami.show
|
|
363
|
+
me.name # => "John Doe"
|
|
364
|
+
me.email # => "john@example.com"
|
|
365
|
+
|
|
366
|
+
# Show an organisation
|
|
367
|
+
org = client.organisation.show(id: 5)
|
|
368
|
+
org.title # => "Colorado SAR"
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Members
|
|
372
|
+
|
|
373
|
+
Members can be listed, filtered, and updated — but not created or destroyed through the API.
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
# List members
|
|
377
|
+
members = client.member.list
|
|
378
|
+
members.each { |m| puts "#{m.name}: #{m.status}" }
|
|
379
|
+
|
|
380
|
+
# Filter by status
|
|
381
|
+
active = client.member.list(status: "OPERATIONAL")
|
|
382
|
+
|
|
383
|
+
# Fetch all members across pages
|
|
384
|
+
everyone = client.member.list_all
|
|
385
|
+
puts "Total members: #{everyone.total_size}"
|
|
386
|
+
|
|
387
|
+
# Update a member
|
|
388
|
+
updated = client.member.update(id: 1, name: "Alice Smith")
|
|
389
|
+
puts updated.name # => "Alice Smith"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Events
|
|
393
|
+
|
|
394
|
+
Events support list, show, create, and update — but not destroy.
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
# List events
|
|
398
|
+
events = client.event.list
|
|
399
|
+
events.each { |e| puts "#{e.reference}: #{e.description}" }
|
|
400
|
+
|
|
401
|
+
# Show a specific event
|
|
402
|
+
event = client.event.show(id: 1)
|
|
403
|
+
puts event.description # => "Monthly drill"
|
|
404
|
+
|
|
405
|
+
# Create an event
|
|
406
|
+
new_event = client.event.create({
|
|
407
|
+
"reference" => "EVT-010",
|
|
408
|
+
"startsAt" => "2026-03-09T08:00:00Z",
|
|
409
|
+
"endsAt" => "2026-03-09T17:00:00Z",
|
|
410
|
+
"title" => "Spring Training",
|
|
411
|
+
})
|
|
412
|
+
puts new_event.id # => 10
|
|
413
|
+
|
|
414
|
+
# Update an event
|
|
415
|
+
client.event.update(id: 1, description: "Updated drill description")
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Incidents
|
|
419
|
+
|
|
420
|
+
Incidents support list, show, create, and update — but not destroy.
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
# List incidents
|
|
424
|
+
incidents = client.incident.list_all
|
|
425
|
+
incidents.each { |i| puts "#{i.reference}: #{i.description}" }
|
|
426
|
+
|
|
427
|
+
# Show a specific incident
|
|
428
|
+
incident = client.incident.show(id: 7)
|
|
429
|
+
puts incident.description # => "Missing hiker"
|
|
430
|
+
|
|
431
|
+
# Create an incident
|
|
432
|
+
new_incident = client.incident.create({
|
|
433
|
+
"reference" => "INC-008",
|
|
434
|
+
"description" => "Lost hikers near Flatirons",
|
|
435
|
+
})
|
|
436
|
+
puts new_incident.id # => 8
|
|
437
|
+
|
|
438
|
+
# Update an incident
|
|
439
|
+
client.incident.update(id: 7, description: "Missing hiker — found safe")
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Exercises
|
|
443
|
+
|
|
444
|
+
Exercises support full CRUD.
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
# List exercises
|
|
448
|
+
client.exercise.list.each { |ex| puts ex.reference }
|
|
449
|
+
|
|
450
|
+
# Create an exercise
|
|
451
|
+
ex = client.exercise.create({"reference" => "EX-005", "title" => "Night Navigation"})
|
|
452
|
+
|
|
453
|
+
# Update an exercise
|
|
454
|
+
client.exercise.update(id: ex.id, title: "Night Navigation — Advanced")
|
|
455
|
+
|
|
456
|
+
# Destroy an exercise
|
|
457
|
+
client.exercise.destroy(id: ex.id)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Attendance
|
|
461
|
+
|
|
462
|
+
Attendance records can be listed, shown, and created — but not updated or destroyed.
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
# List attendance
|
|
466
|
+
records = client.attendance.list
|
|
467
|
+
records.each { |a| puts "#{a.status} — Member #{a.member.id}" }
|
|
468
|
+
|
|
469
|
+
# Show a specific attendance record
|
|
470
|
+
att = client.attendance.show(id: 100)
|
|
471
|
+
puts att.status # => "ATTENDING"
|
|
472
|
+
puts att.member.id # => 1
|
|
473
|
+
|
|
474
|
+
# Record attendance
|
|
475
|
+
new_att = client.attendance.create({
|
|
476
|
+
"memberId" => 1,
|
|
477
|
+
"activityId" => 5,
|
|
478
|
+
"status" => "ATTENDING",
|
|
479
|
+
})
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Equipment
|
|
483
|
+
|
|
484
|
+
Equipment supports full CRUD with nested data.
|
|
485
|
+
|
|
486
|
+
```ruby
|
|
487
|
+
# List equipment with filters
|
|
488
|
+
critical = client.equipment.list(is_critical: true, size: 10)
|
|
489
|
+
|
|
490
|
+
# Show equipment details
|
|
491
|
+
item = client.equipment.show(id: 10)
|
|
492
|
+
puts item.ref # => "E010"
|
|
493
|
+
puts item.brand.title # => "Petzl"
|
|
494
|
+
puts item.owner.id # => 42
|
|
495
|
+
|
|
496
|
+
# Create equipment
|
|
497
|
+
new_item = client.equipment.create({
|
|
498
|
+
"ref" => "E100",
|
|
499
|
+
"categoryId" => 1,
|
|
500
|
+
"kindId" => 2,
|
|
501
|
+
})
|
|
502
|
+
puts new_item.ref # => "E100"
|
|
503
|
+
|
|
504
|
+
# Update equipment
|
|
505
|
+
client.equipment.update(id: 10, ref: "E010-A")
|
|
506
|
+
|
|
507
|
+
# Destroy equipment
|
|
508
|
+
client.equipment.destroy(id: 10)
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Documents
|
|
512
|
+
|
|
513
|
+
Documents support full CRUD. Note that **update uses PUT** (not PATCH) per the D4H API.
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
# List documents
|
|
517
|
+
docs = client.document.list
|
|
518
|
+
docs.each { |d| puts d.title }
|
|
519
|
+
|
|
520
|
+
# Show a document
|
|
521
|
+
doc = client.document.show(id: 1)
|
|
522
|
+
puts doc.title # => "SOP Manual"
|
|
523
|
+
|
|
524
|
+
# Create a document
|
|
525
|
+
new_doc = client.document.create({"title" => "New Procedure"})
|
|
526
|
+
|
|
527
|
+
# Update a document (uses PUT)
|
|
528
|
+
client.document.update(id: 1, title: "Updated SOP Manual")
|
|
529
|
+
|
|
530
|
+
# Destroy a document
|
|
531
|
+
client.document.destroy(id: 1)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Tags
|
|
535
|
+
|
|
536
|
+
Tags support full CRUD.
|
|
537
|
+
|
|
538
|
+
```ruby
|
|
539
|
+
# List tags
|
|
540
|
+
client.tag.list.each { |t| puts t.title }
|
|
541
|
+
|
|
542
|
+
# Show a tag
|
|
543
|
+
tag = client.tag.show(id: 5)
|
|
544
|
+
puts tag.title # => "Avalanche"
|
|
545
|
+
|
|
546
|
+
# Create a tag
|
|
547
|
+
new_tag = client.tag.create({"title" => "High Angle"})
|
|
548
|
+
puts new_tag.id # => 10
|
|
549
|
+
|
|
550
|
+
# Update a tag
|
|
551
|
+
client.tag.update(id: 5, title: "Avalanche Response")
|
|
552
|
+
|
|
553
|
+
# Destroy a tag
|
|
554
|
+
client.tag.destroy(id: 5)
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Custom Fields
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
# List custom fields
|
|
561
|
+
fields = client.custom_field.list
|
|
562
|
+
fields.each { |f| puts "#{f.title} (#{f.type})" }
|
|
563
|
+
|
|
564
|
+
# Show a custom field
|
|
565
|
+
cf = client.custom_field.show(id: 3)
|
|
566
|
+
puts cf.title # => "Badge Number"
|
|
567
|
+
puts cf.type # => "TEXT"
|
|
568
|
+
|
|
569
|
+
# Create / update / destroy
|
|
570
|
+
client.custom_field.create({"title" => "Radio Call Sign", "type" => "TEXT"})
|
|
571
|
+
client.custom_field.update(id: 3, title: "Employee Badge")
|
|
572
|
+
client.custom_field.destroy(id: 3)
|
|
573
|
+
|
|
574
|
+
# List custom field options for entities
|
|
575
|
+
options = client.custom_field_for_entity.list
|
|
576
|
+
opt = client.custom_field_for_entity.show(id: 1)
|
|
577
|
+
puts opt.label # => "Option A"
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Member Groups & Qualifications
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
# Member groups (full CRUD)
|
|
584
|
+
groups = client.member_group.list
|
|
585
|
+
client.member_group.create({"title" => "Bravo Team"})
|
|
586
|
+
client.member_group.destroy(id: 1)
|
|
587
|
+
|
|
588
|
+
# Member group memberships (read-only)
|
|
589
|
+
memberships = client.member_group_membership.list
|
|
590
|
+
|
|
591
|
+
# Member qualifications (read-only)
|
|
592
|
+
quals = client.member_qualification.list
|
|
593
|
+
quals.each { |q| puts q.title }
|
|
594
|
+
|
|
595
|
+
# Award a qualification to a member (create only, no show/update/destroy)
|
|
596
|
+
client.member_qualification_award.create({
|
|
597
|
+
"memberId" => 10,
|
|
598
|
+
"qualificationId" => 5,
|
|
599
|
+
})
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Roles & Duties
|
|
603
|
+
|
|
604
|
+
```ruby
|
|
605
|
+
# List and show roles (read-only)
|
|
606
|
+
roles = client.role.list
|
|
607
|
+
role = client.role.show(id: 1)
|
|
608
|
+
puts role.title # => "Team Leader"
|
|
609
|
+
|
|
610
|
+
# List and show duties (read-only)
|
|
611
|
+
duties = client.duty.list
|
|
612
|
+
duty = client.duty.show(id: 1)
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Health & Safety
|
|
616
|
+
|
|
617
|
+
```ruby
|
|
618
|
+
# Reports (read-only)
|
|
619
|
+
reports = client.health_safety_report.list
|
|
620
|
+
puts reports.first.title # => "Near miss"
|
|
621
|
+
|
|
622
|
+
# Categories (full CRUD)
|
|
623
|
+
categories = client.health_safety_category.list
|
|
624
|
+
client.health_safety_category.create({"title" => "Equipment Failure"})
|
|
625
|
+
|
|
626
|
+
# Severities (full CRUD)
|
|
627
|
+
client.health_safety_severity.list
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Animals & Handlers
|
|
631
|
+
|
|
632
|
+
```ruby
|
|
633
|
+
# Animals (read-only)
|
|
634
|
+
animals = client.animal.list
|
|
635
|
+
animal = client.animal.show(id: 1)
|
|
636
|
+
puts "#{animal.name} — #{animal.breed}"
|
|
637
|
+
|
|
638
|
+
# Animal groups (full CRUD)
|
|
639
|
+
client.animal_group.create({"title" => "Tracking Dogs"})
|
|
640
|
+
client.animal_group.destroy(id: 2)
|
|
641
|
+
|
|
642
|
+
# Handler groups (full CRUD)
|
|
643
|
+
client.handler_group.list
|
|
644
|
+
|
|
645
|
+
# Handler qualifications (read-only)
|
|
646
|
+
client.handler_qualification.list
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Whiteboard
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
# Full CRUD
|
|
653
|
+
notes = client.whiteboard.list
|
|
654
|
+
note = client.whiteboard.create({"title" => "Weather advisory"})
|
|
655
|
+
client.whiteboard.update(id: note.id, title: "Storm warning")
|
|
656
|
+
client.whiteboard.destroy(id: note.id)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Repairs
|
|
660
|
+
|
|
661
|
+
```ruby
|
|
662
|
+
# Full CRUD
|
|
663
|
+
repairs = client.repair.list
|
|
664
|
+
repair = client.repair.create({"description" => "Replace worn rope"})
|
|
665
|
+
client.repair.update(id: repair.id, description: "Replace worn rope — completed")
|
|
666
|
+
client.repair.destroy(id: repair.id)
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Search
|
|
670
|
+
|
|
671
|
+
```ruby
|
|
672
|
+
# Search across resources (read-only)
|
|
673
|
+
results = client.search.list(query: "rope")
|
|
674
|
+
results.each { |r| puts "#{r.resourceType}: #{r.title}" }
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
## Complete Resource Reference
|
|
678
|
+
|
|
679
|
+
Every resource is accessible as a method on the client. The table below shows which operations each resource supports.
|
|
680
|
+
|
|
681
|
+
| Client Method | API Endpoint | list | show | create | update | destroy |
|
|
682
|
+
|---|---|:---:|:---:|:---:|:---:|:---:|
|
|
683
|
+
| `animal` | animals | x | x | | | |
|
|
684
|
+
| `animal_group` | animal-groups | x | x | x | x | x |
|
|
685
|
+
| `animal_group_membership` | animal-group-memberships | x | x | | | |
|
|
686
|
+
| `animal_qualification` | animal-qualifications | x | x | | | |
|
|
687
|
+
| `attendance` | attendance | x | x | x | | |
|
|
688
|
+
| `custom_field` | custom-fields | x | x | x | x | x |
|
|
689
|
+
| `custom_field_for_entity` | custom-field-options | x | x | | | |
|
|
690
|
+
| `customer_identifier` | customer-identifiers | x | | | | |
|
|
691
|
+
| `d4h_module` | modules | x | | | | |
|
|
692
|
+
| `d4h_task` | tasks | x | | | | |
|
|
693
|
+
| `document` | documents | x | x | x | x* | x |
|
|
694
|
+
| `duty` | duties | x | x | | | |
|
|
695
|
+
| `equipment` | equipment | x | x | x | x | x |
|
|
696
|
+
| `equipment_brand` | equipment-brands | x | x | x | x | x |
|
|
697
|
+
| `equipment_category` | equipment-categories | x | x | x | x | x |
|
|
698
|
+
| `equipment_fund` | equipment-funds | x | x | x | x | x |
|
|
699
|
+
| `equipment_inspection` | equipment-inspections | x | x | | | |
|
|
700
|
+
| `equipment_inspection_result` | equipment-inspection-results | x | x | | x | x |
|
|
701
|
+
| `equipment_inspection_step` | equipment-inspection-steps | x | x | x | x | x |
|
|
702
|
+
| `equipment_inspection_step_result` | equipment-inspection-step-results | x | x | x | x | x |
|
|
703
|
+
| `equipment_kind` | equipment-kinds | x | x | x | x | x |
|
|
704
|
+
| `equipment_location` | equipment-locations | x | x | | | |
|
|
705
|
+
| `equipment_model` | equipment-models | x | x | x | x | x |
|
|
706
|
+
| `equipment_retired_reason` | equipment-retired-reasons | x | x | x | x | x |
|
|
707
|
+
| `equipment_supplier` | equipment-suppliers | x | x | x | x | x |
|
|
708
|
+
| `equipment_supplier_ref` | equipment-supplier-refs | x | x | x | x | x |
|
|
709
|
+
| `equipment_usage` | equipment-usages | x | x | x | x | x |
|
|
710
|
+
| `event` | events | x | x | x | x | |
|
|
711
|
+
| `exercise` | exercises | x | x | x | x | x |
|
|
712
|
+
| `handler_group` | handler-groups | x | x | x | x | x |
|
|
713
|
+
| `handler_group_membership` | handler-group-memberships | x | x | | | |
|
|
714
|
+
| `handler_qualification` | handler-qualifications | x | x | | | |
|
|
715
|
+
| `health_safety_category` | health-safety-categories | x | x | x | x | x |
|
|
716
|
+
| `health_safety_report` | health-safety-reports | x | x | | | |
|
|
717
|
+
| `health_safety_severity` | health-safety-severities | x | x | x | x | x |
|
|
718
|
+
| `incident` | incidents | x | x | x | x | |
|
|
719
|
+
| `incident_involved_injury` | incident-involved-injuries | x | x | | | |
|
|
720
|
+
| `incident_involved_metadata` | incident-involved-metadata | x | | | | |
|
|
721
|
+
| `incident_involved_person` | incident-involved-persons | x | x | | | |
|
|
722
|
+
| `location_bookmark` | location-bookmarks | x | x | | | |
|
|
723
|
+
| `member` | members | x | | | x | |
|
|
724
|
+
| `member_custom_status` | member-custom-statuses | x | | | | |
|
|
725
|
+
| `member_group` | member-groups | x | x | x | x | x |
|
|
726
|
+
| `member_group_membership` | member-group-memberships | x | x | | | |
|
|
727
|
+
| `member_qualification` | member-qualifications | x | x | | | |
|
|
728
|
+
| `member_qualification_award` | member-qualification-awards | x | | x | | |
|
|
729
|
+
| `member_retired_reason` | member-retired-reasons | x | | | | |
|
|
730
|
+
| `organisation` | organisations | | x | | | |
|
|
731
|
+
| `repair` | repairs | x | x | x | x | x |
|
|
732
|
+
| `resource_bundle` | resource-bundles | x | x | | | |
|
|
733
|
+
| `role` | roles | x | x | | | |
|
|
734
|
+
| `search` | search | x | | | | |
|
|
735
|
+
| `tag` | tags | x | x | x | x | x |
|
|
736
|
+
| `team` | teams | | x | | | |
|
|
737
|
+
| `whiteboard` | whiteboard | x | x | x | x | x |
|
|
738
|
+
| `whoami` | whoami | | x** | | | |
|
|
739
|
+
|
|
740
|
+
\* Document update uses PUT instead of PATCH.
|
|
741
|
+
\*\* Whoami `show` takes no arguments — it returns the current authenticated user.
|
|
742
|
+
|
|
743
|
+
All resources with `list` also support `list_all` for automatic pagination.
|
|
744
|
+
|
|
745
|
+
## Development
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
git clone https://github.com/rockymountainrescue/d4h_api.git
|
|
749
|
+
cd d4h_api
|
|
750
|
+
bin/setup
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
Run the full test suite:
|
|
754
|
+
|
|
755
|
+
```bash
|
|
756
|
+
bin/rake test
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
Run code quality checks (Reek + RuboCop):
|
|
760
|
+
|
|
761
|
+
```bash
|
|
762
|
+
bin/rake code_quality
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Run everything (quality + tests):
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
bin/rake
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Open an interactive console:
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
bin/console
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
## [License](LICENSE.md)
|
|
778
|
+
|
|
779
|
+
Hippocratic License 2.1
|
|
780
|
+
|
|
781
|
+
## [Versions](VERSIONS.md)
|
|
782
|
+
|
|
783
|
+
## Credits
|
|
784
|
+
|
|
785
|
+
Built by [Rocky Mountain Rescue Group](https://rockymountainrescue.org) and [Pawel Osiczko](https://github.com/posiczko).
|