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.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +136 -0
  4. data/README.md +785 -0
  5. data/d4h_api.gemspec +28 -0
  6. data/lib/d4h/api/client.rb +216 -0
  7. data/lib/d4h/api/collection.rb +55 -0
  8. data/lib/d4h/api/error.rb +31 -0
  9. data/lib/d4h/api/model.rb +57 -0
  10. data/lib/d4h/api/models/animal.rb +8 -0
  11. data/lib/d4h/api/models/animal_group.rb +8 -0
  12. data/lib/d4h/api/models/animal_group_membership.rb +8 -0
  13. data/lib/d4h/api/models/animal_qualification.rb +8 -0
  14. data/lib/d4h/api/models/attendance.rb +8 -0
  15. data/lib/d4h/api/models/custom_field.rb +8 -0
  16. data/lib/d4h/api/models/custom_field_for_entity.rb +8 -0
  17. data/lib/d4h/api/models/customer_identifier.rb +8 -0
  18. data/lib/d4h/api/models/d4h_module.rb +8 -0
  19. data/lib/d4h/api/models/d4h_task.rb +8 -0
  20. data/lib/d4h/api/models/document.rb +8 -0
  21. data/lib/d4h/api/models/duty.rb +8 -0
  22. data/lib/d4h/api/models/equipment.rb +8 -0
  23. data/lib/d4h/api/models/equipment_brand.rb +8 -0
  24. data/lib/d4h/api/models/equipment_category.rb +8 -0
  25. data/lib/d4h/api/models/equipment_fund.rb +8 -0
  26. data/lib/d4h/api/models/equipment_inspection.rb +8 -0
  27. data/lib/d4h/api/models/equipment_inspection_result.rb +8 -0
  28. data/lib/d4h/api/models/equipment_inspection_step.rb +8 -0
  29. data/lib/d4h/api/models/equipment_inspection_step_result.rb +8 -0
  30. data/lib/d4h/api/models/equipment_kind.rb +8 -0
  31. data/lib/d4h/api/models/equipment_location.rb +8 -0
  32. data/lib/d4h/api/models/equipment_model.rb +8 -0
  33. data/lib/d4h/api/models/equipment_retired_reason.rb +8 -0
  34. data/lib/d4h/api/models/equipment_supplier.rb +8 -0
  35. data/lib/d4h/api/models/equipment_supplier_ref.rb +8 -0
  36. data/lib/d4h/api/models/equipment_usage.rb +8 -0
  37. data/lib/d4h/api/models/event.rb +8 -0
  38. data/lib/d4h/api/models/exercise.rb +8 -0
  39. data/lib/d4h/api/models/handler_group.rb +8 -0
  40. data/lib/d4h/api/models/handler_group_membership.rb +8 -0
  41. data/lib/d4h/api/models/handler_qualification.rb +8 -0
  42. data/lib/d4h/api/models/health_safety_category.rb +8 -0
  43. data/lib/d4h/api/models/health_safety_report.rb +8 -0
  44. data/lib/d4h/api/models/health_safety_severity.rb +8 -0
  45. data/lib/d4h/api/models/incident.rb +8 -0
  46. data/lib/d4h/api/models/incident_involved_injury.rb +8 -0
  47. data/lib/d4h/api/models/incident_involved_metadata.rb +8 -0
  48. data/lib/d4h/api/models/incident_involved_person.rb +8 -0
  49. data/lib/d4h/api/models/location_bookmark.rb +8 -0
  50. data/lib/d4h/api/models/member.rb +8 -0
  51. data/lib/d4h/api/models/member_custom_status.rb +8 -0
  52. data/lib/d4h/api/models/member_group.rb +8 -0
  53. data/lib/d4h/api/models/member_group_membership.rb +8 -0
  54. data/lib/d4h/api/models/member_qualification.rb +8 -0
  55. data/lib/d4h/api/models/member_qualification_award.rb +8 -0
  56. data/lib/d4h/api/models/member_retired_reason.rb +8 -0
  57. data/lib/d4h/api/models/organisation.rb +8 -0
  58. data/lib/d4h/api/models/repair.rb +8 -0
  59. data/lib/d4h/api/models/resource_bundle.rb +8 -0
  60. data/lib/d4h/api/models/role.rb +8 -0
  61. data/lib/d4h/api/models/search_result.rb +8 -0
  62. data/lib/d4h/api/models/tag.rb +8 -0
  63. data/lib/d4h/api/models/team.rb +8 -0
  64. data/lib/d4h/api/models/whiteboard.rb +8 -0
  65. data/lib/d4h/api/models/whoami.rb +8 -0
  66. data/lib/d4h/api/resource.rb +171 -0
  67. data/lib/d4h/api/resources/animal_group_membership_resource.rb +21 -0
  68. data/lib/d4h/api/resources/animal_group_resource.rb +33 -0
  69. data/lib/d4h/api/resources/animal_qualification_resource.rb +21 -0
  70. data/lib/d4h/api/resources/animal_resource.rb +21 -0
  71. data/lib/d4h/api/resources/attendance_resource.rb +25 -0
  72. data/lib/d4h/api/resources/custom_field_for_entity_resource.rb +17 -0
  73. data/lib/d4h/api/resources/custom_field_resource.rb +33 -0
  74. data/lib/d4h/api/resources/customer_identifier_resource.rb +17 -0
  75. data/lib/d4h/api/resources/d4h_module_resource.rb +17 -0
  76. data/lib/d4h/api/resources/d4h_task_resource.rb +17 -0
  77. data/lib/d4h/api/resources/document_resource.rb +33 -0
  78. data/lib/d4h/api/resources/duty_resource.rb +21 -0
  79. data/lib/d4h/api/resources/equipment_brand_resource.rb +33 -0
  80. data/lib/d4h/api/resources/equipment_category_resource.rb +33 -0
  81. data/lib/d4h/api/resources/equipment_fund_resource.rb +33 -0
  82. data/lib/d4h/api/resources/equipment_inspection_resource.rb +21 -0
  83. data/lib/d4h/api/resources/equipment_inspection_result_resource.rb +29 -0
  84. data/lib/d4h/api/resources/equipment_inspection_step_resource.rb +33 -0
  85. data/lib/d4h/api/resources/equipment_inspection_step_result_resource.rb +33 -0
  86. data/lib/d4h/api/resources/equipment_kind_resource.rb +33 -0
  87. data/lib/d4h/api/resources/equipment_location_resource.rb +21 -0
  88. data/lib/d4h/api/resources/equipment_model_resource.rb +33 -0
  89. data/lib/d4h/api/resources/equipment_resource.rb +33 -0
  90. data/lib/d4h/api/resources/equipment_retired_reason_resource.rb +33 -0
  91. data/lib/d4h/api/resources/equipment_supplier_ref_resource.rb +33 -0
  92. data/lib/d4h/api/resources/equipment_supplier_resource.rb +33 -0
  93. data/lib/d4h/api/resources/equipment_usage_resource.rb +33 -0
  94. data/lib/d4h/api/resources/event_resource.rb +29 -0
  95. data/lib/d4h/api/resources/exercise_resource.rb +33 -0
  96. data/lib/d4h/api/resources/handler_group_membership_resource.rb +21 -0
  97. data/lib/d4h/api/resources/handler_group_resource.rb +33 -0
  98. data/lib/d4h/api/resources/handler_qualification_resource.rb +21 -0
  99. data/lib/d4h/api/resources/health_safety_category_resource.rb +33 -0
  100. data/lib/d4h/api/resources/health_safety_report_resource.rb +21 -0
  101. data/lib/d4h/api/resources/health_safety_severity_resource.rb +33 -0
  102. data/lib/d4h/api/resources/incident_involved_injury_resource.rb +21 -0
  103. data/lib/d4h/api/resources/incident_involved_metadata_resource.rb +17 -0
  104. data/lib/d4h/api/resources/incident_involved_person_resource.rb +21 -0
  105. data/lib/d4h/api/resources/incident_resource.rb +29 -0
  106. data/lib/d4h/api/resources/location_bookmark_resource.rb +21 -0
  107. data/lib/d4h/api/resources/member_custom_status_resource.rb +17 -0
  108. data/lib/d4h/api/resources/member_group_membership_resource.rb +21 -0
  109. data/lib/d4h/api/resources/member_group_resource.rb +33 -0
  110. data/lib/d4h/api/resources/member_qualification_award_resource.rb +21 -0
  111. data/lib/d4h/api/resources/member_qualification_resource.rb +21 -0
  112. data/lib/d4h/api/resources/member_resource.rb +21 -0
  113. data/lib/d4h/api/resources/member_retired_reason_resource.rb +17 -0
  114. data/lib/d4h/api/resources/organisation_resource.rb +13 -0
  115. data/lib/d4h/api/resources/repair_resource.rb +33 -0
  116. data/lib/d4h/api/resources/resource_bundle_resource.rb +21 -0
  117. data/lib/d4h/api/resources/role_resource.rb +21 -0
  118. data/lib/d4h/api/resources/search_result_resource.rb +17 -0
  119. data/lib/d4h/api/resources/tag_resource.rb +33 -0
  120. data/lib/d4h/api/resources/team_resource.rb +13 -0
  121. data/lib/d4h/api/resources/whiteboard_resource.rb +33 -0
  122. data/lib/d4h/api/resources/whoami_resource.rb +13 -0
  123. data/lib/d4h.rb +156 -0
  124. data.tar.gz.sig +0 -0
  125. metadata +264 -0
  126. 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).