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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31301e5ab8d81e4a073f6a86da51671dd7d41673eff44b48373659831b971010
4
- data.tar.gz: e308514b6009f7b703d8861e739d963ea0048ed930778329bd453b1f28f1e82c
3
+ metadata.gz: dcac91743cd8f8d50d032638c3a690f88c786a369960d8b5df8bdead6006149c
4
+ data.tar.gz: d8df90b16b2ff3e2311af4751396ff1ae54173575e4507989df0e7c910bd82eb
5
5
  SHA512:
6
- metadata.gz: c8936171d0c8228e3a1390ef3d164ab5ca08cccecb656087cc983d61c859eaed3153d63715e4fd6f4bcd3f6b62cea4c85c1a6410669925ef3c73185f550eb28b
7
- data.tar.gz: 23ad7e64604e97d9c18a0b99b8917af0b97974f2c67d1dc842d16070650a3e97ef79e0902d96f4dd4f8e71f84bdc30d09811aa16fb61342bbdcacb6e7979b8fc
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 modular resource endpoints, configurable defaults, optional auto-pagination, and a clean error hierarchy.
3
+ A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-based client with configurable defaults, model-driven resource querying, and a clean error hierarchy.
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/toc_doc.svg)](https://badge.fury.io/rb/toc_doc)
6
6
  [![CI](https://github.com/01max/toc_doc/actions/workflows/main.yml/badge.svg)](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
- # Use the pre-configured module-level client …
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
- response.total # => 5
77
- response.next_slot # => "2026-02-28T10:00:00.000+01:00"
75
+ collection.total # => 5
76
+ collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
78
77
 
79
- response.availabilities.each do |avail|
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.availabilities(visit_motive_ids: 123, agenda_ids: 456)
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
- de_client = TocDoc::Client.new(api_endpoint: 'https://www.doctolib.de')
110
- it_client = TocDoc::Client.new(api_endpoint: 'https://www.doctolib.it', per_page: 3)
108
+ # Germany
109
+ TocDoc.configure { |c| c.api_endpoint = 'https://www.doctolib.de'; c.per_page = 3 }
110
+ TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
111
+
112
+ # Reset and switch to Italy
113
+ TocDoc.reset!
114
+ TocDoc.configure { |c| c.api_endpoint = 'https://www.doctolib.it' }
115
+ TocDoc::Availability.where(visit_motive_ids: 789, agenda_ids: 101)
116
+ ```
117
+
118
+ Alternatively, use `TocDoc::Client` directly for lower-level access ().
111
119
 
112
- de_client.availabilities(visit_motive_ids: 123, agenda_ids: 456)
113
- it_client.availabilities(visit_motive_ids: 789, agenda_ids: 101)
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 0.1.0` | `User-Agent` header sent with every request. |
130
+ | `user_agent` | `TocDoc Ruby Gem 1.2.0` | `User-Agent` header sent with every request. |
122
131
  | `default_media_type` | `application/json` | `Accept` and `Content-Type` headers. |
123
- | `per_page` | `5` | Default number of results returned per request, platform's max is currently at `15`. |
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
- client.availabilities(
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
- client.availabilities(
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::Response::Availability` (see [Response objects](#response-objects)).
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::Response::Availability`
190
+ ### `TocDoc::Availability::Collection`
182
191
 
183
- Returned by `#availabilities`.
192
+ Returned by `TocDoc::Availability.where`; also accessible via the `TocDoc.availabilities` module-level shortcut.
193
+ Implements `Enumerable`, yielding `TocDoc::Availability` instances that have at least one slot.
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
- | `#availabilities` | `Array<TocDoc::Availability>` | One entry per date. |
190
- | `#to_h` | `Hash` | Plain-hash representation including expanded availability entries. |
199
+ | `#each` | — | Yields each `TocDoc::Availability` that has at least one slot (excludes empty-slot dates). |
200
+ | `#raw_availabilities` | `Array<TocDoc::Availability>` | All date entries, including those with no slots. |
201
+ | `#to_h` | `Hash` | Plain-hash representation (only dates with slots in the `availabilities` key). |
191
202
 
192
203
  ### `TocDoc::Availability`
193
204
 
194
- Each element of `Response::Availability#availabilities`.
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
- response = TocDoc.availabilities(visit_motive_ids: 123, agenda_ids: 456)
216
+ collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
206
217
 
207
- response.total # => 5
208
- response.next_slot # => "2026-02-28T10:00:00.000+01:00"
218
+ collection.total # => 5
219
+ collection.next_slot # => "2026-02-28T10:00:00.000+01:00"
209
220
 
210
- response.availabilities.first.date # => #<Date: 2026-02-28>
211
- response.availabilities.first.slots # => [#<DateTime: 2026-02-28T10:00:00+01:00>, ...]
221
+ collection.first.date # => #<Date: 2026-02-28>
222
+ collection.first.slots # => [#<DateTime: 2026-02-28T10:00:00+01:00>, ...]
212
223
 
213
- response.to_h
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 paginated by `start_date` and `limit`.
226
- TocDoc can manage this automatically.
236
+ The Doctolib availability endpoint is window-based: each request returns up to
237
+ `limit` dates starting from `start_date`.
227
238
 
228
- ### Automatic pagination
239
+ ### Automatic next-slot resolution
229
240
 
230
- Set `auto_paginate: true` on the client (or at module level) to fetch all pages
231
- and have results merged into a single `Response::Availability` object:
241
+ `TocDoc::Availability.where` automatically follows `next_slot` once: if the
242
+ first API response contains a `next_slot` key (indicating no available slots in
243
+ the requested window), a second request is issued transparently from that date
244
+ before the collection is returned.
232
245
 
233
- ```ruby
234
- client = TocDoc::Client.new(auto_paginate: true, per_page: 5)
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
- all_slots = client.availabilities(
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
- all_slots.total # total across every page
243
- all_slots.availabilities # every date entry, concatenated
244
- ```
245
-
246
- Pagination stops automatically when the API returns `next_slot: null`.
247
-
248
- ### Module-level toggle
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.availabilities(visit_motive_ids: 0, agenda_ids: 0)
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/client/<resource>.rb` and define a module
323
- `TocDoc::Client::<Resource>` with your endpoint methods.
324
- 2. Call `get`/`post`/`paginate` (from `TocDoc::Connection`) to issue requests.
325
- 3. Create `lib/toc_doc/models/response/<resource>.rb` (and any model classes)
326
- inheriting from `TocDoc::Resource`.
327
- 4. Include the new module in `TocDoc::Client` (`lib/toc_doc/client.rb`).
328
- 5. Add corresponding specs under `spec/toc_doc/client/`.
334
+ 1. Create `lib/toc_doc/models/<resource>.rb` with a model class inheriting from
335
+ `TocDoc::Resource`. Add a class-level `.where` (or equivalent) query method
336
+ that calls `TocDoc.client.get` / `.post` to issue requests.
337
+ 2. If the endpoint is paginated, create
338
+ `lib/toc_doc/models/<resource>/collection.rb` with an `Enumerable` collection
339
+ class (see `TocDoc::Availability::Collection` for the pattern).
340
+ 3. Require the new files from `lib/toc_doc/models.rb`.
341
+ 4. Add specs under `spec/toc_doc/models/`.
329
342
 
330
343
  ### Generating documentation
331
344
 
data/TODO.md CHANGED
@@ -1,50 +1,38 @@
1
1
  # PLAN
2
2
 
3
- ### Extra Endpoints
4
- - [x] Identify additional endpoints
5
- - [ ] Prioritize implementation of resource modules for those endpoints
3
+ [POTENTIAL_ENDPOINTS][POTENTIAL_ENDPOINTS.md]
6
4
 
7
- ## 1.2
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
- ## 1.4
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.
@@ -3,7 +3,6 @@
3
3
  require 'toc_doc/core/configurable'
4
4
  require 'toc_doc/core/connection'
5
5
  require 'toc_doc/core/uri_utils'
6
- require 'toc_doc/client/availabilities'
7
6
 
8
7
  module TocDoc
9
8
  # The main entry-point for interacting with the Doctolib API.
@@ -16,18 +15,14 @@ module TocDoc
16
15
  # api_endpoint: 'https://www.doctolib.de',
17
16
  # per_page: 5
18
17
  # )
19
- # client.availabilities(visit_motive_ids: 123, agenda_ids: 456)
20
18
  #
21
19
  # @see TocDoc::Configurable
22
20
  # @see TocDoc::Connection
23
- # @see TocDoc::Client::Availabilities
24
21
  class Client
25
22
  include TocDoc::Configurable
26
23
  include TocDoc::Connection
27
24
  include TocDoc::UriUtils
28
25
 
29
- include TocDoc::Client::Availabilities
30
-
31
26
  # Creates a new client instance.
32
27
  #
33
28
  # Options are merged on top of the module-level {TocDoc::Default} values.
@@ -39,7 +34,6 @@ module TocDoc
39
34
  # @option options [String] :user_agent User-Agent header value
40
35
  # @option options [String] :default_media_type Accept / Content-Type header
41
36
  # @option options [Integer] :per_page Results per page
42
- # @option options [Boolean] :auto_paginate Follow pagination automatically
43
37
  # @option options [Faraday::RackBuilder] :middleware Custom Faraday middleware
44
38
  # @option options [Hash] :connection_options Additional Faraday options
45
39
  #
@@ -29,7 +29,6 @@ module TocDoc
29
29
  connection_options
30
30
  default_media_type
31
31
  per_page
32
- auto_paginate
33
32
  ].freeze
34
33
 
35
34
  # @!attribute [rw] api_endpoint
@@ -42,8 +41,6 @@ module TocDoc
42
41
  # @return [Hash] additional Faraday connection options
43
42
  # @!attribute [rw] default_media_type
44
43
  # @return [String] the Accept / Content-Type header value
45
- # @!attribute [rw] auto_paginate
46
- # @return [Boolean] whether to follow pagination automatically
47
44
  attr_accessor(*VALID_CONFIG_KEYS)
48
45
 
49
46
  # Set the number of results per page, clamped to
@@ -9,8 +9,9 @@ module TocDoc
9
9
  # (`get`, `post`, `put`, `patch`, `delete`, `head`), a memoised
10
10
  # Faraday connection, pagination support, and response tracking.
11
11
  #
12
- # All HTTP methods are **private** callers interact with them through
13
- # higher-level endpoint modules (e.g. {TocDoc::Client::Availabilities}).
12
+ # {#get} and {#paginate} are public so that model classes (e.g.
13
+ # {TocDoc::Availability}) can call them via `TocDoc.client`; all other
14
+ # HTTP verb methods remain private.
14
15
  #
15
16
  # @see TocDoc::Client
16
17
  module Connection
@@ -107,12 +108,11 @@ module TocDoc
107
108
 
108
109
  # Performs a paginated GET, accumulating results across pages.
109
110
  #
110
- # When {Configurable#auto_paginate} is disabled or no block is given,
111
- # behaves exactly like {#get}.
111
+ # Behaves exactly like {#get} when no block is given.
112
112
  #
113
- # When +auto_paginate+ is +true+ **and** a block is provided, the block is
114
- # yielded after every page fetch — including the first — with
115
- # +(accumulator, last_response)+. The block must:
113
+ # When a block is provided, it is yielded after every page fetch —
114
+ # including the first — with +(accumulator, last_response)+. The block
115
+ # must:
116
116
  #
117
117
  # 1. Detect whether it is a continuation call by comparing object identity:
118
118
  # `acc.equal?(last_response.body)` is `true` only on the first yield,
@@ -129,7 +129,7 @@ module TocDoc
129
129
  # @return [Object] the fully-accumulated response body
130
130
  def paginate(path, options = {}, &)
131
131
  data = get(path, options)
132
- return data unless block_given? && auto_paginate
132
+ return data unless block_given?
133
133
 
134
134
  loop do
135
135
  next_options = yield(data, last_response)
@@ -192,5 +192,7 @@ module TocDoc
192
192
 
193
193
  [query || {}, explicit_headers]
194
194
  end
195
+
196
+ public :get, :paginate
195
197
  end
196
198
  end
@@ -28,9 +28,6 @@ module TocDoc
28
28
  # @return [Integer] the hard upper limit for per_page
29
29
  MAX_PER_PAGE = 15
30
30
 
31
- # @return [Boolean] whether to auto-paginate by default
32
- AUTO_PAGINATE = false
33
-
34
31
  # @return [Integer] the default maximum number of retries
35
32
  MAX_RETRY = 3
36
33
 
@@ -45,7 +42,6 @@ module TocDoc
45
42
  user_agent: user_agent,
46
43
  default_media_type: default_media_type,
47
44
  per_page: per_page,
48
- auto_paginate: auto_paginate,
49
45
  middleware: middleware,
50
46
  connection_options: connection_options
51
47
  }
@@ -93,19 +89,6 @@ module TocDoc
93
89
  PER_PAGE
94
90
  end
95
91
 
96
- # Whether to follow pagination automatically.
97
- #
98
- # Falls back to the `TOCDOC_AUTO_PAGINATE` environment variable (set to
99
- # `"true"` to enable), then {AUTO_PAGINATE}.
100
- #
101
- # @return [Boolean]
102
- def auto_paginate
103
- env_val = ENV.fetch('TOCDOC_AUTO_PAGINATE', nil)
104
- return AUTO_PAGINATE if env_val.nil?
105
-
106
- env_val.casecmp('true').zero?
107
- end
108
-
109
92
  # The default Faraday middleware stack.
110
93
  #
111
94
  # Includes retry logic, error raising, JSON parsing, and the default
@@ -5,13 +5,13 @@ module TocDoc
5
5
  #
6
6
  # Doctolib expects certain ID list parameters to be dash-joined strings
7
7
  # rather than standard repeated/bracket array notation. Include this module
8
- # into endpoint modules and call +dashed_ids+ explicitly for each such param:
8
+ # and call +dashed_ids+ explicitly for each such param:
9
9
  #
10
- # module TocDoc::Client::Availabilities
11
- # include TocDoc::UriUtils
10
+ # class TocDoc::Availability
11
+ # extend TocDoc::UriUtils
12
12
  #
13
- # def availabilities(visit_motive_ids:, agenda_ids:, **opts)
14
- # get('/availabilities.json', query: {
13
+ # def self.where(visit_motive_ids:, agenda_ids:, **opts)
14
+ # client.get('/availabilities.json', query: {
15
15
  # visit_motive_ids: dashed_ids(visit_motive_ids),
16
16
  # agenda_ids: dashed_ids(agenda_ids),
17
17
  # **opts
@@ -4,5 +4,5 @@ module TocDoc
4
4
  # The current version of the TocDoc gem.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '1.1.0'
7
+ VERSION = '1.2.0'
8
8
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'toc_doc/models/availability'
5
+
6
+ module TocDoc
7
+ class Availability
8
+ # An Enumerable collection of {TocDoc::Availability} instances returned
9
+ # by {TocDoc::Availability.where}.
10
+ #
11
+ # @example Iterate over available slots
12
+ # collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
13
+ # collection.each { |avail| puts avail.date }
14
+ #
15
+ # @example Access metadata
16
+ # collection.total #=> 5
17
+ # collection.next_slot #=> "2026-02-28T10:00:00.000+01:00"
18
+ class Collection
19
+ include Enumerable
20
+
21
+ attr_reader :path, :query
22
+
23
+ # @param data [Hash] parsed first-page response body
24
+ # @param query [Hash] original query params (used to build next-page requests)
25
+ # @param path [String] API path for subsequent requests
26
+ def initialize(data, query: {}, path: '/availabilities.json')
27
+ @data = data.dup
28
+ @query = query
29
+ @path = path
30
+ end
31
+
32
+ # Iterates over {TocDoc::Availability} instances that have at least one slot.
33
+ #
34
+ # @yieldparam availability [TocDoc::Availability]
35
+ # @return [Enumerator] if no block given
36
+ def each(&)
37
+ filtered_entries.each(&)
38
+ end
39
+
40
+ # The total number of available slots in the collection.
41
+ #
42
+ # @return [Integer]
43
+ def total
44
+ @data['total']
45
+ end
46
+
47
+ # The nearest available appointment slot.
48
+ #
49
+ # Returns the +next_slot+ value from the API when present (which only
50
+ # occurs when none of the loaded dates have any slots). Otherwise
51
+ # returns the first slot of the first date that has one.
52
+ #
53
+ # @return [String, nil] ISO 8601 datetime string, or +nil+ when unavailable
54
+ def next_slot
55
+ return @data['next_slot'] if @data.key?('next_slot')
56
+
57
+ Array(@data['availabilities']).each do |entry|
58
+ slots = Array(entry['slots'])
59
+ return slots.first unless slots.empty?
60
+ end
61
+
62
+ nil
63
+ end
64
+
65
+ # All date entries — including those with no slots — as {TocDoc::Availability}
66
+ # objects.
67
+ #
68
+ # @return [Array<TocDoc::Availability>]
69
+ def raw_availabilities
70
+ Array(@data['availabilities']).map { |entry| TocDoc::Availability.new(entry) }
71
+ end
72
+
73
+ # Returns a plain Hash representation of the collection.
74
+ #
75
+ # The +availabilities+ key contains only dates with slots (filtered),
76
+ # serialised back to plain Hashes.
77
+ #
78
+ # @return [Hash{String => Object}]
79
+ def to_h
80
+ @data.merge('availabilities' => filtered_entries.map(&:to_h))
81
+ end
82
+
83
+ # Fetches the next window of availabilities (starting the day after the
84
+ # last date in the current collection) and merges them in.
85
+ #
86
+ # @param page_data [Hash] parsed response body to merge into this collection
87
+ # @return [self]
88
+ def merge_page!(page_data)
89
+ @data['availabilities'] = @data.fetch('availabilities', []) + page_data.fetch('availabilities', [])
90
+ @data['total'] = @data.fetch('total', 0) + page_data.fetch('total', 0)
91
+ self
92
+ end
93
+
94
+ private
95
+
96
+ def filtered_entries
97
+ Array(@data['availabilities'])
98
+ .select { |entry| Array(entry['slots']).any? }
99
+ .map { |entry| TocDoc::Availability.new(entry) }
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,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
- # @param attrs [Hash] raw attributes from the API response, expected to include
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)
@@ -2,5 +2,4 @@
2
2
 
3
3
  require 'toc_doc/models/resource'
4
4
  require 'toc_doc/models/availability'
5
-
6
- require 'toc_doc/models/response/availability'
5
+ require 'toc_doc/models/availability/collection'
data/lib/toc_doc.rb CHANGED
@@ -16,13 +16,9 @@ require 'toc_doc/client'
16
16
  # Configuration can be set at the module level and will be inherited by every
17
17
  # {TocDoc::Client} instance created via {.client} or {.setup}.
18
18
  #
19
- # Any method available on {TocDoc::Client} can be called directly on `TocDoc`
20
- # and will be forwarded to the memoized {.client}.
21
- #
22
19
  # @example Quick start
23
20
  # TocDoc.setup do |config|
24
21
  # config.api_endpoint = 'https://www.doctolib.de'
25
- # config.auto_paginate = true
26
22
  # end
27
23
  #
28
24
  # TocDoc.availabilities(
@@ -70,6 +66,16 @@ module TocDoc
70
66
  client
71
67
  end
72
68
 
69
+ # Returns available appointment slots.
70
+ #
71
+ # Delegates to {TocDoc::Availability.where} — see that method for full
72
+ # parameter documentation.
73
+ #
74
+ # @return [TocDoc::Availability::Collection]
75
+ def availabilities(**)
76
+ TocDoc::Availability.where(**)
77
+ end
78
+
73
79
  # @!visibility private
74
80
  def method_missing(method_name, ...)
75
81
  if client.respond_to?(method_name)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toc_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
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