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/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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class Animal < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class AnimalGroup < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class AnimalGroupMembership < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class AnimalQualification < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class Attendance < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class CustomField < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class CustomFieldForEntity < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class CustomerIdentifier < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class D4hModule < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class D4hTask < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class Document < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class Duty < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class Equipment < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentBrand < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentCategory < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentFund < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentInspection < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentInspectionResult < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentInspectionStep < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentInspectionStepResult < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentKind < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentLocation < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentModel < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentRetiredReason < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentSupplier < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentSupplierRef < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class EquipmentUsage < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class Event < Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module D4H
4
+ module API
5
+ class Exercise < Model
6
+ end
7
+ end
8
+ end