toc_doc 1.4.0 → 1.6.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 +30 -0
- data/POTENTIAL_ENDPOINTS.md +4 -4
- data/README.md +107 -4
- data/TODO.md +51 -14
- data/lib/toc_doc/core/configurable.rb +19 -1
- data/lib/toc_doc/core/connection.rb +2 -1
- data/lib/toc_doc/core/default.rb +54 -17
- data/lib/toc_doc/core/error.rb +75 -1
- data/lib/toc_doc/core/version.rb +1 -1
- data/lib/toc_doc/http/middleware/raise_error.rb +67 -10
- data/lib/toc_doc/models/agenda.rb +21 -0
- data/lib/toc_doc/models/availability.rb +2 -0
- data/lib/toc_doc/models/booking_info.rb +130 -0
- data/lib/toc_doc/models/place.rb +6 -0
- data/lib/toc_doc/models/profile/practitioner.rb +7 -1
- data/lib/toc_doc/models/profile.rb +72 -8
- data/lib/toc_doc/models/search.rb +5 -0
- data/lib/toc_doc/models/visit_motive.rb +18 -0
- data/lib/toc_doc/models.rb +3 -0
- data/lib/toc_doc.rb +11 -0
- metadata +4 -2
- data/lib/toc_doc/http/response/base_middleware.rb +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50818c97b021143ebf2d502a867fb5f20a90299a822ec800b7d02ae4d20999eb
|
|
4
|
+
data.tar.gz: d4ef919a143957c2685fba1f28675786743922387b0703c521a6d5c0b9ab76c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9aa3dd68984a7cec38789a3a718db4f7c9db9a0335ae8e47744f24990dccb73de12e41903aee8806e06f579288769c13a5b9f82327a031dd3c2052d096ae33e5
|
|
7
|
+
data.tar.gz: 10725265d2117c4d93720c89f5c06360ea567377c6d21f9a848e26151de7039876ac9a7b1da936801f0eaf9a7b8141689e8389b5a2917f3d550a01847aa52767
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.6.0] - 2026-03-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Timeout configuration** — two new config keys: `connect_timeout` (TCP connect, default: `5`, env: `TOCDOC_CONNECT_TIMEOUT`) and `read_timeout` (response read, default: `10`, env: `TOCDOC_READ_TIMEOUT`); passed to Faraday as `open_timeout` and `timeout` respectively
|
|
8
|
+
- **`per_page` guard** — `TocDoc::Configurable` now emits a warning when `per_page` exceeds the maximum value the Doctolib API can handle
|
|
9
|
+
- **`TocDoc::Error` hierarchy** — structured error subclasses; `TocDoc::ResponseError` carries `status`, `body`, and `headers`; specific subclasses: `BadRequest` (400), `NotFound` (404), `UnprocessableEntity` (422), `TooManyRequests` (429), `ClientError` (other 4xx), `ServerError` (5xx); `TocDoc::ConnectionError` raised on network/transport failures
|
|
10
|
+
- **`TocDoc::Middleware::RaiseError`** — reworked to map HTTP error codes to structured `TocDoc::Error` subclasses; wraps Faraday transport errors (`TimeoutError`, `ConnectionFailed`, `SSLError`) into `TocDoc::ConnectionError`
|
|
11
|
+
- **`Default.reset!`** — class method to reset memoized middleware defaults, preventing middleware stack leak between configurations
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Max retry** — retry limit logic moved from `BaseMiddleware` into `TocDoc::Default`, eliminating the `BaseMiddleware` class; retry count now derived solely from `Default::MAX_RETRY`
|
|
16
|
+
|
|
17
|
+
## [1.5.0] - 2026-03-28
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`TocDoc::BookingInfo`** — new envelope class for the slot-selection funnel info endpoint (`/online_booking/api/slot_selection_funnel/v1/info.json`); `BookingInfo.find(identifier)` fetches booking context by profile slug or numeric ID and returns a typed `BookingInfo` instance
|
|
22
|
+
- **`TocDoc::BookingInfo#profile`** — returns the typed profile (`Practitioner` or `Organization`) via `Profile.build`
|
|
23
|
+
- **`TocDoc::BookingInfo#specialities`** — returns an array of `TocDoc::Speciality` objects
|
|
24
|
+
- **`TocDoc::BookingInfo#visit_motives`** — returns an array of `TocDoc::VisitMotive` objects
|
|
25
|
+
- **`TocDoc::BookingInfo#agendas`** — returns an array of `TocDoc::Agenda` objects, each pre-resolved with its matching `VisitMotive` objects via `visit_motive_ids`
|
|
26
|
+
- **`TocDoc::BookingInfo#places`** — returns an array of `TocDoc::Place` objects
|
|
27
|
+
- **`TocDoc::BookingInfo#practitioners`** — returns an array of `TocDoc::Profile::Practitioner` objects (marked `partial: true`)
|
|
28
|
+
- **`TocDoc::BookingInfo#organization?`** — delegates to the inner profile
|
|
29
|
+
- **`TocDoc::VisitMotive`** — new `Resource`-based model representing a visit motive (reason for consultation); exposes `#id` and `#name` via dot-notation
|
|
30
|
+
- **`TocDoc::Agenda`** — new `Resource`-based model representing an agenda (calendar); exposes `#id` and `#practice_id` via dot-notation; supports pre-resolved `#visit_motives` when built through `BookingInfo`
|
|
31
|
+
- **`TocDoc.booking_info`** — top-level shortcut delegating to `TocDoc::BookingInfo.find`
|
|
32
|
+
|
|
3
33
|
## [1.4.0] - 2026-03-19
|
|
4
34
|
|
|
5
35
|
### Added
|
data/POTENTIAL_ENDPOINTS.md
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
- Interesting JSONs [GET]
|
|
4
4
|
- [x] Availabilities
|
|
5
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
|
|
6
|
+
- [x] Practitioner
|
|
7
7
|
- https://www.doctolib.fr/pharmacie/paris/pharmacie-faidherbe.json
|
|
8
8
|
- https://www.doctolib.fr/dentiste/bordeaux/mathilde-devun-lesparre-medoc.json
|
|
9
9
|
- https://www.doctolib.fr/a/b/mathilde-devun-lesparre-medoc.json
|
|
10
10
|
- https://www.doctolib.fr/profiles/mathilde-devun-lesparre-medoc.json
|
|
11
|
-
- Rassemblement practiciens / Place Practitioners collection
|
|
11
|
+
- [x] Rassemblement practiciens / Place Practitioners collection
|
|
12
12
|
- https://www.doctolib.fr/profiles/pavillon-de-la-mutualite-bordeaux-rue-vital-carles.json
|
|
13
|
-
- Places
|
|
13
|
+
- [ ] Places
|
|
14
14
|
- https://www.doctolib.fr/patient_app/place_autocomplete.json?query=47300
|
|
15
|
-
- Booking context
|
|
15
|
+
- [x] Booking context
|
|
16
16
|
- https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=brigitte-devun-pujols&locale=fr
|
|
17
17
|
- https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388&locale=fr
|
|
18
18
|
- Interesting NON JSONs
|
data/README.md
CHANGED
|
@@ -29,6 +29,7 @@ A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-b
|
|
|
29
29
|
- [Availabilities](#availabilities)
|
|
30
30
|
- [Search](#search)
|
|
31
31
|
- [Profile](#profile)
|
|
32
|
+
- [BookingInfo](#bookinginfo)
|
|
32
33
|
5. [Response objects](#response-objects)
|
|
33
34
|
6. [Pagination](#pagination)
|
|
34
35
|
7. [Error handling](#error-handling)
|
|
@@ -60,6 +61,16 @@ or install it directly:
|
|
|
60
61
|
gem install toc_doc
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
### Alternative: gem.coop
|
|
65
|
+
|
|
66
|
+
If you prefer to install from [gem.coop](https://gem.coop), toc_doc is also [available there](https://beta.gem.coop/@maxime/toc_doc) :
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
source 'https://gem.coop' do
|
|
70
|
+
gem 'toc_doc'
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
63
74
|
---
|
|
64
75
|
|
|
65
76
|
## Quick start
|
|
@@ -129,9 +140,11 @@ client.get('/availabilities.json', query: { visit_motive_ids: '123', agenda_ids:
|
|
|
129
140
|
| Option | Default | Description |
|
|
130
141
|
|---|---|---|
|
|
131
142
|
| `api_endpoint` | `https://www.doctolib.fr` | Base URL. Change to `.de` / `.it` for other countries. |
|
|
132
|
-
| `user_agent` | `TocDoc Ruby Gem 1.
|
|
143
|
+
| `user_agent` | `TocDoc Ruby Gem 1.6.0` | `User-Agent` header sent with every request. |
|
|
133
144
|
| `default_media_type` | `application/json` | `Accept` and `Content-Type` headers. |
|
|
134
|
-
| `per_page` | `15` | Default number of availability dates per request (capped at `15`). |
|
|
145
|
+
| `per_page` | `15` | Default number of availability dates per request (capped at `15`). Emits a warning if the value exceeds the cap. |
|
|
146
|
+
| `connect_timeout` | `5` | TCP connect timeout in seconds, passed to Faraday as `open_timeout`. Override via `TOCDOC_CONNECT_TIMEOUT`. |
|
|
147
|
+
| `read_timeout` | `10` | Response read timeout in seconds, passed to Faraday as `timeout`. Override via `TOCDOC_READ_TIMEOUT`. |
|
|
135
148
|
| `middleware` | Retry + RaiseError + JSON + adapter | Full Faraday middleware stack. Override to customise completely. |
|
|
136
149
|
| `connection_options` | `{}` | Options passed directly to `Faraday.new`. |
|
|
137
150
|
|
|
@@ -145,6 +158,8 @@ All primary options can be set via environment variables before the gem is loade
|
|
|
145
158
|
| `TOCDOC_USER_AGENT` | `user_agent` |
|
|
146
159
|
| `TOCDOC_MEDIA_TYPE` | `default_media_type` |
|
|
147
160
|
| `TOCDOC_PER_PAGE` | `per_page` |
|
|
161
|
+
| `TOCDOC_CONNECT_TIMEOUT` | `connect_timeout` (default `5`) |
|
|
162
|
+
| `TOCDOC_READ_TIMEOUT` | `read_timeout` (default `10`) |
|
|
148
163
|
| `TOCDOC_RETRY_MAX` | Maximum Faraday retry attempts (default `3`) |
|
|
149
164
|
|
|
150
165
|
---
|
|
@@ -241,6 +256,35 @@ profile.places.first.coordinates # => [44.8386722, -0.5780466]
|
|
|
241
256
|
|
|
242
257
|
**Return value:** a `TocDoc::Profile::Practitioner` or `TocDoc::Profile::Organization` (see [Response objects](#response-objects)).
|
|
243
258
|
|
|
259
|
+
### BookingInfo
|
|
260
|
+
|
|
261
|
+
Fetch the slot-selection funnel context for a practitioner or organization by slug or numeric ID.
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
# by slug
|
|
265
|
+
info = TocDoc::BookingInfo.find('jane-doe-bordeaux')
|
|
266
|
+
|
|
267
|
+
# by numeric ID
|
|
268
|
+
info = TocDoc::BookingInfo.find(1_542_899)
|
|
269
|
+
|
|
270
|
+
# module-level shortcut
|
|
271
|
+
info = TocDoc.booking_info('jane-doe-bordeaux')
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`BookingInfo.find` hits `/online_booking/api/slot_selection_funnel/v1/info.json` and returns a `TocDoc::BookingInfo` instance containing the full booking context needed to drive the appointment-booking funnel.
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
info.profile # => #<TocDoc::Profile::Practitioner ...>
|
|
278
|
+
info.specialities # => [#<TocDoc::Speciality ...>, ...]
|
|
279
|
+
info.visit_motives # => [#<TocDoc::VisitMotive id=..., name="...">, ...]
|
|
280
|
+
info.agendas # => [#<TocDoc::Agenda id=..., practice_id=...>, ...]
|
|
281
|
+
info.places # => [#<TocDoc::Place ...>, ...]
|
|
282
|
+
info.practitioners # => [#<TocDoc::Profile::Practitioner partial=true>, ...]
|
|
283
|
+
info.organization? # => false
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Return value:** a `TocDoc::BookingInfo` (see [Response objects](#response-objects)).
|
|
287
|
+
|
|
244
288
|
---
|
|
245
289
|
|
|
246
290
|
## Response objects
|
|
@@ -337,6 +381,39 @@ Represents a practice location returned inside a full profile response. Inherits
|
|
|
337
381
|
| `#formal_name` | `String \| nil` | Formal practice name, if available. |
|
|
338
382
|
| `#coordinates` | `Array<Float>` | Convenience method returning `[latitude, longitude]`. |
|
|
339
383
|
|
|
384
|
+
### `TocDoc::BookingInfo`
|
|
385
|
+
|
|
386
|
+
Returned by `TocDoc::BookingInfo.find`; also accessible via the `TocDoc.booking_info` module-level shortcut.
|
|
387
|
+
|
|
388
|
+
| Method | Type | Description |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| `#profile` | `Profile::Practitioner \| Profile::Organization` | Typed profile built via `Profile.build`. |
|
|
391
|
+
| `#specialities` | `Array<TocDoc::Speciality>` | Specialities associated with the booking context. |
|
|
392
|
+
| `#visit_motives` | `Array<TocDoc::VisitMotive>` | Available visit motives (reasons for consultation). |
|
|
393
|
+
| `#agendas` | `Array<TocDoc::Agenda>` | Agendas, each pre-resolved with their matching `VisitMotive` objects. |
|
|
394
|
+
| `#places` | `Array<TocDoc::Place>` | Practice locations. |
|
|
395
|
+
| `#practitioners` | `Array<TocDoc::Profile::Practitioner>` | Practitioners associated with this booking context (`partial: true`). |
|
|
396
|
+
| `#organization?` | `Boolean` | Delegates to the inner profile. |
|
|
397
|
+
|
|
398
|
+
### `TocDoc::VisitMotive`
|
|
399
|
+
|
|
400
|
+
Represents a visit motive (reason for consultation) returned inside a `BookingInfo` response. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
401
|
+
|
|
402
|
+
| Method | Type | Description |
|
|
403
|
+
|---|---|---|
|
|
404
|
+
| `#id` | `Integer` | Visit motive identifier. |
|
|
405
|
+
| `#name` | `String` | Human-readable name of the visit motive. |
|
|
406
|
+
|
|
407
|
+
### `TocDoc::Agenda`
|
|
408
|
+
|
|
409
|
+
Represents an agenda (calendar) returned inside a `BookingInfo` response. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
410
|
+
|
|
411
|
+
| Method | Type | Description |
|
|
412
|
+
|---|---|---|
|
|
413
|
+
| `#id` | `Integer` | Agenda identifier. |
|
|
414
|
+
| `#practice_id` | `Integer` | ID of the associated practice. |
|
|
415
|
+
| `#visit_motives` | `Array<TocDoc::VisitMotive>` | Visit motives pre-resolved via `visit_motive_ids` when built through `BookingInfo`. |
|
|
416
|
+
|
|
340
417
|
### `TocDoc::Speciality`
|
|
341
418
|
|
|
342
419
|
Represents a speciality returned by the autocomplete endpoint. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
@@ -411,8 +488,34 @@ rescue TocDoc::Error => e
|
|
|
411
488
|
end
|
|
412
489
|
```
|
|
413
490
|
|
|
414
|
-
|
|
415
|
-
|
|
491
|
+
### Error classes
|
|
492
|
+
|
|
493
|
+
| Class | Raised when |
|
|
494
|
+
|---|---|
|
|
495
|
+
| `TocDoc::Error` | Base class for all TocDoc errors. |
|
|
496
|
+
| `TocDoc::ConnectionError` | Network/transport failure before a response is received (DNS, timeout, SSL). The original cause is available via `#cause`. |
|
|
497
|
+
| `TocDoc::ResponseError` | HTTP response received but indicates an error. Carries `#status`, `#body`, and `#headers`. |
|
|
498
|
+
| `TocDoc::ClientError` | 4xx response not matched by a more specific class. |
|
|
499
|
+
| `TocDoc::BadRequest` | HTTP 400. |
|
|
500
|
+
| `TocDoc::NotFound` | HTTP 404. |
|
|
501
|
+
| `TocDoc::UnprocessableEntity` | HTTP 422. |
|
|
502
|
+
| `TocDoc::TooManyRequests` | HTTP 429. |
|
|
503
|
+
| `TocDoc::ServerError` | 5xx response. |
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
begin
|
|
507
|
+
TocDoc::Profile.find('unknown-slug')
|
|
508
|
+
rescue TocDoc::NotFound => e
|
|
509
|
+
puts e.status # => 404
|
|
510
|
+
puts e.body # => '{"error":"not found"}'
|
|
511
|
+
rescue TocDoc::ConnectionError => e
|
|
512
|
+
puts e.cause # original Faraday exception
|
|
513
|
+
rescue TocDoc::ServerError => e
|
|
514
|
+
puts "Server error: #{e.status}"
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The default middleware stack includes a `Faraday::Retry::Middleware` that automatically
|
|
416
519
|
retries (up to 3 times, with exponential back-off) on:
|
|
417
520
|
|
|
418
521
|
- `429 Too Many Requests`
|
data/TODO.md
CHANGED
|
@@ -2,31 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
[POTENTIAL_ENDPOINTS][POTENTIAL_ENDPOINTS.md]
|
|
4
4
|
|
|
5
|
-
## 1.5
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
- https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388
|
|
6
|
+
## 1.7 — DevX
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
- [ ] Logging middleware (`:logger` config key)
|
|
9
|
+
- [ ] Resource: `define_singleton_method` on first access + `#attribute_names`
|
|
10
|
+
- [ ] Deep `to_h` and `to_json` on `Resource` and `BookingInfo`
|
|
11
|
+
- [ ] `Collection#filtered_entries` memoization
|
|
12
|
+
- [ ] `BookingInfo#agendas` O(n*m) → hash lookup
|
|
13
|
+
|
|
14
|
+
## 1.8 — HTTP Layer Robustness
|
|
15
|
+
|
|
16
|
+
- [ ] Configurable availability pagination depth + `Collection#more?` / `#fetch_next_page`
|
|
17
|
+
- [ ] Client-side rate limiter (token-bucket middleware)
|
|
18
|
+
- [ ] Optional response caching (memory or ActiveSupport-compatible store)
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
- [ ] Rate limiting
|
|
14
|
-
- [ ] Caching
|
|
15
|
-
- [ ] Logging
|
|
20
|
+
## 1.9 — Service Layer rework
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
- [ ] `TocDoc::Services::Availabilities` — extract from `Availability.where`
|
|
23
|
+
- [ ] `TocDoc::Services::Profiles` — extract from `Profile.find`
|
|
24
|
+
- [ ] `TocDoc::Services::Search` — extract from `Search.where`
|
|
25
|
+
- [ ] `TocDoc::Services::BookingInfos` — extract from `BookingInfo.find`
|
|
26
|
+
- [ ] Update top-level shortcuts to delegate to services
|
|
27
|
+
- [ ] Deprecation on old model-level finders
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- [ ]
|
|
22
|
-
- [ ]
|
|
29
|
+
## 2.0 — Breaking Changes
|
|
30
|
+
|
|
31
|
+
- [ ] Remove deprecated model-level finders
|
|
32
|
+
- [ ] `Place#coordinates` as `Data.define(:latitude, :longitude)`
|
|
33
|
+
- [ ] Integration smoke test suite (gated by ENV, weekly CI cron)
|
|
34
|
+
- [ ] Remove unused HTTP verbs from `Connection` (if auth doesn't ship)
|
|
23
35
|
|
|
24
36
|
# ???
|
|
25
37
|
|
|
26
38
|
- [ ] Figure what is `organization_statuses` in the autocomplete endpoint and what to do with it.
|
|
39
|
+
- [ ] Authentication module
|
|
40
|
+
- [ ] Place autocomplete endpoint ? (`/patient_app/place_autocomplete.json`)
|
|
27
41
|
|
|
28
42
|
# DONE & RELEASED
|
|
29
43
|
|
|
44
|
+
## 1.6
|
|
45
|
+
|
|
46
|
+
### Default connection timeouts
|
|
47
|
+
- [x] Add `CONNECT_TIMEOUT` and `READ_TIMEOUT` constants to `Default`
|
|
48
|
+
- [x] Add config keys and ENV overrides (`TOCDOC_CONNECT_TIMEOUT`, `TOCDOC_READ_TIMEOUT`)
|
|
49
|
+
- [x] Wire into `Connection#faraday_options`
|
|
50
|
+
|
|
51
|
+
### Error hierarchy with HTTP context
|
|
52
|
+
- [x] Build error subclass tree (`ConnectionError`, `ResponseError`, `ClientError`, `NotFound`, `TooManyRequests`, `ServerError`)
|
|
53
|
+
- [x] Rewrite `RaiseError` middleware (`on_complete` pattern, status → subclass mapping)
|
|
54
|
+
- [x] Remove `Faraday::Response::RaiseError` from middleware stack
|
|
55
|
+
|
|
56
|
+
### Fixes
|
|
57
|
+
- [x] Fix middleware memoization leak (`Default.reset!`)
|
|
58
|
+
- [x] Warn on silent `per_page` clamping
|
|
59
|
+
- [x] Remove dead code (`base_middleware.rb`, `retry_options_fallback` duplication)
|
|
60
|
+
|
|
61
|
+
## 1.5
|
|
62
|
+
|
|
63
|
+
- [x] Booking context
|
|
64
|
+
- practitioner : https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388
|
|
65
|
+
- organization : https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=325629
|
|
66
|
+
|
|
30
67
|
## 1.4
|
|
31
68
|
|
|
32
69
|
- [x] Profile
|
|
@@ -101,4 +138,4 @@
|
|
|
101
138
|
- [x] on rubygem
|
|
102
139
|
- [x] release on GH
|
|
103
140
|
- [x] gem.coop/@maxime
|
|
104
|
-
- [x] Add test coverage tool
|
|
141
|
+
- [x] Add test coverage tool
|
|
@@ -29,6 +29,8 @@ module TocDoc
|
|
|
29
29
|
connection_options
|
|
30
30
|
default_media_type
|
|
31
31
|
per_page
|
|
32
|
+
connect_timeout
|
|
33
|
+
read_timeout
|
|
32
34
|
].freeze
|
|
33
35
|
|
|
34
36
|
# @!attribute [rw] api_endpoint
|
|
@@ -41,15 +43,26 @@ module TocDoc
|
|
|
41
43
|
# @return [Hash] additional Faraday connection options
|
|
42
44
|
# @!attribute [rw] default_media_type
|
|
43
45
|
# @return [String] the Accept / Content-Type header value
|
|
46
|
+
# @!attribute [rw] connect_timeout
|
|
47
|
+
# @return [Integer] TCP connect timeout in seconds
|
|
48
|
+
# @!attribute [rw] read_timeout
|
|
49
|
+
# @return [Integer] read (response) timeout in seconds
|
|
44
50
|
attr_accessor(*VALID_CONFIG_KEYS)
|
|
45
51
|
|
|
46
52
|
# Set the number of results per page, clamped to
|
|
47
53
|
# {TocDoc::Default::MAX_PER_PAGE}.
|
|
48
54
|
#
|
|
55
|
+
# Emits a warning on +$stderr+ when +value+ exceeds the hard cap so callers
|
|
56
|
+
# are not silently surprised by the lower effective value.
|
|
57
|
+
#
|
|
49
58
|
# @param value [Integer, #to_i] desired page size
|
|
50
59
|
# @return [Integer] the effective page size after clamping
|
|
51
60
|
def per_page=(value)
|
|
52
|
-
|
|
61
|
+
int = value.to_i
|
|
62
|
+
if int > TocDoc::Default::MAX_PER_PAGE
|
|
63
|
+
warn "[TocDoc] per_page #{int} exceeds MAX_PER_PAGE (#{TocDoc::Default::MAX_PER_PAGE}); clamped."
|
|
64
|
+
end
|
|
65
|
+
@per_page = [int, TocDoc::Default::MAX_PER_PAGE].min
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
# Returns the list of recognised configurable attribute names.
|
|
@@ -76,8 +89,13 @@ module TocDoc
|
|
|
76
89
|
|
|
77
90
|
# Reset all configuration options to their {TocDoc::Default} values.
|
|
78
91
|
#
|
|
92
|
+
# Calls {TocDoc::Default.reset!} first so that memoized values such as
|
|
93
|
+
# {TocDoc::Default.middleware} are cleared and rebuilt fresh on the next
|
|
94
|
+
# access, preventing stale middleware stacks from being reused.
|
|
95
|
+
#
|
|
79
96
|
# @return [self]
|
|
80
97
|
def reset!
|
|
98
|
+
TocDoc::Default.reset!
|
|
81
99
|
TocDoc::Default.options.each do |key, value|
|
|
82
100
|
public_send("#{key}=", value)
|
|
83
101
|
end
|
|
@@ -24,7 +24,7 @@ module TocDoc
|
|
|
24
24
|
|
|
25
25
|
# Perform a GET request.
|
|
26
26
|
#
|
|
27
|
-
# @param path [String] API path (relative to {Configurable#api_endpoint})
|
|
27
|
+
# @param path [String] API path (relative to {TocDoc::Configurable#api_endpoint})
|
|
28
28
|
# @param options [Hash] query / header options forwarded to {#request}
|
|
29
29
|
# @return [Object] parsed response body
|
|
30
30
|
def get(path, options = {})
|
|
@@ -93,6 +93,7 @@ module TocDoc
|
|
|
93
93
|
def faraday_options
|
|
94
94
|
opts = connection_options.dup
|
|
95
95
|
opts[:builder] = middleware if middleware
|
|
96
|
+
opts[:request] = { timeout: read_timeout, open_timeout: connect_timeout }
|
|
96
97
|
opts
|
|
97
98
|
end
|
|
98
99
|
|
data/lib/toc_doc/core/default.rb
CHANGED
|
@@ -31,6 +31,12 @@ module TocDoc
|
|
|
31
31
|
# @return [Integer] the default maximum number of retries
|
|
32
32
|
MAX_RETRY = 3
|
|
33
33
|
|
|
34
|
+
# @return [Integer] the default TCP connect timeout in seconds
|
|
35
|
+
CONNECT_TIMEOUT = 5
|
|
36
|
+
|
|
37
|
+
# @return [Integer] the default read (response) timeout in seconds
|
|
38
|
+
READ_TIMEOUT = 10
|
|
39
|
+
|
|
34
40
|
class << self
|
|
35
41
|
# Returns a hash of all default configuration values, suitable for
|
|
36
42
|
# passing to {TocDoc::Configurable#reset!}.
|
|
@@ -43,7 +49,9 @@ module TocDoc
|
|
|
43
49
|
default_media_type: default_media_type,
|
|
44
50
|
per_page: per_page,
|
|
45
51
|
middleware: middleware,
|
|
46
|
-
connection_options: connection_options
|
|
52
|
+
connection_options: connection_options,
|
|
53
|
+
connect_timeout: connect_timeout,
|
|
54
|
+
read_timeout: read_timeout
|
|
47
55
|
}
|
|
48
56
|
end
|
|
49
57
|
|
|
@@ -91,8 +99,9 @@ module TocDoc
|
|
|
91
99
|
|
|
92
100
|
# The default Faraday middleware stack.
|
|
93
101
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
102
|
+
# Stack order (outermost first): RaiseError, retry, JSON parsing, adapter.
|
|
103
|
+
# RaiseError is outermost so it wraps retry and maps the final response or
|
|
104
|
+
# re-raised transport exception into a typed {TocDoc::Error}.
|
|
96
105
|
#
|
|
97
106
|
# @return [Faraday::RackBuilder]
|
|
98
107
|
def middleware
|
|
@@ -106,13 +115,48 @@ module TocDoc
|
|
|
106
115
|
@connection_options ||= {}
|
|
107
116
|
end
|
|
108
117
|
|
|
118
|
+
# The TCP connect timeout in seconds.
|
|
119
|
+
#
|
|
120
|
+
# Falls back to the `TOCDOC_CONNECT_TIMEOUT` environment variable, then
|
|
121
|
+
# {CONNECT_TIMEOUT}. Invalid ENV values fall back to {CONNECT_TIMEOUT}.
|
|
122
|
+
#
|
|
123
|
+
# @return [Integer]
|
|
124
|
+
def connect_timeout
|
|
125
|
+
Integer(ENV.fetch('TOCDOC_CONNECT_TIMEOUT', CONNECT_TIMEOUT), 10)
|
|
126
|
+
rescue ArgumentError
|
|
127
|
+
CONNECT_TIMEOUT
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The read (response) timeout in seconds.
|
|
131
|
+
#
|
|
132
|
+
# Falls back to the `TOCDOC_READ_TIMEOUT` environment variable, then
|
|
133
|
+
# {READ_TIMEOUT}. Invalid ENV values fall back to {READ_TIMEOUT}.
|
|
134
|
+
#
|
|
135
|
+
# @return [Integer]
|
|
136
|
+
def read_timeout
|
|
137
|
+
Integer(ENV.fetch('TOCDOC_READ_TIMEOUT', READ_TIMEOUT), 10)
|
|
138
|
+
rescue ArgumentError
|
|
139
|
+
READ_TIMEOUT
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Clears all memoized values so the next call to {.middleware} and
|
|
143
|
+
# {.connection_options} rebuilds them from scratch.
|
|
144
|
+
#
|
|
145
|
+
# Called by {TocDoc::Configurable#reset!} to ensure each reset produces a
|
|
146
|
+
# fresh middleware stack rather than reusing a stale memoized instance.
|
|
147
|
+
#
|
|
148
|
+
# @return [void]
|
|
149
|
+
def reset!
|
|
150
|
+
@middleware = nil
|
|
151
|
+
@connection_options = nil
|
|
152
|
+
end
|
|
153
|
+
|
|
109
154
|
private
|
|
110
155
|
|
|
111
156
|
def build_middleware
|
|
112
157
|
Faraday::RackBuilder.new do |builder|
|
|
113
|
-
builder.request :retry, retry_options
|
|
114
158
|
builder.use TocDoc::Middleware::RaiseError
|
|
115
|
-
builder.
|
|
159
|
+
builder.request :retry, retry_options
|
|
116
160
|
builder.response :json, content_type: /\bjson$/
|
|
117
161
|
builder.adapter Faraday.default_adapter
|
|
118
162
|
end
|
|
@@ -120,26 +164,19 @@ module TocDoc
|
|
|
120
164
|
|
|
121
165
|
def retry_options
|
|
122
166
|
{
|
|
123
|
-
max:
|
|
167
|
+
max: retry_max,
|
|
124
168
|
interval: 0.5,
|
|
125
169
|
interval_randomness: 0.5,
|
|
126
170
|
backoff_factor: 2,
|
|
127
171
|
retry_statuses: [429, 500, 502, 503, 504],
|
|
128
172
|
methods: %i[get head options]
|
|
129
173
|
}
|
|
130
|
-
rescue ArgumentError
|
|
131
|
-
retry_options_fallback
|
|
132
174
|
end
|
|
133
175
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
interval_randomness: 0.5,
|
|
139
|
-
backoff_factor: 2,
|
|
140
|
-
retry_statuses: [429, 500, 502, 503, 504],
|
|
141
|
-
methods: %i[get head options]
|
|
142
|
-
}
|
|
176
|
+
def retry_max
|
|
177
|
+
Integer(ENV.fetch('TOCDOC_RETRY_MAX', MAX_RETRY), 10)
|
|
178
|
+
rescue ArgumentError
|
|
179
|
+
MAX_RETRY
|
|
143
180
|
end
|
|
144
181
|
end
|
|
145
182
|
end
|
data/lib/toc_doc/core/error.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module TocDoc
|
|
4
4
|
# Base error class for all TocDoc errors.
|
|
5
5
|
#
|
|
6
|
-
# Inherits from +StandardError+ so consumers can rescue
|
|
6
|
+
# Inherits from +StandardError+ so consumers can rescue +TocDoc::Error+
|
|
7
7
|
# without any knowledge of Faraday or other internal HTTP details.
|
|
8
8
|
#
|
|
9
9
|
# @example Rescuing TocDoc errors
|
|
@@ -13,4 +13,78 @@ module TocDoc
|
|
|
13
13
|
# puts e.message
|
|
14
14
|
# end
|
|
15
15
|
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Raised when a network-level failure occurs before an HTTP response is
|
|
18
|
+
# received (e.g. DNS resolution failure, connection refused, timeout).
|
|
19
|
+
#
|
|
20
|
+
# The original low-level exception is available via Ruby's built-in
|
|
21
|
+
# +#cause+ mechanism — no extra attribute needed.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# rescue TocDoc::ConnectionError => e
|
|
25
|
+
# puts e.cause # original Faraday::ConnectionFailed, etc.
|
|
26
|
+
class ConnectionError < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when an HTTP response is received but indicates an error.
|
|
29
|
+
#
|
|
30
|
+
# Carries the raw response details so callers can act on them without
|
|
31
|
+
# needing to reach into Faraday internals.
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# rescue TocDoc::ResponseError => e
|
|
35
|
+
# puts e.status # => 404
|
|
36
|
+
# puts e.body # => '{"error":"not found"}'
|
|
37
|
+
# puts e.headers # => {"content-type"=>"application/json"}
|
|
38
|
+
class ResponseError < Error
|
|
39
|
+
# @return [Integer] the HTTP status code
|
|
40
|
+
attr_reader :status
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] the raw response body
|
|
43
|
+
attr_reader :body
|
|
44
|
+
|
|
45
|
+
# @return [Hash, nil] the response headers
|
|
46
|
+
attr_reader :headers
|
|
47
|
+
|
|
48
|
+
# @param status [Integer] the HTTP status code
|
|
49
|
+
# @param body [String, nil] the raw response body
|
|
50
|
+
# @param headers [Hash, nil] the response headers
|
|
51
|
+
# @param message [String, nil] optional override for the error message;
|
|
52
|
+
# defaults to +"HTTP #{status}"+
|
|
53
|
+
def initialize(status:, body: nil, headers: nil, message: nil)
|
|
54
|
+
@status = status
|
|
55
|
+
@body = body
|
|
56
|
+
@headers = headers
|
|
57
|
+
super(message || "HTTP #{status}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Raised for 4xx client error responses.
|
|
62
|
+
#
|
|
63
|
+
# @see ResponseError
|
|
64
|
+
class ClientError < ResponseError; end
|
|
65
|
+
|
|
66
|
+
# Raised for HTTP 400 Bad Request responses.
|
|
67
|
+
#
|
|
68
|
+
# @see ClientError
|
|
69
|
+
class BadRequest < ClientError; end
|
|
70
|
+
|
|
71
|
+
# Raised for HTTP 404 Not Found responses.
|
|
72
|
+
#
|
|
73
|
+
# @see ClientError
|
|
74
|
+
class NotFound < ClientError; end
|
|
75
|
+
|
|
76
|
+
# Raised for HTTP 422 Unprocessable Entity responses.
|
|
77
|
+
#
|
|
78
|
+
# @see ClientError
|
|
79
|
+
class UnprocessableEntity < ClientError; end
|
|
80
|
+
|
|
81
|
+
# Raised for HTTP 429 Too Many Requests responses.
|
|
82
|
+
#
|
|
83
|
+
# @see ClientError
|
|
84
|
+
class TooManyRequests < ClientError; end
|
|
85
|
+
|
|
86
|
+
# Raised for 5xx server error responses.
|
|
87
|
+
#
|
|
88
|
+
# @see ResponseError
|
|
89
|
+
class ServerError < ResponseError; end
|
|
16
90
|
end
|
data/lib/toc_doc/core/version.rb
CHANGED
|
@@ -4,24 +4,81 @@ require 'faraday'
|
|
|
4
4
|
|
|
5
5
|
module TocDoc
|
|
6
6
|
# @api private
|
|
7
|
+
# Namespace for Faraday middleware used internally by {TocDoc}.
|
|
7
8
|
module Middleware
|
|
8
|
-
# Faraday middleware that translates
|
|
9
|
-
# {TocDoc::Error}, keeping Faraday as an
|
|
9
|
+
# Faraday middleware that translates HTTP error responses and transport-level
|
|
10
|
+
# failures into typed {TocDoc::Error} subclasses, keeping Faraday as an
|
|
11
|
+
# internal implementation detail.
|
|
10
12
|
#
|
|
11
|
-
#
|
|
13
|
+
# Placed as the outermost middleware so the retry middleware operates on raw
|
|
14
|
+
# Faraday exceptions and returns the final response after exhaustion.
|
|
15
|
+
#
|
|
16
|
+
# Two-pronged error mapping:
|
|
17
|
+
# - +on_complete+: inspects the HTTP status and raises the appropriate
|
|
18
|
+
# {TocDoc::ResponseError} subclass for 4xx/5xx responses.
|
|
19
|
+
# - +rescue+ in +call+: catches Faraday transport errors and raises
|
|
20
|
+
# {TocDoc::ConnectionError}.
|
|
12
21
|
#
|
|
13
22
|
# @see TocDoc::Error
|
|
23
|
+
# @see TocDoc::ConnectionError
|
|
24
|
+
# @see TocDoc::ResponseError
|
|
14
25
|
class RaiseError < Faraday::Middleware
|
|
15
|
-
#
|
|
16
|
-
# {TocDoc::
|
|
26
|
+
# Maps specific HTTP status codes to their typed error classes.
|
|
27
|
+
# Statuses not in this map fall back to {TocDoc::ClientError} (4xx) or
|
|
28
|
+
# {TocDoc::ServerError} (5xx).
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash{Integer => Class}]
|
|
31
|
+
STATUS_MAP = {
|
|
32
|
+
400 => TocDoc::BadRequest,
|
|
33
|
+
404 => TocDoc::NotFound,
|
|
34
|
+
422 => TocDoc::UnprocessableEntity,
|
|
35
|
+
429 => TocDoc::TooManyRequests
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
# Executes the request, raises {TocDoc::ConnectionError} on transport
|
|
39
|
+
# failures, and delegates HTTP error mapping to +on_complete+.
|
|
17
40
|
#
|
|
18
41
|
# @param env [Faraday::Env] the Faraday request environment
|
|
19
|
-
# @return [Faraday::
|
|
20
|
-
# @raise [TocDoc::
|
|
42
|
+
# @return [Faraday::Response] the response on success
|
|
43
|
+
# @raise [TocDoc::ConnectionError] on network/transport-level failures
|
|
44
|
+
# @raise [TocDoc::ResponseError] on 4xx or 5xx HTTP responses
|
|
21
45
|
def call(env)
|
|
22
|
-
@app.call(env)
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
@app.call(env).on_complete do |response_env|
|
|
47
|
+
on_complete(response_env)
|
|
48
|
+
end
|
|
49
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::SSLError => e
|
|
50
|
+
raise TocDoc::ConnectionError, e.message
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Maps the HTTP status in +env+ to a typed {TocDoc::ResponseError} and
|
|
56
|
+
# raises it. No-ops for non-error statuses.
|
|
57
|
+
#
|
|
58
|
+
# @param env [Faraday::Env] the completed response environment
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @raise [TocDoc::ResponseError] for 4xx or 5xx status codes
|
|
61
|
+
def on_complete(env)
|
|
62
|
+
status = env[:status]
|
|
63
|
+
body = env[:body]
|
|
64
|
+
headers = env[:response_headers]
|
|
65
|
+
|
|
66
|
+
error_class = error_class_for(status)
|
|
67
|
+
raise error_class.new(status: status, body: body, headers: headers) if error_class
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Resolves the error class for a given HTTP status code.
|
|
71
|
+
#
|
|
72
|
+
# @param status [Integer] the HTTP status code
|
|
73
|
+
# @return [Class, nil] the error class, or +nil+ if the status is not an error
|
|
74
|
+
def error_class_for(status)
|
|
75
|
+
if STATUS_MAP.key?(status)
|
|
76
|
+
STATUS_MAP[status]
|
|
77
|
+
elsif status >= 400 && status < 500
|
|
78
|
+
TocDoc::ClientError
|
|
79
|
+
elsif status >= 500
|
|
80
|
+
TocDoc::ServerError
|
|
81
|
+
end
|
|
25
82
|
end
|
|
26
83
|
end
|
|
27
84
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# Represents an agenda (calendar) returned by the booking info endpoint.
|
|
5
|
+
#
|
|
6
|
+
# The +id+ and +practice_id+ fields are the primary attributes. Additional
|
|
7
|
+
# fields such as +visit_motive_ids+ and +visit_motive_ids_by_practice_id+ are
|
|
8
|
+
# accessible via dot-notation inherited from {TocDoc::Resource}.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# agenda = TocDoc::Agenda.new(
|
|
12
|
+
# 'id' => 42,
|
|
13
|
+
# 'practice_id' => 'practice-125055',
|
|
14
|
+
# 'visit_motive_ids' => [1, 2]
|
|
15
|
+
# )
|
|
16
|
+
# agenda.id #=> 42
|
|
17
|
+
# agenda.practice_id #=> "practice-125055"
|
|
18
|
+
class Agenda < Resource
|
|
19
|
+
main_attrs :id, :practice_id
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'toc_doc/models/profile'
|
|
4
|
+
require 'toc_doc/models/speciality'
|
|
5
|
+
require 'toc_doc/models/place'
|
|
6
|
+
require 'toc_doc/models/visit_motive'
|
|
7
|
+
require 'toc_doc/models/agenda'
|
|
8
|
+
|
|
9
|
+
module TocDoc
|
|
10
|
+
# Envelope returned by the slot-selection funnel info endpoint.
|
|
11
|
+
#
|
|
12
|
+
# Wraps the raw API response and exposes typed collections for the booking
|
|
13
|
+
# context: profile, specialities, visit motives, agendas, places, and
|
|
14
|
+
# practitioners.
|
|
15
|
+
#
|
|
16
|
+
# Unlike {TocDoc::Profile}, this class is NOT a {Resource} subclass — it is
|
|
17
|
+
# a plain envelope class, similar in role to {TocDoc::Search::Result}.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# info = TocDoc::BookingInfo.find('jane-doe-bordeaux')
|
|
21
|
+
# info.profile #=> #<TocDoc::Profile::Practitioner>
|
|
22
|
+
# info.visit_motives #=> [#<TocDoc::VisitMotive>, ...]
|
|
23
|
+
# info.organization? #=> false
|
|
24
|
+
class BookingInfo
|
|
25
|
+
# API path for the slot-selection funnel info endpoint.
|
|
26
|
+
# @return [String]
|
|
27
|
+
PATH = '/online_booking/api/slot_selection_funnel/v1/info.json'
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Fetches booking info for a profile slug or numeric ID.
|
|
31
|
+
#
|
|
32
|
+
# @param identifier [String, Integer] profile slug or numeric ID,
|
|
33
|
+
# forwarded as the +profile_slug+ query parameter
|
|
34
|
+
# @return [BookingInfo]
|
|
35
|
+
# @raise [ArgumentError] if +identifier+ is +nil+
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# TocDoc::BookingInfo.find('jane-doe-bordeaux')
|
|
39
|
+
# TocDoc::BookingInfo.find(325629)
|
|
40
|
+
def find(identifier)
|
|
41
|
+
raise ArgumentError, 'identifier cannot be nil' if identifier.nil?
|
|
42
|
+
|
|
43
|
+
data = TocDoc.client.get(PATH, query: { profile_slug: identifier })['data']
|
|
44
|
+
new(data)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param data [Hash] the +data+ value from the API response body
|
|
49
|
+
def initialize(data)
|
|
50
|
+
@data = data
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The profile associated with this booking context, typed via
|
|
54
|
+
# {TocDoc::Profile.build}.
|
|
55
|
+
#
|
|
56
|
+
# @return [TocDoc::Profile::Practitioner, TocDoc::Profile::Organization]
|
|
57
|
+
def profile
|
|
58
|
+
@profile ||= Profile.build(@data['profile'])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# All specialities for this booking context.
|
|
62
|
+
#
|
|
63
|
+
# @return [Array<TocDoc::Speciality>]
|
|
64
|
+
def specialities
|
|
65
|
+
@specialities ||= Array(@data['specialities']).map do |speciality_attrs|
|
|
66
|
+
Speciality.new(speciality_attrs)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# All visit motives for this booking context.
|
|
71
|
+
#
|
|
72
|
+
# @return [Array<TocDoc::VisitMotive>]
|
|
73
|
+
def visit_motives
|
|
74
|
+
@visit_motives ||= Array(@data['visit_motives']).map do |visit_motive_attrs|
|
|
75
|
+
VisitMotive.new(visit_motive_attrs)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# All agendas for this booking context.
|
|
80
|
+
#
|
|
81
|
+
# @return [Array<TocDoc::Agenda>]
|
|
82
|
+
def agendas
|
|
83
|
+
@agendas ||= Array(@data['agendas']).map do |agenda_attrs|
|
|
84
|
+
agenda_visit_motives = visit_motives.select do |vm|
|
|
85
|
+
agenda_attrs['visit_motive_ids'].include?(vm.id)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Agenda.new(agenda_attrs.merge('visit_motives' => agenda_visit_motives))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# All practice locations for this booking context.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<TocDoc::Place>]
|
|
95
|
+
def places
|
|
96
|
+
@places ||= Array(@data['places']).map do |place_attrs|
|
|
97
|
+
Place.new(place_attrs)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# All practitioners associated with this booking context.
|
|
102
|
+
#
|
|
103
|
+
# Always constructed as {TocDoc::Profile::Practitioner} since the
|
|
104
|
+
# +practitioners+ array in this endpoint exclusively contains practitioners.
|
|
105
|
+
# Marked as partial since the data is a summary, not a full profile page.
|
|
106
|
+
#
|
|
107
|
+
# @return [Array<TocDoc::Profile::Practitioner>]
|
|
108
|
+
def practitioners
|
|
109
|
+
@practitioners ||= Array(@data['practitioners']).map do |practitioner_attrs|
|
|
110
|
+
Profile::Practitioner.new(practitioner_attrs.merge('partial' => true))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns +true+ when the top-level profile is an organization.
|
|
115
|
+
#
|
|
116
|
+
# Delegates to {TocDoc::Profile#organization?}.
|
|
117
|
+
#
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def organization?
|
|
120
|
+
profile.organization?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns the raw data hash.
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash]
|
|
126
|
+
def to_h
|
|
127
|
+
@data
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/toc_doc/models/place.rb
CHANGED
|
@@ -27,6 +27,12 @@ module TocDoc
|
|
|
27
27
|
# place.latitude #=> 44.8386722
|
|
28
28
|
# place.elevator #=> true
|
|
29
29
|
class Place < Resource
|
|
30
|
+
# Returns the geographic coordinates of the place.
|
|
31
|
+
#
|
|
32
|
+
# @return [Array(Float, Float)] +[latitude, longitude]+
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# place.coordinates #=> [44.8386722, -0.5780466]
|
|
30
36
|
def coordinates
|
|
31
37
|
[latitude, longitude]
|
|
32
38
|
end
|
|
@@ -6,8 +6,14 @@ module TocDoc
|
|
|
6
6
|
class Practitioner < Profile
|
|
7
7
|
main_attrs :name_with_title
|
|
8
8
|
|
|
9
|
+
# Returns the practitioner's display name.
|
|
10
|
+
#
|
|
11
|
+
# Prefers +name_with_title+ (e.g. "Dr Jane Doe") when present,
|
|
12
|
+
# falling back to plain +name+.
|
|
13
|
+
#
|
|
14
|
+
# @return [String]
|
|
9
15
|
def to_s
|
|
10
|
-
name_with_title || name
|
|
16
|
+
(respond_to?(:name_with_title) && name_with_title) || name
|
|
11
17
|
end
|
|
12
18
|
end
|
|
13
19
|
end
|
|
@@ -12,6 +12,8 @@ module TocDoc
|
|
|
12
12
|
# profile.practitioner? #=> true
|
|
13
13
|
# profile.name #=> "Dr Smith"
|
|
14
14
|
class Profile < Resource
|
|
15
|
+
# API path template for a full profile page (+sprintf+-style, requires +identifier+).
|
|
16
|
+
# @return [String]
|
|
15
17
|
PATH = '/profiles/%<identifier>s.json'
|
|
16
18
|
|
|
17
19
|
main_attrs :id, :partial
|
|
@@ -19,20 +21,26 @@ module TocDoc
|
|
|
19
21
|
class << self
|
|
20
22
|
# Factory — returns a +Profile::Practitioner+ or +Profile::Organization+.
|
|
21
23
|
#
|
|
22
|
-
# Resolves type via +owner_type+ first (
|
|
24
|
+
# Resolves type via +owner_type+ first (autocomplete context), then falls back
|
|
23
25
|
# to the boolean flags present on profile-page responses.
|
|
24
26
|
#
|
|
25
27
|
# @param attrs [Hash] raw attribute hash from the API response
|
|
26
28
|
# @return [Profile::Practitioner, Profile::Organization]
|
|
29
|
+
# @raise [ArgumentError] if +attrs+ contains an unknown +owner_type+ or the
|
|
30
|
+
# profile type cannot be determined from the available flags
|
|
31
|
+
#
|
|
32
|
+
# @example Build from an autocomplete result
|
|
33
|
+
# TocDoc::Profile.build('owner_type' => 'Account', 'name' => 'Dr Smith')
|
|
34
|
+
# @example Build from a full profile response
|
|
35
|
+
# TocDoc::Profile.build('is_practitioner' => true, 'name' => 'Dr Smith')
|
|
27
36
|
def build(attrs = {})
|
|
28
37
|
attrs = normalize_attrs(attrs)
|
|
29
|
-
return find(attrs['value']) if attrs['force_full_profile'] && attrs['owner_type']
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
return find(attrs['value']) if attrs['force_full_profile']
|
|
40
|
+
|
|
41
|
+
build_from_autocomplete(attrs) ||
|
|
42
|
+
build_from_booking_info(attrs) ||
|
|
43
|
+
build_from_full_profile(attrs)
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# Fetches a full profile page by slug or numeric ID.
|
|
@@ -53,7 +61,31 @@ module TocDoc
|
|
|
53
61
|
|
|
54
62
|
private
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
# Autocomplete results carry an +owner_type+ key that tells us the
|
|
65
|
+
# profile type directly. When +force_full_profile+ is set, fetches
|
|
66
|
+
# the full profile page instead of building a partial.
|
|
67
|
+
#
|
|
68
|
+
# @param attrs [Hash] normalized attribute hash
|
|
69
|
+
# @return [Profile::Practitioner, Profile::Organization, nil] +nil+ when
|
|
70
|
+
# +owner_type+ is absent
|
|
71
|
+
# @raise [ArgumentError] if +owner_type+ is present but unrecognised
|
|
72
|
+
def build_from_autocomplete(attrs)
|
|
73
|
+
return unless attrs['owner_type']
|
|
74
|
+
|
|
75
|
+
case attrs['owner_type']
|
|
76
|
+
when 'Account' then Practitioner.new(attrs.merge('partial' => true))
|
|
77
|
+
when 'Organization' then Organization.new(attrs.merge('partial' => true))
|
|
78
|
+
else raise ArgumentError, "Unknown owner_type: #{attrs['owner_type']}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Builds a profile from a full profile-page response.
|
|
83
|
+
#
|
|
84
|
+
# @param attrs [Hash] normalized attribute hash from a full profile page
|
|
85
|
+
# @return [Profile::Practitioner, Profile::Organization]
|
|
86
|
+
# @raise [ArgumentError] if neither +is_practitioner+ nor +organization+
|
|
87
|
+
# flag is present in +attrs+
|
|
88
|
+
def build_from_full_profile(attrs)
|
|
57
89
|
if attrs['is_practitioner']
|
|
58
90
|
Practitioner.new(attrs.merge('partial' => false))
|
|
59
91
|
elsif attrs['organization']
|
|
@@ -63,6 +95,26 @@ module TocDoc
|
|
|
63
95
|
end
|
|
64
96
|
end
|
|
65
97
|
|
|
98
|
+
# Booking-info profiles carry `organization` (true/false) but lack
|
|
99
|
+
# `is_practitioner`. Returns nil when the shape doesn't match so the
|
|
100
|
+
# caller can fall through to build_from_full_profile.
|
|
101
|
+
#
|
|
102
|
+
# @param attrs [Hash] normalized attribute hash
|
|
103
|
+
# @return [Profile::Practitioner, Profile::Organization, nil] +nil+ when
|
|
104
|
+
# the booking-info shape is not matched
|
|
105
|
+
def build_from_booking_info(attrs)
|
|
106
|
+
return unless attrs.key?('organization') && !attrs.key?('is_practitioner')
|
|
107
|
+
|
|
108
|
+
return Organization.new(attrs.merge('partial' => true)) if attrs['organization']
|
|
109
|
+
|
|
110
|
+
Practitioner.new(attrs.merge('partial' => true))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Extracts and coerces profile attributes from the raw API response.
|
|
114
|
+
#
|
|
115
|
+
# @param data [Hash] the +data+ key from the API response body
|
|
116
|
+
# @return [Hash] merged attribute hash with +speciality+ and +places+
|
|
117
|
+
# coerced into model objects
|
|
66
118
|
def profile_attrs(data)
|
|
67
119
|
data['profile'].merge(
|
|
68
120
|
'speciality' => TocDoc::Speciality.new(data['profile']['speciality'] || {}),
|
|
@@ -78,6 +130,9 @@ module TocDoc
|
|
|
78
130
|
# Returns all skills across all practices as an array of {TocDoc::Resource}.
|
|
79
131
|
#
|
|
80
132
|
# @return [Array<TocDoc::Resource>]
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# profile.skills #=> [#<TocDoc::Resource ...>, ...]
|
|
81
136
|
def skills
|
|
82
137
|
hash = self['skills_by_practice'] || {}
|
|
83
138
|
hash.values.flatten.map { |s| TocDoc::Resource.new(s) }
|
|
@@ -87,17 +142,26 @@ module TocDoc
|
|
|
87
142
|
#
|
|
88
143
|
# @param practice_id [Integer, String] the practice ID
|
|
89
144
|
# @return [Array<TocDoc::Resource>]
|
|
145
|
+
#
|
|
146
|
+
# @example
|
|
147
|
+
# profile.skills_for(123) #=> [#<TocDoc::Resource ...>, ...]
|
|
90
148
|
def skills_for(practice_id)
|
|
91
149
|
hash = self['skills_by_practice'] || {}
|
|
92
150
|
Array(hash[practice_id.to_s]).map { |s| TocDoc::Resource.new(s) }
|
|
93
151
|
end
|
|
94
152
|
|
|
95
153
|
# @return [Boolean] true when this profile is a practitioner
|
|
154
|
+
#
|
|
155
|
+
# @example
|
|
156
|
+
# profile.practitioner? #=> true
|
|
96
157
|
def practitioner?
|
|
97
158
|
is_a?(Practitioner)
|
|
98
159
|
end
|
|
99
160
|
|
|
100
161
|
# @return [Boolean] true when this profile is an organization
|
|
162
|
+
#
|
|
163
|
+
# @example
|
|
164
|
+
# profile.organization? #=> false
|
|
101
165
|
def organization?
|
|
102
166
|
is_a?(Organization)
|
|
103
167
|
end
|
|
@@ -17,7 +17,12 @@ module TocDoc
|
|
|
17
17
|
# TocDoc::Search.where(query: 'dentiste', type: 'practitioner')
|
|
18
18
|
# #=> [#<TocDoc::Profile::Practitioner>, ...]
|
|
19
19
|
class Search
|
|
20
|
+
# API path for the autocomplete / searchbar endpoint.
|
|
21
|
+
# @return [String]
|
|
20
22
|
PATH = '/api/searchbar/autocomplete.json'
|
|
23
|
+
|
|
24
|
+
# Accepted values for the +type:+ keyword in {.where}.
|
|
25
|
+
# @return [Array<String>]
|
|
21
26
|
VALID_TYPES = %w[profile practitioner organization speciality].freeze
|
|
22
27
|
|
|
23
28
|
class << self
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# Represents a visit motive (reason for consultation) returned by the booking
|
|
5
|
+
# info endpoint.
|
|
6
|
+
#
|
|
7
|
+
# The +id+ and +name+ fields are the primary attributes. Additional fields
|
|
8
|
+
# such as +restrictions+ are accessible via dot-notation inherited from
|
|
9
|
+
# {TocDoc::Resource}.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# motive = TocDoc::VisitMotive.new('id' => 1, 'name' => 'Consultation', 'restrictions' => [])
|
|
13
|
+
# motive.id #=> 1
|
|
14
|
+
# motive.name #=> "Consultation"
|
|
15
|
+
class VisitMotive < Resource
|
|
16
|
+
main_attrs :id, :name
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/toc_doc/models.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require 'toc_doc/models/resource'
|
|
4
4
|
require 'toc_doc/models/speciality'
|
|
5
5
|
require 'toc_doc/models/place'
|
|
6
|
+
require 'toc_doc/models/visit_motive'
|
|
7
|
+
require 'toc_doc/models/agenda'
|
|
8
|
+
require 'toc_doc/models/booking_info'
|
|
6
9
|
require 'toc_doc/models/search'
|
|
7
10
|
|
|
8
11
|
require 'toc_doc/models/availability'
|
data/lib/toc_doc.rb
CHANGED
|
@@ -100,6 +100,17 @@ module TocDoc
|
|
|
100
100
|
TocDoc::Profile.find(identifier)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# Fetches booking context data for a profile by slug or numeric ID.
|
|
104
|
+
#
|
|
105
|
+
# Delegates to {TocDoc::BookingInfo.find} — see that method for full
|
|
106
|
+
# parameter documentation.
|
|
107
|
+
#
|
|
108
|
+
# @param identifier [String, Integer] profile slug or numeric ID
|
|
109
|
+
# @return [TocDoc::BookingInfo]
|
|
110
|
+
def booking_info(identifier)
|
|
111
|
+
TocDoc::BookingInfo.find(identifier)
|
|
112
|
+
end
|
|
113
|
+
|
|
103
114
|
# @!visibility private
|
|
104
115
|
def method_missing(method_name, ...)
|
|
105
116
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 01max
|
|
@@ -71,11 +71,12 @@ files:
|
|
|
71
71
|
- lib/toc_doc/core/uri_utils.rb
|
|
72
72
|
- lib/toc_doc/core/version.rb
|
|
73
73
|
- lib/toc_doc/http/middleware/raise_error.rb
|
|
74
|
-
- lib/toc_doc/http/response/base_middleware.rb
|
|
75
74
|
- lib/toc_doc/middleware/.keep
|
|
76
75
|
- lib/toc_doc/models.rb
|
|
76
|
+
- lib/toc_doc/models/agenda.rb
|
|
77
77
|
- lib/toc_doc/models/availability.rb
|
|
78
78
|
- lib/toc_doc/models/availability/collection.rb
|
|
79
|
+
- lib/toc_doc/models/booking_info.rb
|
|
79
80
|
- lib/toc_doc/models/place.rb
|
|
80
81
|
- lib/toc_doc/models/profile.rb
|
|
81
82
|
- lib/toc_doc/models/profile/organization.rb
|
|
@@ -84,6 +85,7 @@ files:
|
|
|
84
85
|
- lib/toc_doc/models/search.rb
|
|
85
86
|
- lib/toc_doc/models/search/result.rb
|
|
86
87
|
- lib/toc_doc/models/speciality.rb
|
|
88
|
+
- lib/toc_doc/models/visit_motive.rb
|
|
87
89
|
- sig/toc_doc.rbs
|
|
88
90
|
homepage: https://github.com/01max/toc_doc
|
|
89
91
|
licenses:
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'faraday'
|
|
4
|
-
|
|
5
|
-
module TocDoc
|
|
6
|
-
# @api private
|
|
7
|
-
module Response
|
|
8
|
-
# Abstract base class for TocDoc Faraday response middleware.
|
|
9
|
-
#
|
|
10
|
-
# Subclasses **must** override {#on_complete} to inspect or transform
|
|
11
|
-
# the response environment.
|
|
12
|
-
#
|
|
13
|
-
# @abstract
|
|
14
|
-
class BaseMiddleware < Faraday::Middleware
|
|
15
|
-
# Called by Faraday after the response has been received.
|
|
16
|
-
#
|
|
17
|
-
# @param env [Faraday::Env] the Faraday response environment
|
|
18
|
-
# @return [void]
|
|
19
|
-
# @raise [NotImplementedError] always — subclasses must override
|
|
20
|
-
def on_complete(env)
|
|
21
|
-
raise NotImplementedError, "#{self.class} must implement #on_complete"
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|