toc_doc 1.0.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3637aae8586b1ea3b5b568a4ef64a295b3e3664ada7d252163e8176ce429aa37
4
- data.tar.gz: da283d2f5efd9c37b7f6b20d6bc207dcb79f826850b8d7d0953157473f36345f
3
+ metadata.gz: dcac91743cd8f8d50d032638c3a690f88c786a369960d8b5df8bdead6006149c
4
+ data.tar.gz: d8df90b16b2ff3e2311af4751396ff1ae54173575e4507989df0e7c910bd82eb
5
5
  SHA512:
6
- metadata.gz: 79a3d614929d14b5709dec397ab39a78c07db2db0e31da2e2de7423af8da4a1af5b5dbc77f3943da8b104ab25b85531328e739697384fb7a8a53451320bf9951
7
- data.tar.gz: 962d1c35bb6ede7899491b93e0f00c868a3aa87ffe4a32b076576b0017d96ff09e3214eb37000bee0ba40f43269597c75d21daf18c77e92787b39d61b29b4696
6
+ metadata.gz: e8886203725b1dd9bbd59e62ee56aaf4f0965c908b31d478568c9d0149a7062f7059509f5692f18273e08b4a05d90d8a6d6e8bb02e89342ab2d1a18e92b2b16a
7
+ data.tar.gz: fd9271676246e8da0e7e626ebc3c10316f5b86b2ab6fc811a67986bb276aa1e817c74f7d17fec64372cfb15048638b1aac32e08b2dce981142646ed4a1289977
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.0] - 2026-03-08
4
+
5
+ ### Added
6
+
7
+ - **`TocDoc::Availability::Collection`** — new `Enumerable` collection class returned by `TocDoc::Availability.where`; provides `#total`, `#next_slot`, `#each`, `#raw_availabilities`, `#to_h`, and `#merge_page!`
8
+ - **`TocDoc::Availability.where`** — class-level query method replacing `Client#availabilities`; automatically follows a `next_slot` response key with a second request before returning the collection
9
+ - **`TocDoc.availabilities`** — top-level shortcut delegating to `TocDoc::Availability.where`
10
+ - **Dependabot** — automated Bundler dependency updates via `.github/dependabot.yml`
11
+
12
+ ### Changed
13
+
14
+ - **Connection** — `#get` and `#paginate` are now public on `TocDoc::Connection`, allowing model classes to call them directly via `TocDoc.client`
15
+ - **`TocDoc::UriUtils`** — updated module-level example to reflect actual usage (`TocDoc::Availability` with `extend`, not the removed `Client::Availabilities`)
16
+
17
+ ### Removed
18
+
19
+ - **`TocDoc::Client::Availabilities`** — endpoint module removed; availability querying now lives in `TocDoc::Availability.where` and `TocDoc::Availability::Collection`
20
+ - **`TocDoc::Response::Availability`** — response wrapper model removed; replaced by `TocDoc::Availability::Collection`
21
+ - **`auto_paginate`** — configuration key, default, and all related logic removed from `TocDoc::Configurable` and `TocDoc::Default`
22
+
23
+ ## [1.1.0] - 2026-03-06
24
+
25
+ ### Changed
26
+
27
+ - **`TocDoc::Availability`** — `#date` now returns a parsed `Date` object instead of a raw string; `#slots` now returns an array of `DateTime` objects instead of strings
28
+ - **`TocDoc::Response::Availability#next_slot`** — falls back to inferring the next slot from the first available slot when the API response omits the `next_slot` key
29
+
30
+ ### Removed
31
+
32
+ - **VCR** — removed VCR dependency from the test suite; HTTP interactions are now stubbed directly with WebMock
33
+
3
34
  ## [1.0.0] - 2026-03-04
4
35
 
5
36
  ### Added
@@ -0,0 +1,31 @@
1
+ # Potential Endpoints
2
+
3
+ - Interesting JSONs [GET]
4
+ - [x] Availabilities
5
+ - https://www.doctolib.fr/availabilities.json?visit_motive_ids=7767829&agenda_ids=1101600&practice_ids=377272&telehealth=false&start_date=2026-03-06&limit=5
6
+ - Practitioner ?
7
+ - https://www.doctolib.fr/pharmacie/paris/pharmacie-faidherbe.json
8
+ - https://www.doctolib.fr/dentiste/bordeaux/mathilde-devun-lesparre-medoc.json
9
+ - https://www.doctolib.fr/a/b/mathilde-devun-lesparre-medoc.json
10
+ - https://www.doctolib.fr/profiles/mathilde-devun-lesparre-medoc.json
11
+ - Rassemblement practiciens / Place Practitioners collection
12
+ - https://www.doctolib.fr/profiles/pavillon-de-la-mutualite-bordeaux-rue-vital-carles.json
13
+ - Places
14
+ - https://www.doctolib.fr/patient_app/place_autocomplete.json?query=47300
15
+ - Booking context
16
+ - https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=brigitte-devun-pujols&locale=fr
17
+ - https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388&locale=fr
18
+ - Interesting NON JSONs
19
+ - City practitioners (❗️JSON-LD in a script tag of an HTML page - data-id="removable-json-ld")
20
+ - https://www.doctolib.fr/dentiste/bordeaux/
21
+ - https://www.doctolib.fr/medecin-generaliste/bordeaux/
22
+ - https://www.doctolib.fr/medecin-generaliste/villeneuve-sur-lot
23
+ - Non-interesting
24
+ - Legal links
25
+ - https://www.doctolib.fr/search/footer_legal_links.json
26
+ - FAQ
27
+ - https://www.doctolib.fr/search/footer_public_content.json?hub_search=false&display_faq=true&speciality_id=2&place_id=18733
28
+ - Social media links
29
+ - https://www.doctolib.fr/search/footer_social_media_links.json
30
+ - New Booking [POST]
31
+ - online_booking/draft/new.json
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # TocDoc
2
2
 
