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 +4 -4
- data/CHANGELOG.md +31 -0
- data/POTENTIAL_ENDPOINTS.md +31 -0
- data/README.md +80 -61
- data/TODO.md +56 -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 +91 -8
- data/lib/toc_doc/models.rb +1 -2
- data/lib/toc_doc.rb +10 -4
- metadata +4 -5
- 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,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
|
|
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
|
+
[](https://badge.fury.io/rb/toc_doc)
|
|
6
|
+
[](https://github.com/01max/toc_doc/actions)
|
|
7
|
+
[](https://coveralls.io/github/01max/toc_doc?branch=main)
|
|
8
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
9
|
+
[](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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
75
|
+
collection.total # => 5
|
|
76
|
+
collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
|
|
72
77
|
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
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` | `
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
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::
|
|
190
|
+
### `TocDoc::Availability::Collection`
|
|
176
191
|
|
|
177
|
-
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.
|
|
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
|
-
| `#
|
|
184
|
-
| `#
|
|
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
|
|
205
|
+
Represents a single availability date entry. Each element yielded by the collection.
|
|
189
206
|
|
|
190
207
|
| Method | Type | Description |
|
|
191
208
|
|---|---|---|
|
|
192
|
-
| `#date` | `
|
|
193
|
-
| `#slots` | `Array<
|
|
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
|
-
|
|
216
|
+
collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
|
|
200
217
|
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
collection.total # => 5
|
|
219
|
+
collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
|
|
203
220
|
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
collection.first.date # => #<Date: 2026-02-28>
|
|
222
|
+
collection.first.slots # => [#<DateTime: 2026-02-28T10:00:00+01:00>, ...]
|
|
206
223
|
|
|
207
|
-
|
|
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
|
|
220
|
-
|
|
236
|
+
The Doctolib availability endpoint is window-based: each request returns up to
|
|
237
|
+
`limit` dates starting from `start_date`.
|
|
221
238
|
|
|
222
|
-
### Automatic
|
|
239
|
+
### Automatic next-slot resolution
|
|
223
240
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
client = TocDoc::Client.new(auto_paginate: true, per_page: 5)
|
|
246
|
+
### Manual window advancement
|
|
229
247
|
|
|
230
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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.
|
|
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/
|
|
317
|
-
`TocDoc::
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
+
# PLAN
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
### 8 – Pagination
|
|
41
78
|
- [x] Analyze pagination model
|
|
42
79
|
- [x] Implement Connection#paginate
|
|
43
80
|
- [x] Pagination config & specs
|
|
44
81
|
|
|
45
|
-
|
|
82
|
+
### 9 – Docs & Release
|
|
46
83
|
- [x] README
|
|
47
84
|
- [x] YARD docs
|
|
48
85
|
- [x] CHANGELOG
|
|
49
86
|
- [x] FIX GH CI
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
- [
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
##
|
|
57
|
-
- [ ] Parse date / datetime
|
|
58
|
-
- [ ] ?
|
|
93
|
+
## 1.1
|
|
59
94
|
|
|
60
|
-
|
|
61
|
-
- [
|
|
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
|
-
##
|
|
68
|
-
- [ ] Identify additional endpoints
|
|
69
|
-
- [ ] Implement resource modules
|
|
70
|
-
- [ ] Specs per endpoint
|
|
98
|
+
## 1.2
|
|
71
99
|
|
|
72
|
-
|
|
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.
|
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,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
|
|
9
|
-
# avail.
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
# @
|
|
17
|
-
|
|
18
|
-
|
|
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
|
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
|
|
@@ -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
|
-
|
|
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
|