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/d4h_api.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "d4h_api"
|
|
5
|
+
spec.version = "2.0.0"
|
|
6
|
+
spec.authors = ["Pawel Osiczko"]
|
|
7
|
+
spec.email = ["p.osiczko@tetrapyloctomy.org"]
|
|
8
|
+
spec.homepage = "https://github.com/rockymountainrescue/d4h_api"
|
|
9
|
+
spec.summary = "D4H API in Ruby"
|
|
10
|
+
spec.license = "Hippocratic-2.1"
|
|
11
|
+
spec.rdoc_options = ["--main", "README.md", "--markup", "tomdoc"]
|
|
12
|
+
|
|
13
|
+
spec.metadata = {"label" => "D4H", "rubygems_mfa_required" => "true"}
|
|
14
|
+
|
|
15
|
+
spec.signing_key = Gem.default_key_path
|
|
16
|
+
spec.cert_chain = [Gem.default_cert_path]
|
|
17
|
+
|
|
18
|
+
spec.required_ruby_version = ">= 4.0"
|
|
19
|
+
spec.add_dependency("ostruct", "~> 0.6")
|
|
20
|
+
spec.add_dependency("zeitwerk", "~> 2.6")
|
|
21
|
+
|
|
22
|
+
spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
|
|
23
|
+
spec.files = Dir["*.gemspec", "lib/**/*"]
|
|
24
|
+
|
|
25
|
+
spec.add_dependency("dotenv", "~> 3.0")
|
|
26
|
+
spec.add_dependency("faraday", "~> 2.13")
|
|
27
|
+
spec.add_dependency("faraday-retry", "~> 2.4")
|
|
28
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/middleware"
|
|
5
|
+
require "faraday/retry"
|
|
6
|
+
|
|
7
|
+
module D4H
|
|
8
|
+
module API
|
|
9
|
+
# Public: HTTP client for the D4H Developer API v3.
|
|
10
|
+
#
|
|
11
|
+
# Wraps a Faraday connection with Bearer token authentication and
|
|
12
|
+
# provides accessor methods for every supported API resource.
|
|
13
|
+
#
|
|
14
|
+
# The client builds URL paths as `v3/{context}/{context_id}/{resource}`,
|
|
15
|
+
# where context is typically "team" but can be "organisation" for
|
|
16
|
+
# organisation-scoped endpoints.
|
|
17
|
+
#
|
|
18
|
+
# Examples
|
|
19
|
+
#
|
|
20
|
+
# # Team context (default)
|
|
21
|
+
# client = D4H::API::Client.new(
|
|
22
|
+
# api_key: ENV.fetch("D4H_TOKEN"),
|
|
23
|
+
# context_id: 42,
|
|
24
|
+
# )
|
|
25
|
+
# client.member.list
|
|
26
|
+
# client.event.show(id: 1)
|
|
27
|
+
#
|
|
28
|
+
# # Organisation context
|
|
29
|
+
# client = D4H::API::Client.new(
|
|
30
|
+
# api_key: ENV.fetch("D4H_TOKEN"),
|
|
31
|
+
# context: "organisation",
|
|
32
|
+
# context_id: 99,
|
|
33
|
+
# )
|
|
34
|
+
class Client
|
|
35
|
+
# Internal: Default base URL for the D4H API. Override via D4H_BASE_URL
|
|
36
|
+
# env var or the base_url: constructor parameter.
|
|
37
|
+
DEFAULT_BASE_URL = "https://api.team-manager.us.d4h.com"
|
|
38
|
+
|
|
39
|
+
# Public: Returns the base URL, API token, Faraday adapter, context type,
|
|
40
|
+
# context ID, max retries, and retry interval.
|
|
41
|
+
attr_reader :base_url, :api_key, :adapter, :context, :context_id, :max_retries, :retry_interval
|
|
42
|
+
|
|
43
|
+
# Public: Initialize a new D4H API client.
|
|
44
|
+
#
|
|
45
|
+
# api_key: - A String Bearer token for API authentication.
|
|
46
|
+
# context: - The context scope, either "team" (default) or "organisation".
|
|
47
|
+
# context_id: - The Integer ID of the team or organisation.
|
|
48
|
+
# base_url: - The base URL for the D4H API. Defaults to D4H_BASE_URL env
|
|
49
|
+
# var, or "https://api.team-manager.us.d4h.com" if unset.
|
|
50
|
+
# Change this for EU or other regional endpoints.
|
|
51
|
+
# adapter: - The Faraday adapter to use (default: Faraday.default_adapter).
|
|
52
|
+
# Pass `[:test, stubs]` for testing with Faraday::Adapter::Test.
|
|
53
|
+
# max_retries: - Integer number of retries for transient errors (default: 3).
|
|
54
|
+
# Set to 0 to disable retries.
|
|
55
|
+
# retry_interval: - Float base interval in seconds between retries (default: 1).
|
|
56
|
+
# Set to 0 in tests to avoid sleeping.
|
|
57
|
+
def initialize(api_key:, context: "team", context_id:,
|
|
58
|
+
base_url: ENV.fetch("D4H_BASE_URL", DEFAULT_BASE_URL),
|
|
59
|
+
adapter: Faraday.default_adapter,
|
|
60
|
+
max_retries: MAX_RETRIES, retry_interval: RETRY_INTERVAL)
|
|
61
|
+
@api_key = api_key
|
|
62
|
+
@base_url = base_url
|
|
63
|
+
@context = context
|
|
64
|
+
@context_id = context_id
|
|
65
|
+
@adapter = adapter
|
|
66
|
+
@max_retries = max_retries
|
|
67
|
+
@retry_interval = retry_interval
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Public: Returns the versioned, context-scoped path prefix.
|
|
71
|
+
#
|
|
72
|
+
# Examples
|
|
73
|
+
#
|
|
74
|
+
# client.base_path # => "v3/team/42"
|
|
75
|
+
def base_path
|
|
76
|
+
"v3/#{context}/#{context_id}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Internal: Maximum number of retries for transient errors (429, 5xx).
|
|
80
|
+
MAX_RETRIES = 3
|
|
81
|
+
|
|
82
|
+
# Internal: Base interval in seconds for exponential backoff.
|
|
83
|
+
RETRY_INTERVAL = 1
|
|
84
|
+
|
|
85
|
+
# Internal: Maximum backoff interval in seconds.
|
|
86
|
+
MAX_RETRY_INTERVAL = 30
|
|
87
|
+
|
|
88
|
+
# Internal: Backoff factor — each retry doubles the wait time.
|
|
89
|
+
RETRY_BACKOFF_FACTOR = 2
|
|
90
|
+
|
|
91
|
+
# Internal: HTTP status codes that are transient and safe to retry.
|
|
92
|
+
RETRIABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
93
|
+
|
|
94
|
+
# Internal: HTTP methods to retry. All methods are retriable for
|
|
95
|
+
# transient server errors since the request may not have been processed.
|
|
96
|
+
RETRIABLE_METHODS = %i[delete get head options patch post put].freeze
|
|
97
|
+
|
|
98
|
+
# Public: Returns the memoized Faraday connection.
|
|
99
|
+
#
|
|
100
|
+
# Configured with URL-encoded request encoding, JSON response parsing,
|
|
101
|
+
# exponential backoff retry on transient errors, and the chosen adapter.
|
|
102
|
+
#
|
|
103
|
+
# The retry middleware retries on 429 and 5xx status codes up to
|
|
104
|
+
# max_retries times with exponential backoff (1s, 2s, 4s) capped at
|
|
105
|
+
# 30s. It also respects the D4H API's ratelimit headers for wait times.
|
|
106
|
+
# Set max_retries: 0 to disable retries.
|
|
107
|
+
def connection
|
|
108
|
+
@connection ||= Faraday.new do |f|
|
|
109
|
+
f.url_prefix = base_url
|
|
110
|
+
f.request(:url_encoded)
|
|
111
|
+
if max_retries > 0
|
|
112
|
+
f.request(:retry,
|
|
113
|
+
max: max_retries,
|
|
114
|
+
interval: retry_interval,
|
|
115
|
+
max_interval: MAX_RETRY_INTERVAL,
|
|
116
|
+
backoff_factor: RETRY_BACKOFF_FACTOR,
|
|
117
|
+
retry_statuses: RETRIABLE_STATUSES,
|
|
118
|
+
methods: RETRIABLE_METHODS,
|
|
119
|
+
retry_block: ->(env:, options:, retry_count:, exception:, will_retry_in:) {
|
|
120
|
+
Kernel.warn("[D4H] Retry #{retry_count + 1}/#{options.max} for #{env[:method].upcase} " \
|
|
121
|
+
"#{env[:url]} (#{exception.class}) in #{will_retry_in}s")
|
|
122
|
+
})
|
|
123
|
+
end
|
|
124
|
+
f.response(:json, content_type: "application/json")
|
|
125
|
+
f.adapter(*Array(adapter))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Public: Returns a short string representation that hides the API key.
|
|
130
|
+
def inspect
|
|
131
|
+
"#<D4H::Client>"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# -- Resource accessors --
|
|
135
|
+
#
|
|
136
|
+
# Each method returns a new Resource instance bound to this client.
|
|
137
|
+
# Resources are organized by domain below.
|
|
138
|
+
|
|
139
|
+
# Animals
|
|
140
|
+
def animal = AnimalResource.new(self)
|
|
141
|
+
def animal_group = AnimalGroupResource.new(self)
|
|
142
|
+
def animal_group_membership = AnimalGroupMembershipResource.new(self)
|
|
143
|
+
def animal_qualification = AnimalQualificationResource.new(self)
|
|
144
|
+
|
|
145
|
+
# Attendance
|
|
146
|
+
def attendance = AttendanceResource.new(self)
|
|
147
|
+
|
|
148
|
+
# Custom Fields
|
|
149
|
+
def custom_field = CustomFieldResource.new(self)
|
|
150
|
+
def custom_field_for_entity = CustomFieldForEntityResource.new(self)
|
|
151
|
+
|
|
152
|
+
# Documents
|
|
153
|
+
def document = DocumentResource.new(self)
|
|
154
|
+
|
|
155
|
+
# Equipment
|
|
156
|
+
def equipment = EquipmentResource.new(self)
|
|
157
|
+
def equipment_brand = EquipmentBrandResource.new(self)
|
|
158
|
+
def equipment_category = EquipmentCategoryResource.new(self)
|
|
159
|
+
def equipment_fund = EquipmentFundResource.new(self)
|
|
160
|
+
def equipment_inspection = EquipmentInspectionResource.new(self)
|
|
161
|
+
def equipment_inspection_result = EquipmentInspectionResultResource.new(self)
|
|
162
|
+
def equipment_inspection_step = EquipmentInspectionStepResource.new(self)
|
|
163
|
+
def equipment_inspection_step_result = EquipmentInspectionStepResultResource.new(self)
|
|
164
|
+
def equipment_kind = EquipmentKindResource.new(self)
|
|
165
|
+
def equipment_location = EquipmentLocationResource.new(self)
|
|
166
|
+
def equipment_model = EquipmentModelResource.new(self)
|
|
167
|
+
def equipment_retired_reason = EquipmentRetiredReasonResource.new(self)
|
|
168
|
+
def equipment_supplier = EquipmentSupplierResource.new(self)
|
|
169
|
+
def equipment_supplier_ref = EquipmentSupplierRefResource.new(self)
|
|
170
|
+
def equipment_usage = EquipmentUsageResource.new(self)
|
|
171
|
+
|
|
172
|
+
# Events & Incidents
|
|
173
|
+
def event = EventResource.new(self)
|
|
174
|
+
def exercise = ExerciseResource.new(self)
|
|
175
|
+
def incident = IncidentResource.new(self)
|
|
176
|
+
def incident_involved_injury = IncidentInvolvedInjuryResource.new(self)
|
|
177
|
+
def incident_involved_metadata = IncidentInvolvedMetadataResource.new(self)
|
|
178
|
+
def incident_involved_person = IncidentInvolvedPersonResource.new(self)
|
|
179
|
+
|
|
180
|
+
# Handlers
|
|
181
|
+
def handler_group = HandlerGroupResource.new(self)
|
|
182
|
+
def handler_group_membership = HandlerGroupMembershipResource.new(self)
|
|
183
|
+
def handler_qualification = HandlerQualificationResource.new(self)
|
|
184
|
+
|
|
185
|
+
# Health & Safety
|
|
186
|
+
def health_safety_category = HealthSafetyCategoryResource.new(self)
|
|
187
|
+
def health_safety_report = HealthSafetyReportResource.new(self)
|
|
188
|
+
def health_safety_severity = HealthSafetySeverityResource.new(self)
|
|
189
|
+
|
|
190
|
+
# Members
|
|
191
|
+
def member = MemberResource.new(self)
|
|
192
|
+
def member_custom_status = MemberCustomStatusResource.new(self)
|
|
193
|
+
def member_group = MemberGroupResource.new(self)
|
|
194
|
+
def member_group_membership = MemberGroupMembershipResource.new(self)
|
|
195
|
+
def member_qualification = MemberQualificationResource.new(self)
|
|
196
|
+
def member_qualification_award = MemberQualificationAwardResource.new(self)
|
|
197
|
+
def member_retired_reason = MemberRetiredReasonResource.new(self)
|
|
198
|
+
|
|
199
|
+
# Operations & Organization
|
|
200
|
+
def customer_identifier = CustomerIdentifierResource.new(self)
|
|
201
|
+
def d4h_module = D4hModuleResource.new(self)
|
|
202
|
+
def d4h_task = D4hTaskResource.new(self)
|
|
203
|
+
def duty = DutyResource.new(self)
|
|
204
|
+
def location_bookmark = LocationBookmarkResource.new(self)
|
|
205
|
+
def organisation = OrganisationResource.new(self)
|
|
206
|
+
def repair = RepairResource.new(self)
|
|
207
|
+
def resource_bundle = ResourceBundleResource.new(self)
|
|
208
|
+
def role = RoleResource.new(self)
|
|
209
|
+
def search = SearchResultResource.new(self)
|
|
210
|
+
def tag = TagResource.new(self)
|
|
211
|
+
def team = TeamResource.new(self)
|
|
212
|
+
def whiteboard = WhiteboardResource.new(self)
|
|
213
|
+
def whoami = WhoamiResource.new(self)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module D4H
|
|
4
|
+
module API
|
|
5
|
+
# Public: An Enumerable wrapper around a paginated D4H API list response.
|
|
6
|
+
#
|
|
7
|
+
# Parses the standard D4H v3 list envelope — which contains "results",
|
|
8
|
+
# "page", "pageSize", and "totalSize" — and converts each result into
|
|
9
|
+
# the given model class.
|
|
10
|
+
#
|
|
11
|
+
# Includes Enumerable, so you can call `map`, `select`, `first`, `count`,
|
|
12
|
+
# and any other Enumerable method directly on a collection.
|
|
13
|
+
#
|
|
14
|
+
# Examples
|
|
15
|
+
#
|
|
16
|
+
# collection = client.member.list
|
|
17
|
+
# collection.total_size # => 90
|
|
18
|
+
# collection.page # => 0
|
|
19
|
+
# collection.map(&:name) # => ["Alice", "Bob", ...]
|
|
20
|
+
# collection.first.status # => "OPERATIONAL"
|
|
21
|
+
# collection.select { |m| m.status == "OPERATIONAL" }
|
|
22
|
+
#
|
|
23
|
+
# # Raw JSON envelope
|
|
24
|
+
# collection.to_json # => {"results" => [...], "page" => 0, ...}
|
|
25
|
+
class Collection
|
|
26
|
+
# Public: Returns the Array of Model instances from this page.
|
|
27
|
+
# Public: Returns the zero-based page number.
|
|
28
|
+
# Public: Returns the page size (number of results per page).
|
|
29
|
+
# Public: Returns the total number of results across all pages.
|
|
30
|
+
# Public: Returns the original JSON hash envelope.
|
|
31
|
+
attr_reader :results, :page, :page_size, :total_size, :to_json
|
|
32
|
+
|
|
33
|
+
# Public: Initialize a Collection from a parsed JSON response body.
|
|
34
|
+
#
|
|
35
|
+
# body - A Hash with "results", "page", "pageSize", "totalSize" keys.
|
|
36
|
+
# model_class - The Model subclass to wrap each result in (e.g. Member, Event).
|
|
37
|
+
def initialize(body, model_class:)
|
|
38
|
+
@to_json = body
|
|
39
|
+
@results = (body["results"] || []).map { |attrs| model_class.new(attrs) }
|
|
40
|
+
@page = body["page"]
|
|
41
|
+
@page_size = body["pageSize"]
|
|
42
|
+
@total_size = body["totalSize"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
include Enumerable
|
|
46
|
+
|
|
47
|
+
# Public: Yield each Model in the results array.
|
|
48
|
+
#
|
|
49
|
+
# Yields each Model instance.
|
|
50
|
+
def each(&block)
|
|
51
|
+
results.each(&block)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module D4H
|
|
4
|
+
module API
|
|
5
|
+
# Public: Raised when the D4H API returns a non-2xx HTTP response.
|
|
6
|
+
#
|
|
7
|
+
# The message is built from the response body's "error" and "message"
|
|
8
|
+
# fields, joined with a colon.
|
|
9
|
+
#
|
|
10
|
+
# Examples
|
|
11
|
+
#
|
|
12
|
+
# begin
|
|
13
|
+
# client.equipment.show(id: 999_999)
|
|
14
|
+
# rescue D4H::API::Error => e
|
|
15
|
+
# e.message # => "Not Found: Equipment not found"
|
|
16
|
+
# end
|
|
17
|
+
class Error < StandardError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Public: Raised for transient HTTP errors that are safe to retry.
|
|
21
|
+
#
|
|
22
|
+
# Triggered by 429 (rate limited) and 5xx (server error) responses.
|
|
23
|
+
# The Faraday retry middleware catches this exception and retries
|
|
24
|
+
# with exponential backoff.
|
|
25
|
+
#
|
|
26
|
+
# If all retries are exhausted the exception propagates to the caller,
|
|
27
|
+
# so it can still be rescued like any other D4H::API::Error.
|
|
28
|
+
class RetriableError < Error
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module D4H
|
|
6
|
+
module API
|
|
7
|
+
# Public: Base class for all D4H API response models.
|
|
8
|
+
#
|
|
9
|
+
# Wraps a JSON response hash in an OpenStruct so that attributes are
|
|
10
|
+
# accessible via dot-notation. Nested hashes and arrays are recursively
|
|
11
|
+
# converted, allowing deep attribute traversal.
|
|
12
|
+
#
|
|
13
|
+
# Subclasses (e.g. Member, Event, Equipment) are thin wrappers that
|
|
14
|
+
# exist primarily for type identification via `kind_of?`.
|
|
15
|
+
#
|
|
16
|
+
# Examples
|
|
17
|
+
#
|
|
18
|
+
# model = D4H::API::Model.new({"id" => 1, "name" => "Alice"})
|
|
19
|
+
# model.id # => 1
|
|
20
|
+
# model.name # => "Alice"
|
|
21
|
+
#
|
|
22
|
+
# # Nested data
|
|
23
|
+
# model = D4H::API::Model.new({"brand" => {"title" => "Petzl"}})
|
|
24
|
+
# model.brand.title # => "Petzl"
|
|
25
|
+
#
|
|
26
|
+
# # Raw JSON hash
|
|
27
|
+
# model.to_json # => {"id" => 1, "name" => "Alice"}
|
|
28
|
+
class Model < OpenStruct
|
|
29
|
+
# Public: Returns the original JSON hash that was used to build this model.
|
|
30
|
+
attr_reader :to_json
|
|
31
|
+
|
|
32
|
+
# Public: Initialize a Model from a JSON response hash.
|
|
33
|
+
#
|
|
34
|
+
# attributes - A Hash of key/value pairs from the API response.
|
|
35
|
+
def initialize(attributes)
|
|
36
|
+
super(to_ostruct(attributes))
|
|
37
|
+
@to_json = attributes
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Internal: Recursively convert a parsed JSON object into OpenStructs.
|
|
41
|
+
#
|
|
42
|
+
# obj - A Hash, Array, or scalar value from the parsed JSON.
|
|
43
|
+
#
|
|
44
|
+
# Returns an OpenStruct (for Hash), Array of converted values, or the
|
|
45
|
+
# original scalar.
|
|
46
|
+
def to_ostruct(obj)
|
|
47
|
+
if obj.is_a?(Hash)
|
|
48
|
+
OpenStruct.new(obj.map { |key, val| [key, to_ostruct(val)] }.to_h)
|
|
49
|
+
elsif obj.is_a?(Array)
|
|
50
|
+
obj.map { |o| to_ostruct(o) }
|
|
51
|
+
else
|
|
52
|
+
obj
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|