3
- A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-based client with modular resource endpoints, configurable defaults, optional auto-pagination, and a clean error hierarchy.
3
+ A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-based client with configurable defaults, model-driven resource querying, and a clean error hierarchy.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/toc_doc.svg)](https://badge.fury.io/rb/toc_doc)
6
+ [![CI](https://github.com/01max/toc_doc/actions/workflows/main.yml/badge.svg)](https://github.com/01max/toc_doc/actions)
7
+ [![Coverage Status](https://coveralls.io/repos/github/01max/toc_doc/badge.svg?branch=main)](https://coveralls.io/github/01max/toc_doc?branch=main)
8
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
9
+ [![YARD Docs](https://img.shields.io/badge/docs-YARD-blue.svg)](https://rubydoc.info/gems/toc_doc)
4
10
 
5
11
  > **Heads-up:** Doctolib™ does not publish a public API. This gem reverse-engineers
6
12
  > the endpoints used by the Doctolib™ website. Behaviour may change at any time
@@ -59,19 +65,18 @@ gem install toc_doc
59
65
  ```ruby
60
66
  require 'toc_doc'
61
67
 
62
- # Use the pre-configured module-level client …
63
- response = TocDoc.availabilities(
68
+ collection = TocDoc::Availability.where(
64
69
  visit_motive_ids: 7_767_829,
65
70
  agenda_ids: 1_101_600,
66
71
  practice_ids: 377_272,
67
72
  telehealth: false
68
73
  )
69
74
 
70
- response.total # => 5
71
- response.next_slot # => "2026-02-28T10:00:00.000+01:00"
75
+ collection.total # => 5
76
+ collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
72
77
 
73
- response.availabilities.each do |avail|
74
- puts "#{avail.date}: #{avail.slots.join(', ')}"
78
+ collection.each do |avail|
79
+ puts "#{avail.date}: #{avail.slots.map { |s| s.strftime('%H:%M') }.join(', ')}"
75
80
  end
76
81
  ```
77
82
 
@@ -89,7 +94,7 @@ TocDoc.configure do |config|
89
94
  config.per_page = 10
90
95
  end
91
96
 
92
- TocDoc.availabilities(visit_motive_ids: 123, agenda_ids: 456)
97
+ TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
93
98
  ```
94
99
 
95
100
  Calling `TocDoc.reset!` restores all options to their defaults.
@@ -97,14 +102,24 @@ Use `TocDoc.options` to inspect the current configuration hash.
97
102
 
98
103
  ### Per-client configuration
99
104
 
100
- Instantiate independent clients with different options:
105
+ Instantiate independent clients with different options and query via `TocDoc::Availability.where`:
101
106
 
102
107
  ```ruby
103
- de_client = TocDoc::Client.new(api_endpoint: 'https://www.doctolib.de')
104
- it_client = TocDoc::Client.new(api_endpoint: 'https://www.doctolib.it', per_page: 3)
108
+ # Germany
109
+ TocDoc.configure { |c| c.api_endpoint = 'https://www.doctolib.de'; c.per_page = 3 }
110
+ TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
111
+
112
+ # Reset and switch to Italy
113
+ TocDoc.reset!
114
+ TocDoc.configure { |c| c.api_endpoint = 'https://www.doctolib.it' }
115
+ TocDoc::Availability.where(visit_motive_ids: 789, agenda_ids: 101)
116
+ ```
105
117
 
106
- de_client.availabilities(visit_motive_ids: 123, agenda_ids: 456)
107
- it_client.availabilities(visit_motive_ids: 789, agenda_ids: 101)
118
+ Alternatively, use `TocDoc::Client` directly for lower-level access ().
119
+
120
+ ```ruby
121
+ client = TocDoc::Client.new(api_endpoint: 'https://www.doctolib.de', per_page: 5)
122
+ client.get('/availabilities.json', query: { visit_motive_ids: '123', agenda_ids: '456', start_date: Date.today.to_s, limit: 5 })
108
123
  ```
109
124
 
110
125
  ### All configuration options
@@ -112,10 +127,9 @@ it_client.availabilities(visit_motive_ids: 789, agenda_ids: 101)
112
127
  | Option | Default | Description |
113
128
  |---|---|---|
114
129
  | `api_endpoint` | `https://www.doctolib.fr` | Base URL. Change to `.de` / `.it` for other countries. |
115
- | `user_agent` | `TocDoc Ruby Gem 0.1.0` | `User-Agent` header sent with every request. |
130
+ | `user_agent` | `TocDoc Ruby Gem 1.2.0` | `User-Agent` header sent with every request. |
116
131
  | `default_media_type` | `application/json` | `Accept` and `Content-Type` headers. |
117
- | `per_page` | `5` | Default number of results returned per request, platform's max is currently at `15`. |
118
- | `auto_paginate` | `false` | When `true`, automatically fetches all pages and merges results. |
132
+ | `per_page` | `15` | Default number of availability dates per request (capped at `15`). |
119
133
  | `middleware` | Retry + RaiseError + JSON + adapter | Full Faraday middleware stack. Override to customise completely. |
120
134
  | `connection_options` | `{}` | Options passed directly to `Faraday.new`. |
121
135
 
@@ -129,7 +143,6 @@ All primary options can be set via environment variables before the gem is loade
129
143
  | `TOCDOC_USER_AGENT` | `user_agent` |
130
144
  | `TOCDOC_MEDIA_TYPE` | `default_media_type` |
131
145
  | `TOCDOC_PER_PAGE` | `per_page` |
132
- | `TOCDOC_AUTO_PAGINATE` | `auto_paginate` (`"true"` / anything else) |
133
146
  | `TOCDOC_RETRY_MAX` | Maximum Faraday retry attempts (default `3`) |
134
147
 
135
148
  ---
@@ -141,7 +154,7 @@ All primary options can be set via environment variables before the gem is loade
141
154
  Retrieve open appointment slots for a given visit motive and agenda.
142
155
 
143
156
  ```ruby
144
- client.availabilities(
157
+ TocDoc::Availability.where(
145
158
  visit_motive_ids: visit_motive_id, # Integer, String, or Array
146
159
  agenda_ids: agenda_id, # Integer, String, or Array
147
160
  start_date: Date.today, # Date or String (default: today)
@@ -152,18 +165,20 @@ client.availabilities(
152
165
  )
153
166
  ```
154
167
 
168
+ `TocDoc.availabilities(...)` is a module-level shortcut with the same signature.
169
+
155
170
  **Multiple IDs** are accepted as arrays; the gem serialises them with the
156
171
  dash-separated format Doctolib expects:
157
172
 
158
173
  ```ruby
159
- client.availabilities(
174
+ TocDoc::Availability.where(
160
175
  visit_motive_ids: [7_767_829, 7_767_830],
161
176
  agenda_ids: [1_101_600, 1_101_601]
162
177
  )
163
178
  # → GET /availabilities.json?visit_motive_ids=7767829-7767830&agenda_ids=1101600-1101601&…
164
179
  ```
165
180
 
166
- **Return value:** a `TocDoc::Response::Availability` (see [Response objects](#response-objects)).
181
+ **Return value:** a `TocDoc::Availability::Collection` (see [Response objects](#response-objects)).
167
182
 
168
183
  ---
169
184
 
@@ -172,39 +187,41 @@ client.availabilities(
172
187
  All API responses are wrapped in lightweight Ruby objects that provide
173
188
  dot-notation access and a `#to_h` round-trip helper.
174
189
 
175
- ### `TocDoc::Response::Availability`
190
+ ### `TocDoc::Availability::Collection`
176
191
 
177
- Returned by `#availabilities`.
192
+ Returned by `TocDoc::Availability.where`; also accessible via the `TocDoc.availabilities` module-level shortcut.
193
+ Implements `Enumerable`, yielding `TocDoc::Availability` instances that have at least one slot.
178
194
 
179
195
  | Method | Type | Description |
180
196
  |---|---|---|
181
197
  | `#total` | `Integer` | Total number of available slots across all dates. |
182
198
  | `#next_slot` | `String \| nil` | ISO 8601 datetime of the nearest available slot. `nil` when none remain. |
183
- | `#availabilities` | `Array<TocDoc::Availability>` | One entry per date. |
184
- | `#to_h` | `Hash` | Plain-hash representation including expanded availability entries. |
199
+ | `#each` | — | Yields each `TocDoc::Availability` that has at least one slot (excludes empty-slot dates). |
200
+ | `#raw_availabilities` | `Array<TocDoc::Availability>` | All date entries, including those with no slots. |
201
+ | `#to_h` | `Hash` | Plain-hash representation (only dates with slots in the `availabilities` key). |
185
202
 
186
203
  ### `TocDoc::Availability`
187
204
 
188
- Each element of `Response::Availability#availabilities`.
205
+ Represents a single availability date entry. Each element yielded by the collection.
189
206
 
190
207
  | Method | Type | Description |
191
208
  |---|---|---|
192
- | `#date` | `String` | Date in `YYYY-MM-DD` format. |
193
- | `#slots` | `Array<String>` | ISO 8601 datetimes for each bookable slot on that date. |
209
+ | `#date` | `Date` | Parsed date object. |
210
+ | `#slots` | `Array<DateTime>` | Parsed datetime objects for each bookable slot on that date. |
194
211
  | `#to_h` | `Hash` | Plain-hash representation. |
195
212
 
196
213
  **Example:**
197
214
 
198
215
  ```ruby
199
- response = TocDoc.availabilities(visit_motive_ids: 123, agenda_ids: 456)
216
+ collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
200
217
 
201
- response.total # => 5
202
- response.next_slot # => "2026-02-28T10:00:00.000+01:00"
218
+ collection.total # => 5
219
+ collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
203
220
 
204
- response.availabilities.first.date # => "2026-02-28"
205
- response.availabilities.first.slots # => ["2026-02-28T10:00:00.000+01:00", ...]
221
+ collection.first.date # => #<Date: 2026-02-28>
222
+ collection.first.slots # => [#<DateTime: 2026-02-28T10:00:00+01:00>, ...]
206
223
 
207
- response.to_h
224
+ collection.to_h
208
225
  # => {
209
226
  # "total" => 5,
210
227
  # "next_slot" => "2026-02-28T10:00:00.000+01:00",
@@ -216,35 +233,36 @@ response.to_h
216
233
 
217
234
  ## Pagination
218
235
 
219
- The Doctolib availability endpoint is paginated by `start_date` and `limit`.
220
- TocDoc can manage this automatically.
236
+ The Doctolib availability endpoint is window-based: each request returns up to
237
+ `limit` dates starting from `start_date`.
221
238
 
222
- ### Automatic pagination
239
+ ### Automatic next-slot resolution
223
240
 
224
- Set `auto_paginate: true` on the client (or at module level) to fetch all pages
225
- and have results merged into a single `Response::Availability` object:
241
+ `TocDoc::Availability.where` automatically follows `next_slot` once: if the
242
+ first API response contains a `next_slot` key (indicating no available slots in
243
+ the requested window), a second request is issued transparently from that date
244
+ before the collection is returned.
226
245
 
227
- ```ruby
228
- client = TocDoc::Client.new(auto_paginate: true, per_page: 5)
246
+ ### Manual window advancement
229
247
 
230
- all_slots = client.availabilities(
248
+ To fetch additional date windows, call `TocDoc::Availability.where` again with a
249
+ later `start_date`:
250
+
251
+ ```ruby
252
+ first_page = TocDoc::Availability.where(
231
253
  visit_motive_ids: 7_767_829,
232
254
  agenda_ids: 1_101_600,
233
255
  start_date: Date.today
234
256
  )
235
257
 
236
- all_slots.total # total across every page
237
- all_slots.availabilities # every date entry, concatenated
238
- ```
239
-
240
- Pagination stops automatically when the API returns `next_slot: null`.
241
-
242
- ### Module-level toggle
243
-
244
- ```ruby
245
- TocDoc.configure { |c| c.auto_paginate = true }
246
-
247
- TocDoc.availabilities(visit_motive_ids: 123, agenda_ids: 456)
258
+ if first_page.any?
259
+ next_start = first_page.raw_availabilities.last.date + 1
260
+ next_page = TocDoc::Availability.where(
261
+ visit_motive_ids: 7_767_829,
262
+ agenda_ids: 1_101_600,
263
+ start_date: next_start
264
+ )
265
+ end
248
266
  ```
249
267
 
250
268
  ---
@@ -256,7 +274,7 @@ so you can rescue the whole hierarchy with a single clause:
256
274
 
257
275
  ```ruby
258
276
  begin
259
- TocDoc.availabilities(visit_motive_ids: 0, agenda_ids: 0)
277
+ TocDoc::Availability.where(visit_motive_ids: 0, agenda_ids: 0)
260
278
  rescue TocDoc::Error => e
261
279
  puts "Doctolib error: #{e.message}"
262
280
  end
@@ -313,13 +331,14 @@ bundle exec rake install
313
331
 
314
332
  ### Adding new endpoints
315
333
 
316
- 1. Create `lib/toc_doc/client/<resource>.rb` and define a module
317
- `TocDoc::Client::<Resource>` with your endpoint methods.
318
- 2. Call `get`/`post`/`paginate` (from `TocDoc::Connection`) to issue requests.
319
- 3. Create `lib/toc_doc/models/response/<resource>.rb` (and any model classes)
320
- inheriting from `TocDoc::Resource`.
321
- 4. Include the new module in `TocDoc::Client` (`lib/toc_doc/client.rb`).
322
- 5. Add corresponding specs under `spec/toc_doc/client/`.
334
+ 1. Create `lib/toc_doc/models/<resource>.rb` with a model class inheriting from
335
+ `TocDoc::Resource`. Add a class-level `.where` (or equivalent) query method
336
+ that calls `TocDoc.client.get` / `.post` to issue requests.
337
+ 2. If the endpoint is paginated, create
338
+ `lib/toc_doc/models/<resource>/collection.rb` with an `Enumerable` collection
339
+ class (see `TocDoc::Availability::Collection` for the pattern).
340
+ 3. Require the new files from `lib/toc_doc/models.rb`.
341
+ 4. Add specs under `spec/toc_doc/models/`.
323
342
 
324
343
  ### Generating documentation
325
344
 
data/TODO.md CHANGED
@@ -1,77 +1,100 @@
1
- # 1.0
1
+ # PLAN
2
2
 
3
- ## 1 – Skeleton & Tooling
3
+ [POTENTIAL_ENDPOINTS][POTENTIAL_ENDPOINTS.md]
4
+
5
+ ## 1.3
6
+
7
+ - [ ] Search (autocomplete)
8
+ - [ ] search profile : https://www.doctolib.fr/api/searchbar/autocomplete.json?search=devun
9
+ - [ ] search specialty : https://www.doctolib.fr/api/searchbar/autocomplete.json?search=dentiste
10
+
11
+ ## 1.4
12
+
13
+ - [ ] Profile
14
+ - slug : https://www.doctolib.fr/profiles/mathilde-devun-lesparre-medoc.json
15
+ - id : https://www.doctolib.fr/profiles/926388.json
16
+
17
+ ## 1.5
18
+
19
+ - [ ] Booking context
20
+ - https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388
21
+
22
+ ## 1.6
23
+
24
+ ### Better API usage
25
+ - [ ] Rate limiting
26
+ - [ ] Caching
27
+ - [ ] Logging
28
+
29
+ ## 2.0
30
+
31
+ ### Auth / User-based actions
32
+ - [ ] Research auth scheme
33
+ - [ ] Authentication module + headers
34
+ - [ ] Auth specs
35
+
36
+ # DONE & RELEASED
37
+
38
+ ## 1.0
39
+
40
+ ### 1 – Skeleton & Tooling
4
41
  - [x] Scaffold gem & layout
5
42
  - [x] Gem spec metadata & deps
6
43
  - [x] Lib structure (default/config/client/etc.)
7
44
  - [x] CI workflow (RSpec + RuboCop)
8
45
  - [x] RSpec + WebMock + VCR setup
9
46
 
10
- ## 2 – Configuration
47
+ ### 2 – Configuration
11
48
  - [x] Default options & ENV fallbacks
12
49
  - [x] Configurable module (keys, reset, options)
13
50
  - [x] Top-level TocDoc wiring (client, setup, delegation)
14
51
  - [x] Config specs (module + client)
15
52
 
16
- ## 3 – Connection & HTTP
53
+ ### 3 – Connection & HTTP
17
54
  - [x] Connection module (agent, request helpers)
18
55
  - [x] ~Faraday middleware~
19
56
  - [x] URL building helpers
20
57
  - [x] Connection specs
21
58
 
22
- ## 4 – Error Handling
59
+ ### 4 – Error Handling
23
60
  - [x] Error base class & factory
24
61
  - [x] Error subclasses (4xx/5xx)
25
62
  - [x] RaiseError middleware
26
63
  - [x] Error mapping specs
27
64
 
28
- ## 5 – Client & Availabilities
65
+ ### 5 – Client & Availabilities
29
66
  - [x] Client includes config + connection
30
67
  - [x] Availabilities endpoint module
31
68
  - [x] TocDoc.availabilities delegation
32
69
  - [x] Availabilities specs (stubs/VCR)
33
70
 
34
- ## 6 – Response Objects
71
+ ### 6 – Response Objects
35
72
  - [x] Resource wrapper
36
73
  - [x] Availability objects
37
74
  - [x] Client mapping to response objects
38
75
  - [x] Response specs
39
76
 
40
- ## 8 – Pagination
77
+ ### 8 – Pagination
41
78
  - [x] Analyze pagination model
42
79
  - [x] Implement Connection#paginate
43
80
  - [x] Pagination config & specs
44
81
 
45
- ## 9 – Docs & Release
82
+ ### 9 – Docs & Release
46
83
  - [x] README
47
84
  - [x] YARD docs
48
85
  - [x] CHANGELOG
49
86
  - [x] FIX GH CI
50
- - [ ] Build & publish gem
51
- - [ ] on rubygem
52
- - [ ] gem.coop ?
53
-
54
- # 1.1
87
+ - [x] Build & publish gem
88
+ - [x] on rubygem
89
+ - [x] release on GH
90
+ - [x] gem.coop/@maxime
91
+ - [x] Add test coverage tool
55
92
 
56
- ## Parse raw API data
57
- - [ ] Parse date / datetime
58
- - [ ] ?
93
+ ## 1.1
59
94
 
60
- ## Better API usage
61
- - [ ] Rate limiting
62
- - [ ] Caching
63
- - [ ] Logging
64
- - [ ] Better multi-region support ?
65
- - [ ] Async support ?
95
+ ### Parse raw API data
96
+ - [x] Parse date / datetime
66
97
 
67
- ## Extra Endpoints
68
- - [ ] Identify additional endpoints
69
- - [ ] Implement resource modules
70
- - [ ] Specs per endpoint
98
+ ## 1.2
71
99
 
72
- # 1.2
73
-
74
- ## Auth / User-based actions
75
- - [ ] Research auth scheme
76
- - [ ] Authentication module + headers
77
- - [ ] Auth specs
100
+ - [x] Rework Availability's client, model and collection architecture.
@@ -3,7 +3,6 @@
3
3
  require 'toc_doc/core/configurable'
4
4
  require 'toc_doc/core/connection'
5
5
  require 'toc_doc/core/uri_utils'
6
- require 'toc_doc/client/availabilities'
7
6
 
8
7
  module TocDoc
9
8
  # The main entry-point for interacting with the Doctolib API.
@@ -16,18 +15,14 @@ module TocDoc
16
15
  # api_endpoint: 'https://www.doctolib.de',
17
16
  # per_page: 5
18
17
  # )
19
- # client.availabilities(visit_motive_ids: 123, agenda_ids: 456)
20
18
  #
21
19
  # @see TocDoc::Configurable
22
20
  # @see TocDoc::Connection
23
- # @see TocDoc::Client::Availabilities
24
21
  class Client
25
22
  include TocDoc::Configurable
26
23
  include TocDoc::Connection
27
24
  include TocDoc::UriUtils
28
25
 
29
- include TocDoc::Client::Availabilities
30
-
31
26
  # Creates a new client instance.
32
27
  #
33
28
  # Options are merged on top of the module-level {TocDoc::Default} values.
@@ -39,7 +34,6 @@ module TocDoc
39
34
  # @option options [String] :user_agent User-Agent header value
40
35
  # @option options [String] :default_media_type Accept / Content-Type header
41
36
  # @option options [Integer] :per_page Results per page
42
- # @option options [Boolean] :auto_paginate Follow pagination automatically
43
37
  # @option options [Faraday::RackBuilder] :middleware Custom Faraday middleware
44
38
  # @option options [Hash] :connection_options Additional Faraday options
45
39
  #
@@ -29,7 +29,6 @@ module TocDoc
29
29
  connection_options
30
30
  default_media_type
31
31
  per_page
32
- auto_paginate
33
32
  ].freeze
34
33
 
35
34
  # @!attribute [rw] api_endpoint
@@ -42,8 +41,6 @@ module TocDoc
42
41
  # @return [Hash] additional Faraday connection options
43
42
  # @!attribute [rw] default_media_type
44
43
  # @return [String] the Accept / Content-Type header value
45
- # @!attribute [rw] auto_paginate
46
- # @return [Boolean] whether to follow pagination automatically
47
44
  attr_accessor(*VALID_CONFIG_KEYS)
48
45
 
49
46
  # Set the number of results per page, clamped to
@@ -9,8 +9,9 @@ module TocDoc
9
9
  # (`get`, `post`, `put`, `patch`, `delete`, `head`), a memoised
10
10
  # Faraday connection, pagination support, and response tracking.
11
11
  #
12
- # All HTTP methods are **private** callers interact with them through
13
- # higher-level endpoint modules (e.g. {TocDoc::Client::Availabilities}).
12
+ # {#get} and {#paginate} are public so that model classes (e.g.
13
+ # {TocDoc::Availability}) can call them via `TocDoc.client`; all other
14
+ # HTTP verb methods remain private.
14
15
  #
15
16
  # @see TocDoc::Client
16
17
  module Connection
@@ -107,12 +108,11 @@ module TocDoc
107
108
 
108
109
  # Performs a paginated GET, accumulating results across pages.
109
110
  #
110
- # When {Configurable#auto_paginate} is disabled or no block is given,
111
- # behaves exactly like {#get}.
111
+ # Behaves exactly like {#get} when no block is given.
112
112
  #
113
- # When +auto_paginate+ is +true+ **and** a block is provided, the block is
114
- # yielded after every page fetch — including the first — with
115
- # +(accumulator, last_response)+. The block must:
113
+ # When a block is provided, it is yielded after every page fetch —
114
+ # including the first — with +(accumulator, last_response)+. The block
115
+ # must:
116
116
  #
117
117
  # 1. Detect whether it is a continuation call by comparing object identity:
118
118
  # `acc.equal?(last_response.body)` is `true` only on the first yield,
@@ -129,7 +129,7 @@ module TocDoc
129
129
  # @return [Object] the fully-accumulated response body
130
130
  def paginate(path, options = {}, &)
131
131
  data = get(path, options)
132
- return data unless block_given? && auto_paginate
132
+ return data unless block_given?
133
133
 
134
134
  loop do
135
135
  next_options = yield(data, last_response)
@@ -192,5 +192,7 @@ module TocDoc
192
192
 
193
193
  [query || {}, explicit_headers]
194
194
  end
195
+
196
+ public :get, :paginate
195
197
  end
196
198
  end
@@ -28,9 +28,6 @@ module TocDoc
28
28
  # @return [Integer] the hard upper limit for per_page
29
29
  MAX_PER_PAGE = 15
30
30
 
31
- # @return [Boolean] whether to auto-paginate by default
32
- AUTO_PAGINATE = false
33
-
34
31
  # @return [Integer] the default maximum number of retries
35
32
  MAX_RETRY = 3
36
33
 
@@ -45,7 +42,6 @@ module TocDoc
45
42
  user_agent: user_agent,
46
43
  default_media_type: default_media_type,
47
44
  per_page: per_page,
48
- auto_paginate: auto_paginate,
49
45
  middleware: middleware,
50
46
  connection_options: connection_options
51
47
  }
@@ -93,19 +89,6 @@ module TocDoc
93
89
  PER_PAGE
94
90
  end
95
91
 
96
- # Whether to follow pagination automatically.
97
- #
98
- # Falls back to the `TOCDOC_AUTO_PAGINATE` environment variable (set to
99
- # `"true"` to enable), then {AUTO_PAGINATE}.
100
- #
101
- # @return [Boolean]
102
- def auto_paginate
103
- env_val = ENV.fetch('TOCDOC_AUTO_PAGINATE', nil)
104
- return AUTO_PAGINATE if env_val.nil?
105
-
106
- env_val.casecmp('true').zero?
107
- end
108
-
109
92
  # The default Faraday middleware stack.
110
93
  #
111
94
  # Includes retry logic, error raising, JSON parsing, and the default
@@ -5,13 +5,13 @@ module TocDoc
5
5
  #
6
6
  # Doctolib expects certain ID list parameters to be dash-joined strings
7
7
  # rather than standard repeated/bracket array notation. Include this module
8
- # into endpoint modules and call +dashed_ids+ explicitly for each such param:
8
+ # and call +dashed_ids+ explicitly for each such param:
9
9
  #
10
- # module TocDoc::Client::Availabilities
11
- # include TocDoc::UriUtils
10
+ # class TocDoc::Availability
11
+ # extend TocDoc::UriUtils
12
12
  #
13
- # def availabilities(visit_motive_ids:, agenda_ids:, **opts)
14
- # get('/availabilities.json', query: {
13
+ # def self.where(visit_motive_ids:, agenda_ids:, **opts)
14
+ # client.get('/availabilities.json', query: {
15
15
  # visit_motive_ids: dashed_ids(visit_motive_ids),
16
16
  # agenda_ids: dashed_ids(agenda_ids),
17
17
  # **opts
@@ -4,5 +4,5 @@ module TocDoc
4
4
  # The current version of the TocDoc gem.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '1.0.0'
7
+ VERSION = '1.2.0'
8
8
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'toc_doc/models/availability'
5
+
6
+ module TocDoc
7
+ class Availability
8
+ # An Enumerable collection of {TocDoc::Availability} instances returned
9
+ # by {TocDoc::Availability.where}.
10
+ #
11
+ # @example Iterate over available slots
12
+ # collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
13
+ # collection.each { |avail| puts avail.date }
14
+ #
15
+ # @example Access metadata
16
+ # collection.total #=> 5
17
+ # collection.next_slot #=> "2026-02-28T10:00:00.000+01:00"
18
+ class Collection
19
+ include Enumerable
20
+
21
+ attr_reader :path, :query
22
+
23
+ # @param data [Hash] parsed first-page response body
24
+ # @param query [Hash] original query params (used to build next-page requests)
25
+ # @param path [String] API path for subsequent requests
26
+ def initialize(data, query: {}, path: '/availabilities.json')
27
+ @data = data.dup
28
+ @query = query
29
+ @path = path
30
+ end
31
+
32
+ # Iterates over {TocDoc::Availability} instances that have at least one slot.
33
+ #
34
+ # @yieldparam availability [TocDoc::Availability]
35
+ # @return [Enumerator] if no block given
36
+ def each(&)
37
+ filtered_entries.each(&)
38
+ end
39
+
40
+ # The total number of available slots in the collection.
41
+ #
42
+ # @return [Integer]
43
+ def total
44
+ @data['total']
45
+ end
46
+
47
+ # The nearest available appointment slot.
48
+ #
49
+ # Returns the +next_slot+ value from the API when present (which only
50
+ # occurs when none of the loaded dates have any slots). Otherwise
51
+ # returns the first slot of the first date that has one.
52
+ #
53
+ # @return [String, nil] ISO 8601 datetime string, or +nil+ when unavailable
54
+ def next_slot
55
+ return @data['next_slot'] if @data.key?('next_slot')
56
+
57
+ Array(@data['availabilities']).each do |entry|
58
+ slots = Array(entry['slots'])
59
+ return slots.first unless slots.empty?
60
+ end
61
+
62
+ nil
63
+ end
64
+
65
+ # All date entries — including those with no slots — as {TocDoc::Availability}
66
+ # objects.
67
+ #
68
+ # @return [Array<TocDoc::Availability>]
69
+ def raw_availabilities
70
+ Array(@data['availabilities']).map { |entry| TocDoc::Availability.new(entry) }
71
+ end
72
+
73
+ # Returns a plain Hash representation of the collection.
74
+ #
75
+ # The +availabilities+ key contains only dates with slots (filtered),
76
+ # serialised back to plain Hashes.
77
+ #
78
+ # @return [Hash{String => Object}]
79
+ def to_h
80
+ @data.merge('availabilities' => filtered_entries.map(&:to_h))
81
+ end
82
+
83
+ # Fetches the next window of availabilities (starting the day after the
84
+ # last date in the current collection) and merges them in.
85
+ #
86
+ # @param page_data [Hash] parsed response body to merge into this collection
87
+ # @return [self]
88
+ def merge_page!(page_data)
89
+ @data['availabilities'] = @data.fetch('availabilities', []) + page_data.fetch('availabilities', [])
90
+ @data['total'] = @data.fetch('total', 0) + page_data.fetch('total', 0)
91
+ self
92
+ end
93
+
94
+ private
95
+
96
+ def filtered_entries
97
+ Array(@data['availabilities'])
98
+ .select { |entry| Array(entry['slots']).any? }
99
+ .map { |entry| TocDoc::Availability.new(entry) }
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,21 +1,104 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
4
+ require 'toc_doc/core/uri_utils'
5
+
3
6
  module TocDoc
4
7
  # Represents a single availability date entry returned by the Doctolib API.
5
8
  #
6
9
  # @example
7
10
  # avail = TocDoc::Availability.new('date' => '2026-02-28', 'slots' => ['2026-02-28T10:00:00.000+01:00'])
8
- # avail.date #=> "2026-02-28"
9
- # avail.slots #=> ["2026-02-28T10:00:00.000+01:00"]
11
+ # avail.date #=> #<Date: 2026-02-28>
12
+ # avail.raw_date #=> "2026-02-28"
13
+ # avail.slots #=> [#<DateTime: 2026-02-28T10:00:00.000+01:00>]
14
+ # avail.raw_slots #=> ["2026-02-28T10:00:00.000+01:00"]
10
15
  class Availability < Resource
11
- # @return [String] date in ISO 8601 format (e.g. "2026-02-28")
12
- def date
13
- @attrs['date']
16
+ extend TocDoc::UriUtils
17
+
18
+ attr_reader :date, :slots
19
+
20
+ PATH = '/availabilities.json'
21
+
22
+ class << self
23
+ # Fetches availabilities from the API and returns an {Availability::Collection}.
24
+ #
25
+ # When the API response contains a +next_slot+ key — indicating that no
26
+ # date in the current window has available slots — a second request is
27
+ # made automatically from that date before the collection is returned.
28
+ #
29
+ # @param visit_motive_ids [Integer, String, Array<Integer>]
30
+ # one or more visit-motive IDs (dash-joined for the API)
31
+ # @param agenda_ids [Integer, String, Array<Integer>]
32
+ # one or more agenda IDs (dash-joined for the API)
33
+ # @param start_date [Date, String]
34
+ # earliest date to search from (default: +Date.today+)
35
+ # @param limit [Integer]
36
+ # maximum availability dates per page (default: +TocDoc.per_page+)
37
+ # @param options [Hash]
38
+ # additional query params forwarded verbatim to the API
39
+ # @return [TocDoc::Availability::Collection]
40
+ #
41
+ # @example
42
+ # TocDoc::Availability.where(
43
+ # visit_motive_ids: 7_767_829,
44
+ # agenda_ids: [1_101_600],
45
+ # start_date: Date.today
46
+ # ).each { |avail| puts avail.date }
47
+ def where(visit_motive_ids:, agenda_ids:, start_date: Date.today,
48
+ limit: TocDoc.per_page, **options)
49
+ client = TocDoc.client
50
+ query = build_query(visit_motive_ids, agenda_ids, start_date, limit, options)
51
+ data = client.get(PATH, query: query)
52
+
53
+ merge_next_page(client, query, data) if data['next_slot']
54
+
55
+ Collection.new(data, query: query, path: PATH)
56
+ end
57
+
58
+ private
59
+
60
+ def merge_next_page(client, query, current_page)
61
+ next_date = Date.parse(current_page['next_slot']).to_s
62
+ next_page = client.get(PATH, query: query.merge(start_date: next_date))
63
+ merge_page_data(current_page, next_page)
64
+ end
65
+
66
+ def merge_page_data(current_page, next_page)
67
+ current_page['availabilities'] =
68
+ current_page.fetch('availabilities', []) + next_page.fetch('availabilities', [])
69
+ current_page['total'] = current_page.fetch('total', 0) + next_page.fetch('total', 0)
70
+ current_page.delete('next_slot')
71
+ current_page['next_slot'] = next_page['next_slot'] if next_page.key?('next_slot')
72
+ end
73
+
74
+ def build_query(visit_motive_ids, agenda_ids, start_date, limit, extra)
75
+ {
76
+ visit_motive_ids: dashed_ids(visit_motive_ids),
77
+ agenda_ids: dashed_ids(agenda_ids),
78
+ start_date: start_date.to_s,
79
+ limit: [limit.to_i, TocDoc::Default::MAX_PER_PAGE].min,
80
+ **extra
81
+ }
82
+ end
14
83
  end
15
84
 
16
- # @return [Array<String>] ISO 8601 datetime strings for each available slot
17
- def slots
18
- @attrs['slots'] || []
85
+ # @param attrs [Hash] raw attributes from the API response; expected to include
86
+ # a +date+ key (ISO 8601 date string) and a +slots+ key (Array of ISO 8601 datetime strings)
87
+ def initialize(*attrs)
88
+ super
89
+ raw = build_raw(@attrs)
90
+
91
+ @date = Date.parse(raw['date']) if raw['date']
92
+ @slots = raw['slots'].map { |s| DateTime.parse(s) }
93
+ end
94
+
95
+ private
96
+
97
+ def build_raw(attrs)
98
+ {
99
+ 'date' => attrs['date'],
100
+ 'slots' => attrs['slots'] || []
101
+ }
19
102
  end
20
103
  end
21
104
  end
@@ -2,5 +2,4 @@
2
2
 
3
3
  require 'toc_doc/models/resource'
4
4
  require 'toc_doc/models/availability'
5
-
6
- require 'toc_doc/models/response/availability'
5
+ require 'toc_doc/models/availability/collection'
data/lib/toc_doc.rb CHANGED
@@ -16,13 +16,9 @@ require 'toc_doc/client'
16
16
  # Configuration can be set at the module level and will be inherited by every
17
17
  # {TocDoc::Client} instance created via {.client} or {.setup}.
18
18
  #
19
- # Any method available on {TocDoc::Client} can be called directly on `TocDoc`
20
- # and will be forwarded to the memoized {.client}.
21
- #
22
19
  # @example Quick start
23
20
  # TocDoc.setup do |config|
24
21
  # config.api_endpoint = 'https://www.doctolib.de'
25
- # config.auto_paginate = true
26
22
  # end
27
23
  #
28
24
  # TocDoc.availabilities(
@@ -70,6 +66,16 @@ module TocDoc
70
66
  client
71
67
  end
72
68
 
69
+ # Returns available appointment slots.
70
+ #
71
+ # Delegates to {TocDoc::Availability.where} — see that method for full
72
+ # parameter documentation.
73
+ #
74
+ # @return [TocDoc::Availability::Collection]
75
+ def availabilities(**)
76
+ TocDoc::Availability.where(**)
77
+ end
78
+
73
79
  # @!visibility private
74
80
  def method_missing(method_name, ...)
75
81
  if client.respond_to?(method_name)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toc_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 01max
@@ -45,7 +45,7 @@ dependencies:
45
45
  version: '2.0'
46
46
  description: |-
47
47
  A standalone Ruby gem providing a Faraday-based client
48
- with modular resource endpoints, configurable defaults, and a clean error hierarchy.
48
+ to interact with the (unofficial) Doctolib API.
49
49
  email:
50
50
  - m.louguet@gmail.com
51
51
  executables: []
@@ -57,12 +57,12 @@ files:
57
57
  - CHANGELOG.md
58
58
  - CODE_OF_CONDUCT.md
59
59
  - LICENSE.md
60
+ - POTENTIAL_ENDPOINTS.md
60
61
  - README.md
61
62
  - Rakefile
62
63
  - TODO.md
63
64
  - lib/toc_doc.rb
64
65
  - lib/toc_doc/client.rb
65
- - lib/toc_doc/client/availabilities.rb
66
66
  - lib/toc_doc/core/authentication.rb
67
67
  - lib/toc_doc/core/configurable.rb
68
68
  - lib/toc_doc/core/connection.rb
@@ -75,14 +75,13 @@ files:
75
75
  - lib/toc_doc/middleware/.keep
76
76
  - lib/toc_doc/models.rb
77
77
  - lib/toc_doc/models/availability.rb
78
+ - lib/toc_doc/models/availability/collection.rb
78
79
  - lib/toc_doc/models/resource.rb
79
- - lib/toc_doc/models/response/availability.rb
80
80
  - sig/toc_doc.rbs
81
81
  homepage: https://github.com/01max/toc_doc
82
82
  licenses:
83
83
  - GPL-3.0-or-later
84
84
  metadata:
85
- allowed_push_host: https://rubygems.org
86
85
  homepage_uri: https://github.com/01max/toc_doc
87
86
  source_code_uri: https://github.com/01max/toc_doc/tree/main
88
87
  changelog_uri: https://github.com/01max/toc_doc/blob/main/CHANGELOG.md
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'date'
4
-
5
- module TocDoc
6
- class Client
7
- # Endpoint module for the Doctolib availabilities API.
8
- #
9
- # Included into {TocDoc::Client}; methods delegate to
10
- # {TocDoc::Connection#get} via the enclosing client instance.
11
- #
12
- # @see https://www.doctolib.fr/availabilities.json Doctolib availability endpoint
13
- module Availabilities
14
- # Returns available appointment slots for the given visit motives and
15
- # agendas.
16
- #
17
- # When +auto_paginate+ is enabled, all pages of results are
18
- # fetched and merged automatically.
19
- #
20
- # @param visit_motive_ids [Integer, String, Array<Integer>]
21
- # one or more visit-motive IDs (dash-joined for the API)
22
- # @param agenda_ids [Integer, String, Array<Integer>]
23
- # one or more agenda IDs (dash-joined for the API)
24
- # @param start_date [Date, String]
25
- # earliest date to search from (default: +Date.today+)
26
- # @param limit [Integer]
27
- # maximum number of availability dates per page
28
- # (default: +per_page+ config)
29
- # @param options [Hash]
30
- # additional query params forwarded verbatim to the API
31
- # (e.g. +practice_ids:+, +telehealth:+)
32
- # @return [TocDoc::Response::Availability] structured response object
33
- #
34
- # @example Fetch availabilities for a single practitioner
35
- # client.availabilities(
36
- # visit_motive_ids: 7_767_829,
37
- # agenda_ids: [1_101_600],
38
- # practice_ids: 377_272,
39
- # telehealth: false
40
- # )
41
- #
42
- # @example Via the module-level shortcut
43
- # TocDoc.availabilities(visit_motive_ids: 123, agenda_ids: 456)
44
- def availabilities(visit_motive_ids:, agenda_ids:, start_date: Date.today, limit: per_page, **options)
45
- base_query = build_availability_query(visit_motive_ids, agenda_ids, start_date, limit, options)
46
-
47
- response = paginate('/availabilities.json', query: base_query) do |acc, last_resp|
48
- paginate_availability_page(acc, last_resp, base_query)
49
- end
50
-
51
- TocDoc::Response::Availability.new(response)
52
- end
53
-
54
- private
55
-
56
- # Builds the query hash sent to the availabilities endpoint.
57
- #
58
- # @param visit_motive_ids [Integer, String, Array] raw motive IDs
59
- # @param agenda_ids [Integer, String, Array] raw agenda IDs
60
- # @param start_date [Date, String] earliest search date
61
- # @param limit [Integer] page size
62
- # @param extra [Hash] additional query params
63
- # @return [Hash{Symbol => Object}] ready-to-send query hash
64
- def build_availability_query(visit_motive_ids, agenda_ids, start_date, limit, extra)
65
- {
66
- visit_motive_ids: dashed_ids(visit_motive_ids),
67
- agenda_ids: dashed_ids(agenda_ids),
68
- start_date: start_date.to_s,
69
- limit: [limit.to_i, TocDoc::Default::MAX_PER_PAGE].min,
70
- **extra
71
- }
72
- end
73
-
74
- # Merges the latest page body into the accumulator and returns options
75
- # for the next page, or +nil+ to halt pagination.
76
- #
77
- # On the first yield +acc+ *is* the first-page body (identical object),
78
- # so the merge step is skipped.
79
- #
80
- # @param acc [Hash] growing accumulator
81
- # @param last_resp [Faraday::Response] the most-recent raw response
82
- # @param base_query [Hash] the original query hash
83
- # @return [Hash, nil] options for the next page, or +nil+ to stop
84
- def paginate_availability_page(acc, last_resp, base_query)
85
- latest = last_resp.body
86
-
87
- merge_availability_page(acc, latest) unless acc.equal?(latest)
88
- availability_next_page_options(latest, base_query)
89
- end
90
-
91
- # Merges a new page body into the running accumulator.
92
- #
93
- # @param acc [Hash] the accumulator hash
94
- # @param latest [Hash] the most-recent page body
95
- # @return [void]
96
- def merge_availability_page(acc, latest)
97
- acc['availabilities'] = (acc['availabilities'] || []) + (latest['availabilities'] || [])
98
- acc['total'] = (acc['total'] || 0) + (latest['total'] || 0)
99
- acc['next_slot'] = latest['next_slot']
100
- end
101
-
102
- # Determines the options for the next page of availabilities, or +nil+
103
- # if pagination should stop.
104
- #
105
- # @param latest [Hash] the most-recent page body
106
- # @param base_query [Hash] the original query hash
107
- # @return [Hash, nil] next-page options, or +nil+ to halt
108
- def availability_next_page_options(latest, base_query)
109
- avails = latest['availabilities'] || []
110
- last_date_str = avails.last&.dig('date')
111
- return unless last_date_str && latest['next_slot']
112
-
113
- next_start = (Date.parse(last_date_str) + 1).to_s
114
- { query: base_query.merge(start_date: next_start) }
115
- end
116
- end
117
- end
118
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TocDoc
4
- module Response
5
- # Wraps the top-level response from the Doctolib availabilities API.
6
- #
7
- # @example
8
- # response = TocDoc::Response::Availability.new(parsed_json)
9
- # response.total #=> 2
10
- # response.next_slot #=> "2026-02-28T10:00:00.000+01:00"
11
- # response.availabilities #=> [#<TocDoc::Availability ...>, ...]
12
- class Availability < Resource
13
- # The total number of available slots across all dates.
14
- #
15
- # @return [Integer]
16
- #
17
- # @example
18
- # response.total #=> 5
19
- def total
20
- @attrs['total']
21
- end
22
-
23
- # The nearest available appointment slot.
24
- #
25
- # When the API includes an explicit +next_slot+ key (common when there
26
- # are no slots in the loaded date window) that value is returned
27
- # directly. Otherwise the first slot of the first date that has one
28
- # is returned.
29
- #
30
- # @return [String, nil] ISO 8601 datetime string, or +nil+ when no
31
- # slot is available
32
- #
33
- # @example
34
- # response.next_slot #=> "2026-02-28T10:00:00.000+01:00"
35
- def next_slot
36
- return @attrs['next_slot'] if @attrs.key?('next_slot')
37
-
38
- Array(@attrs['availabilities']).each do |entry|
39
- slots = Array(entry['slots'])
40
- return slots.first unless slots.empty?
41
- end
42
-
43
- nil
44
- end
45
-
46
- # Dates that have at least one available slot, wrapped as
47
- # {TocDoc::Availability} objects.
48
- #
49
- # @return [Array<TocDoc::Availability>]
50
- #
51
- # @example
52
- # response.availabilities.each do |avail|
53
- # puts "#{avail.date}: #{avail.slots.size} slot(s)"
54
- # end
55
- def availabilities
56
- @availabilities ||= Array(@attrs['availabilities'])
57
- .select { |entry| Array(entry['slots']).any? }
58
- .map { |entry| TocDoc::Availability.new(entry) }
59
- end
60
-
61
- # All availability date entries, including those with no slots.
62
- #
63
- # @return [Array<TocDoc::Availability>]
64
- def raw_availabilities
65
- @raw_availabilities ||= Array(@attrs['availabilities']).map do |entry|
66
- TocDoc::Availability.new(entry)
67
- end
68
- end
69
-
70
- # Returns a plain Hash representation, with nested +availabilities+
71
- # expanded back to raw Hashes.
72
- #
73
- # @return [Hash{String => Object}]
74
- def to_h
75
- super.merge('availabilities' => availabilities.map(&:to_h))
76
- end
77
- end
78
- end
79
- end