toc_doc 1.1.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 +4 -4
- data/CHANGELOG.md +20 -0
- data/POTENTIAL_ENDPOINTS.md +31 -0
- data/README.md +71 -58
- data/TODO.md +25 -33
- data/lib/toc_doc/client.rb +0 -6
- data/lib/toc_doc/core/configurable.rb +0 -3
- data/lib/toc_doc/core/connection.rb +10 -8
- data/lib/toc_doc/core/default.rb +0 -17
- data/lib/toc_doc/core/uri_utils.rb +5 -5
- data/lib/toc_doc/core/version.rb +1 -1
- data/lib/toc_doc/models/availability/collection.rb +103 -0
- data/lib/toc_doc/models/availability.rb +70 -1
- data/lib/toc_doc/models.rb +1 -2
- data/lib/toc_doc.rb +10 -4
- metadata +3 -3
- data/lib/toc_doc/client/availabilities.rb +0 -118
- data/lib/toc_doc/models/response/availability.rb +0 -79
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcac91743cd8f8d50d032638c3a690f88c786a369960d8b5df8bdead6006149c
|
|
4
|
+
data.tar.gz: d8df90b16b2ff3e2311af4751396ff1ae54173575e4507989df0e7c910bd82eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8886203725b1dd9bbd59e62ee56aaf4f0965c908b31d478568c9d0149a7062f7059509f5692f18273e08b4a05d90d8a6d6e8bb02e89342ab2d1a18e92b2b16a
|
|
7
|
+
data.tar.gz: fd9271676246e8da0e7e626ebc3c10316f5b86b2ab6fc811a67986bb276aa1e817c74f7d17fec64372cfb15048638b1aac32e08b2dce981142646ed4a1289977
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
3
23
|
## [1.1.0] - 2026-03-06
|
|
4
24
|
|
|
5
25
|
### Changed
|
|
@@ -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,6 @@
|
|
|
1
1
|
# TocDoc
|
|
2
2
|
|
|
3
|
-
A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-based client with
|
|
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
4
|
|
|
5
5
|
[](https://badge.fury.io/rb/toc_doc)
|
|
6
6
|
[](https://github.com/01max/toc_doc/actions)
|
|
@@ -65,18 +65,17 @@ gem install toc_doc
|
|
|
65
65
|
```ruby
|
|
66
66
|
require 'toc_doc'
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
response = TocDoc.availabilities(
|
|
68
|
+
collection = TocDoc::Availability.where(
|
|
70
69
|
visit_motive_ids: 7_767_829,
|
|
71
70
|
agenda_ids: 1_101_600,
|
|
72
71
|
practice_ids: 377_272,
|
|
73
72
|
telehealth: false
|
|
74
73
|
)
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
collection.total # => 5
|
|
76
|
+
collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
|
|
78
77
|
|
|
79
|
-
|
|
78
|
+
collection.each do |avail|
|
|
80
79
|
puts "#{avail.date}: #{avail.slots.map { |s| s.strftime('%H:%M') }.join(', ')}"
|
|
81
80
|
end
|
|
82
81
|
```
|
|
@@ -95,7 +94,7 @@ TocDoc.configure do |config|
|
|
|
95
94
|
config.per_page = 10
|
|
96
95
|
end
|
|
97
96
|
|
|
98
|
-
TocDoc.
|
|
97
|
+
TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
|
|
99
98
|
```
|
|
100
99
|
|
|
101
100
|
Calling `TocDoc.reset!` restores all options to their defaults.
|
|
@@ -103,14 +102,24 @@ Use `TocDoc.options` to inspect the current configuration hash.
|
|
|
103
102
|
|
|
104
103
|
### Per-client configuration
|
|
105
104
|
|
|
106
|
-
Instantiate independent clients with different options
|
|
105
|
+
Instantiate independent clients with different options and query via `TocDoc::Availability.where`:
|
|
107
106
|
|
|
108
107
|
```ruby
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
```
|
|
117
|
+
|
|
118
|
+
Alternatively, use `TocDoc::Client` directly for lower-level access ().
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
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 })
|
|
114
123
|
```
|
|
115
124
|
|
|
116
125
|
### All configuration options
|
|
@@ -118,10 +127,9 @@ it_client.availabilities(visit_motive_ids: 789, agenda_ids: 101)
|
|
|
118
127
|
| Option | Default | Description |
|
|
119
128
|
|---|---|---|
|
|
120
129
|
| `api_endpoint` | `https://www.doctolib.fr` | Base URL. Change to `.de` / `.it` for other countries. |
|
|
121
|
-
| `user_agent` | `TocDoc Ruby Gem
|
|
130
|
+
| `user_agent` | `TocDoc Ruby Gem 1.2.0` | `User-Agent` header sent with every request. |
|
|
122
131
|
| `default_media_type` | `application/json` | `Accept` and `Content-Type` headers. |
|
|
123
|
-
| `per_page` | `
|
|
124
|
-
| `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`). |
|
|
125
133
|
| `middleware` | Retry + RaiseError + JSON + adapter | Full Faraday middleware stack. Override to customise completely. |
|
|
126
134
|
| `connection_options` | `{}` | Options passed directly to `Faraday.new`. |
|
|
127
135
|
|
|
@@ -135,7 +143,6 @@ All primary options can be set via environment variables before the gem is loade
|
|
|
135
143
|
| `TOCDOC_USER_AGENT` | `user_agent` |
|
|
136
144
|
| `TOCDOC_MEDIA_TYPE` | `default_media_type` |
|
|
137
145
|
| `TOCDOC_PER_PAGE` | `per_page` |
|
|
138
|
-
| `TOCDOC_AUTO_PAGINATE` | `auto_paginate` (`"true"` / anything else) |
|
|
139
146
|
| `TOCDOC_RETRY_MAX` | Maximum Faraday retry attempts (default `3`) |
|
|
140
147
|
|
|
141
148
|
---
|
|
@@ -147,7 +154,7 @@ All primary options can be set via environment variables before the gem is loade
|
|
|
147
154
|
Retrieve open appointment slots for a given visit motive and agenda.
|
|
148
155
|
|
|
149
156
|
```ruby
|
|
150
|
-
|
|
157
|
+
TocDoc::Availability.where(
|
|
151
158
|
visit_motive_ids: visit_motive_id, # Integer, String, or Array
|
|
152
159
|
agenda_ids: agenda_id, # Integer, String, or Array
|
|
153
160
|
start_date: Date.today, # Date or String (default: today)
|
|
@@ -158,18 +165,20 @@ client.availabilities(
|
|
|
158
165
|
)
|
|
159
166
|
```
|
|
160
167
|
|
|
168
|
+
`TocDoc.availabilities(...)` is a module-level shortcut with the same signature.
|
|
169
|
+
|
|
161
170
|
**Multiple IDs** are accepted as arrays; the gem serialises them with the
|
|
162
171
|
dash-separated format Doctolib expects:
|
|
163
172
|
|
|
164
173
|
```ruby
|
|
165
|
-
|
|
174
|
+
TocDoc::Availability.where(
|
|
166
175
|
visit_motive_ids: [7_767_829, 7_767_830],
|
|
167
176
|
agenda_ids: [1_101_600, 1_101_601]
|
|
168
177
|
)
|
|
169
178
|
# → GET /availabilities.json?visit_motive_ids=7767829-7767830&agenda_ids=1101600-1101601&…
|
|
170
179
|
```
|
|
171
180
|
|
|
172
|
-
**Return value:** a `TocDoc::
|
|
181
|
+
**Return value:** a `TocDoc::Availability::Collection` (see [Response objects](#response-objects)).
|
|
173
182
|
|
|
174
183
|
---
|
|
175
184
|
|
|
@@ -178,20 +187,22 @@ client.availabilities(
|
|
|
178
187
|
All API responses are wrapped in lightweight Ruby objects that provide
|
|
179
188
|
dot-notation access and a `#to_h` round-trip helper.
|
|
180
189
|
|
|
181
|
-
### `TocDoc::
|
|
190
|
+
### `TocDoc::Availability::Collection`
|
|
182
191
|
|
|
183
|
-
Returned by
|
|
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.
|
|
184
194
|
|
|
185
195
|
| Method | Type | Description |
|
|
186
196
|
|---|---|---|
|
|
187
197
|
| `#total` | `Integer` | Total number of available slots across all dates. |
|
|
188
198
|
| `#next_slot` | `String \| nil` | ISO 8601 datetime of the nearest available slot. `nil` when none remain. |
|
|
189
|
-
| `#
|
|
190
|
-
| `#
|
|
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). |
|
|
191
202
|
|
|
192
203
|
### `TocDoc::Availability`
|
|
193
204
|
|
|
194
|
-
Each element
|
|
205
|
+
Represents a single availability date entry. Each element yielded by the collection.
|
|
195
206
|
|
|
196
207
|
| Method | Type | Description |
|
|
197
208
|
|---|---|---|
|
|
@@ -202,15 +213,15 @@ Each element of `Response::Availability#availabilities`.
|
|
|
202
213
|
**Example:**
|
|
203
214
|
|
|
204
215
|
```ruby
|
|
205
|
-
|
|
216
|
+
collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
|
|
206
217
|
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
collection.total # => 5
|
|
219
|
+
collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
|
|
209
220
|
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
collection.first.date # => #<Date: 2026-02-28>
|
|
222
|
+
collection.first.slots # => [#<DateTime: 2026-02-28T10:00:00+01:00>, ...]
|
|
212
223
|
|
|
213
|
-
|
|
224
|
+
collection.to_h
|
|
214
225
|
# => {
|
|
215
226
|
# "total" => 5,
|
|
216
227
|
# "next_slot" => "2026-02-28T10:00:00.000+01:00",
|
|
@@ -222,35 +233,36 @@ response.to_h
|
|
|
222
233
|
|
|
223
234
|
## Pagination
|
|
224
235
|
|
|
225
|
-
The Doctolib availability endpoint is
|
|
226
|
-
|
|
236
|
+
The Doctolib availability endpoint is window-based: each request returns up to
|
|
237
|
+
`limit` dates starting from `start_date`.
|
|
227
238
|
|
|
228
|
-
### Automatic
|
|
239
|
+
### Automatic next-slot resolution
|
|
229
240
|
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
232
245
|
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
### Manual window advancement
|
|
247
|
+
|
|
248
|
+
To fetch additional date windows, call `TocDoc::Availability.where` again with a
|
|
249
|
+
later `start_date`:
|
|
235
250
|
|
|
236
|
-
|
|
251
|
+
```ruby
|
|
252
|
+
first_page = TocDoc::Availability.where(
|
|
237
253
|
visit_motive_ids: 7_767_829,
|
|
238
254
|
agenda_ids: 1_101_600,
|
|
239
255
|
start_date: Date.today
|
|
240
256
|
)
|
|
241
257
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
TocDoc.configure { |c| c.auto_paginate = true }
|
|
252
|
-
|
|
253
|
-
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
|
|
254
266
|
```
|
|
255
267
|
|
|
256
268
|
---
|
|
@@ -262,7 +274,7 @@ so you can rescue the whole hierarchy with a single clause:
|
|
|
262
274
|
|
|
263
275
|
```ruby
|
|
264
276
|
begin
|
|
265
|
-
TocDoc.
|
|
277
|
+
TocDoc::Availability.where(visit_motive_ids: 0, agenda_ids: 0)
|
|
266
278
|
rescue TocDoc::Error => e
|
|
267
279
|
puts "Doctolib error: #{e.message}"
|
|
268
280
|
end
|
|
@@ -319,13 +331,14 @@ bundle exec rake install
|
|
|
319
331
|
|
|
320
332
|
### Adding new endpoints
|
|
321
333
|
|
|
322
|
-
1. Create `lib/toc_doc/
|
|
323
|
-
`TocDoc::
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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/`.
|
|
329
342
|
|
|
330
343
|
### Generating documentation
|
|
331
344
|
|
data/TODO.md
CHANGED
|
@@ -1,50 +1,38 @@
|
|
|
1
1
|
# PLAN
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- [x] Identify additional endpoints
|
|
5
|
-
- [ ] Prioritize implementation of resource modules for those endpoints
|
|
3
|
+
[POTENTIAL_ENDPOINTS][POTENTIAL_ENDPOINTS.md]
|
|
6
4
|
|
|
7
|
-
## 1.
|
|
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
|
|
8
23
|
|
|
9
24
|
### Better API usage
|
|
10
25
|
- [ ] Rate limiting
|
|
11
26
|
- [ ] Caching
|
|
12
27
|
- [ ] Logging
|
|
13
28
|
|
|
14
|
-
##
|
|
29
|
+
## 2.0
|
|
15
30
|
|
|
16
31
|
### Auth / User-based actions
|
|
17
32
|
- [ ] Research auth scheme
|
|
18
33
|
- [ ] Authentication module + headers
|
|
19
34
|
- [ ] Auth specs
|
|
20
35
|
|
|
21
|
-
# Potential Endpoints
|
|
22
|
-
|
|
23
|
-
- Interesting JSONs [GET]
|
|
24
|
-
- Practitioner ?
|
|
25
|
-
- https://www.doctolib.fr/pharmacie/paris/pharmacie-faidherbe.json
|
|
26
|
-
- https://www.doctolib.fr/dentiste/bordeaux/mathilde-devun-lesparre-medoc.json
|
|
27
|
-
- https://www.doctolib.fr/a/b/mathilde-devun-lesparre-medoc.json
|
|
28
|
-
- https://www.doctolib.fr/profiles/mathilde-devun-lesparre-medoc.json
|
|
29
|
-
- Rassemblement practiciens / Place Practitioners collection
|
|
30
|
-
- https://www.doctolib.fr/profiles/pavillon-de-la-mutualite-bordeaux-rue-vital-carles.json
|
|
31
|
-
- Places
|
|
32
|
-
- https://www.doctolib.fr/patient_app/place_autocomplete.json?query=47300
|
|
33
|
-
- Interesting NON JSONs
|
|
34
|
-
- City practitioners (❗️JSON-LD in a script tag of an HTML page - data-id="removable-json-ld")
|
|
35
|
-
- https://www.doctolib.fr/dentiste/bordeaux/
|
|
36
|
-
- https://www.doctolib.fr/medecin-generaliste/bordeaux/
|
|
37
|
-
- https://www.doctolib.fr/medecin-generaliste/villeneuve-sur-lot
|
|
38
|
-
- Non-interesting
|
|
39
|
-
- Legal links
|
|
40
|
-
- https://www.doctolib.fr/search/footer_legal_links.json
|
|
41
|
-
- FAQ
|
|
42
|
-
- https://www.doctolib.fr/search/footer_public_content.json?hub_search=false&display_faq=true&speciality_id=2&place_id=18733
|
|
43
|
-
- Social media links
|
|
44
|
-
- https://www.doctolib.fr/search/footer_social_media_links.json
|
|
45
|
-
- New Booking [POST]
|
|
46
|
-
- online_booking/draft/new.json
|
|
47
|
-
|
|
48
36
|
# DONE & RELEASED
|
|
49
37
|
|
|
50
38
|
## 1.0
|
|
@@ -105,4 +93,8 @@
|
|
|
105
93
|
## 1.1
|
|
106
94
|
|
|
107
95
|
### Parse raw API data
|
|
108
|
-
- [x] Parse date / datetime
|
|
96
|
+
- [x] Parse date / datetime
|
|
97
|
+
|
|
98
|
+
## 1.2
|
|
99
|
+
|
|
100
|
+
- [x] Rework Availability's client, model and collection architecture.
|
data/lib/toc_doc/client.rb
CHANGED
|
@@ -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
|
-
#
|
|
13
|
-
#
|
|
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
|
-
#
|
|
111
|
-
# behaves exactly like {#get}.
|
|
111
|
+
# Behaves exactly like {#get} when no block is given.
|
|
112
112
|
#
|
|
113
|
-
# When
|
|
114
|
-
#
|
|
115
|
-
#
|
|
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?
|
|
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
|
data/lib/toc_doc/core/default.rb
CHANGED
|
@@ -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
|
-
#
|
|
8
|
+
# and call +dashed_ids+ explicitly for each such param:
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# class TocDoc::Availability
|
|
11
|
+
# extend TocDoc::UriUtils
|
|
12
12
|
#
|
|
13
|
-
# def
|
|
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
|
data/lib/toc_doc/core/version.rb
CHANGED
|
@@ -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,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'date'
|
|
4
|
+
require 'toc_doc/core/uri_utils'
|
|
4
5
|
|
|
5
6
|
module TocDoc
|
|
6
7
|
# Represents a single availability date entry returned by the Doctolib API.
|
|
@@ -12,9 +13,77 @@ module TocDoc
|
|
|
12
13
|
# avail.slots #=> [#<DateTime: 2026-02-28T10:00:00.000+01:00>]
|
|
13
14
|
# avail.raw_slots #=> ["2026-02-28T10:00:00.000+01:00"]
|
|
14
15
|
class Availability < Resource
|
|
16
|
+
extend TocDoc::UriUtils
|
|
17
|
+
|
|
15
18
|
attr_reader :date, :slots
|
|
16
19
|
|
|
17
|
-
|
|
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
|
|
83
|
+
end
|
|
84
|
+
|
|
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)
|
|
18
87
|
def initialize(*attrs)
|
|
19
88
|
super
|
|
20
89
|
raw = build_raw(@attrs)
|
data/lib/toc_doc/models.rb
CHANGED
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.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 01max
|
|
@@ -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,8 +75,8 @@ 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:
|
|
@@ -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
|