toc_doc 1.3.0 → 1.5.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 +34 -0
- data/README.md +124 -4
- data/TODO.md +12 -11
- data/lib/toc_doc/core/version.rb +1 -1
- data/lib/toc_doc/models/agenda.rb +21 -0
- data/lib/toc_doc/models/booking_info.rb +128 -0
- data/lib/toc_doc/models/place.rb +34 -0
- data/lib/toc_doc/models/profile/practitioner.rb +7 -1
- data/lib/toc_doc/models/profile.rb +139 -11
- data/lib/toc_doc/models/resource.rb +43 -2
- data/lib/toc_doc/models/visit_motive.rb +18 -0
- data/lib/toc_doc/models.rb +5 -1
- data/lib/toc_doc.rb +22 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: beef9501f028dfc80d31fb212f9ff876b20b952801ce7712702144bfa572fb32
|
|
4
|
+
data.tar.gz: ade0bafa23b88557931944c4199e64b9f45c972814d5b7af4b6b343602ae32eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fcf8a1d6b3e3ef1e200bbd34c91922859f1c31ebb1ed208c6810d75470e56a63030c1f325f2ec9c39bbb54a73b97f831b5503d26528e34f62863f8a63cbedda
|
|
7
|
+
data.tar.gz: 20b38a89845bdf86a3b6effae5f47fbfc97e1d461b309a95a0c1a5b072bce2e9b6c58ee627719a07b02751b93e0edcfb309e552d6c6516998c3e34a8fb1ab2ef
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-03-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`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
|
|
8
|
+
- **`TocDoc::BookingInfo#profile`** — returns the typed profile (`Practitioner` or `Organization`) via `Profile.build`
|
|
9
|
+
- **`TocDoc::BookingInfo#specialities`** — returns an array of `TocDoc::Speciality` objects
|
|
10
|
+
- **`TocDoc::BookingInfo#visit_motives`** — returns an array of `TocDoc::VisitMotive` objects
|
|
11
|
+
- **`TocDoc::BookingInfo#agendas`** — returns an array of `TocDoc::Agenda` objects, each pre-resolved with its matching `VisitMotive` objects via `visit_motive_ids`
|
|
12
|
+
- **`TocDoc::BookingInfo#places`** — returns an array of `TocDoc::Place` objects
|
|
13
|
+
- **`TocDoc::BookingInfo#practitioners`** — returns an array of `TocDoc::Profile::Practitioner` objects (marked `partial: true`)
|
|
14
|
+
- **`TocDoc::BookingInfo#organization?`** — delegates to the inner profile
|
|
15
|
+
- **`TocDoc::VisitMotive`** — new `Resource`-based model representing a visit motive (reason for consultation); exposes `#id` and `#name` via dot-notation
|
|
16
|
+
- **`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`
|
|
17
|
+
- **`TocDoc.booking_info`** — top-level shortcut delegating to `TocDoc::BookingInfo.find`
|
|
18
|
+
|
|
19
|
+
## [1.4.0] - 2026-03-19
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`TocDoc::Place`** — new `Resource`-based model representing a practice location inside a profile response; exposes address/geo fields (`#address`, `#zipcode`, `#city`, `#full_address`, `#landline_number`, `#latitude`, `#longitude`, `#elevator`, `#handicap`, `#formal_name`) via dot-notation and adds `#coordinates` returning `[latitude, longitude]`
|
|
24
|
+
- **`TocDoc::Profile.find`** — class method that fetches a full profile page by slug or numeric ID from `/profiles/:identifier.json`; returns a typed `Profile::Practitioner` or `Profile::Organization` with `partial: false`
|
|
25
|
+
- **`TocDoc::Profile#skills`** — returns all skills across every practice as an array of `TocDoc::Resource` objects
|
|
26
|
+
- **`TocDoc::Profile#skills_for(practice_id)`** — returns skills for a single practice
|
|
27
|
+
- **`TocDoc.profile`** — top-level shortcut delegating to `TocDoc::Profile.find`
|
|
28
|
+
- **`TocDoc::Resource.main_attrs`** — class macro declaring which attribute keys appear in `#inspect`; inheritable by subclasses
|
|
29
|
+
- **`TocDoc::Resource.normalize_attrs`** — extracted as a public class method (string-key normalisation)
|
|
30
|
+
- **`TocDoc::Resource#inspect`** — custom implementation using `main_attrs` when declared, falling back to all keys
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **`TocDoc::Profile.build`** — updated to resolve full profile responses via boolean flags (`is_practitioner` / `organization`) in addition to the existing `owner_type` path used for search results; profiles built from search results are now tagged `partial: true`, full fetches `partial: false`; a `force_full_profile` flag on a search result transparently delegates to `Profile.find`
|
|
35
|
+
- **`TocDoc::Profile`** — `main_attrs :id, :partial` declared so inspect output stays concise
|
|
36
|
+
|
|
3
37
|
## [1.3.0] - 2026-03-15
|
|
4
38
|
|
|
5
39
|
### Added
|
data/README.md
CHANGED
|
@@ -28,6 +28,8 @@ A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-b
|
|
|
28
28
|
4. [Endpoints](#endpoints)
|
|
29
29
|
- [Availabilities](#availabilities)
|
|
30
30
|
- [Search](#search)
|
|
31
|
+
- [Profile](#profile)
|
|
32
|
+
- [BookingInfo](#bookinginfo)
|
|
31
33
|
5. [Response objects](#response-objects)
|
|
32
34
|
6. [Pagination](#pagination)
|
|
33
35
|
7. [Error handling](#error-handling)
|
|
@@ -128,7 +130,7 @@ client.get('/availabilities.json', query: { visit_motive_ids: '123', agenda_ids:
|
|
|
128
130
|
| Option | Default | Description |
|
|
129
131
|
|---|---|---|
|
|
130
132
|
| `api_endpoint` | `https://www.doctolib.fr` | Base URL. Change to `.de` / `.it` for other countries. |
|
|
131
|
-
| `user_agent` | `TocDoc Ruby Gem 1.
|
|
133
|
+
| `user_agent` | `TocDoc Ruby Gem 1.5.0` | `User-Agent` header sent with every request. |
|
|
132
134
|
| `default_media_type` | `application/json` | `Accept` and `Content-Type` headers. |
|
|
133
135
|
| `per_page` | `15` | Default number of availability dates per request (capped at `15`). |
|
|
134
136
|
| `middleware` | Retry + RaiseError + JSON + adapter | Full Faraday middleware stack. Override to customise completely. |
|
|
@@ -209,6 +211,66 @@ Valid `type:` values: `'profile'` (all profiles), `'practitioner'`, `'organizati
|
|
|
209
211
|
|
|
210
212
|
**Return value:** a `TocDoc::Search::Result` when `type:` is omitted, or a filtered `Array` otherwise (see [Response objects](#response-objects)).
|
|
211
213
|
|
|
214
|
+
### Profile
|
|
215
|
+
|
|
216
|
+
Fetch a full practitioner or organization profile page by slug or numeric ID.
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# by slug
|
|
220
|
+
profile = TocDoc::Profile.find('jane-doe-bordeaux')
|
|
221
|
+
|
|
222
|
+
# by numeric ID
|
|
223
|
+
profile = TocDoc::Profile.find(1_542_899)
|
|
224
|
+
|
|
225
|
+
# module-level shortcut
|
|
226
|
+
profile = TocDoc.profile('jane-doe-bordeaux')
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`Profile.find` returns a typed `TocDoc::Profile::Practitioner` or `TocDoc::Profile::Organization` instance with `partial: false` (i.e. full profile data).
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
profile.name # => "Dr. Jane Doe"
|
|
233
|
+
profile.partial # => false
|
|
234
|
+
profile.practitioner? # => true
|
|
235
|
+
|
|
236
|
+
profile.skills # => [#<TocDoc::Resource ...>, ...]
|
|
237
|
+
profile.skills_for(377_272) # => skills for a specific practice
|
|
238
|
+
|
|
239
|
+
profile.places.first.city # => "Bordeaux"
|
|
240
|
+
profile.places.first.coordinates # => [44.8386722, -0.5780466]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Return value:** a `TocDoc::Profile::Practitioner` or `TocDoc::Profile::Organization` (see [Response objects](#response-objects)).
|
|
244
|
+
|
|
245
|
+
### BookingInfo
|
|
246
|
+
|
|
247
|
+
Fetch the slot-selection funnel context for a practitioner or organization by slug or numeric ID.
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# by slug
|
|
251
|
+
info = TocDoc::BookingInfo.find('jane-doe-bordeaux')
|
|
252
|
+
|
|
253
|
+
# by numeric ID
|
|
254
|
+
info = TocDoc::BookingInfo.find(1_542_899)
|
|
255
|
+
|
|
256
|
+
# module-level shortcut
|
|
257
|
+
info = TocDoc.booking_info('jane-doe-bordeaux')
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`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.
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
info.profile # => #<TocDoc::Profile::Practitioner ...>
|
|
264
|
+
info.specialities # => [#<TocDoc::Speciality ...>, ...]
|
|
265
|
+
info.visit_motives # => [#<TocDoc::VisitMotive id=..., name="...">, ...]
|
|
266
|
+
info.agendas # => [#<TocDoc::Agenda id=..., practice_id=...>, ...]
|
|
267
|
+
info.places # => [#<TocDoc::Place ...>, ...]
|
|
268
|
+
info.practitioners # => [#<TocDoc::Profile::Practitioner partial=true>, ...]
|
|
269
|
+
info.organization? # => false
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Return value:** a `TocDoc::BookingInfo` (see [Response objects](#response-objects)).
|
|
273
|
+
|
|
212
274
|
---
|
|
213
275
|
|
|
214
276
|
## Response objects
|
|
@@ -270,15 +332,73 @@ Returned by `TocDoc::Search.where` when `type:` is omitted.
|
|
|
270
332
|
|
|
271
333
|
### `TocDoc::Profile`
|
|
272
334
|
|
|
273
|
-
Represents a
|
|
335
|
+
Represents a practitioner or organization profile. Can be a lightweight search result (`partial: true`) or a full profile page (`partial: false`).
|
|
274
336
|
|
|
275
337
|
| Method | Type | Description |
|
|
276
338
|
|---|---|---|
|
|
277
|
-
| `Profile.
|
|
339
|
+
| `Profile.find(identifier)` | `Profile::Practitioner \| Profile::Organization` | Fetches a full profile by slug or numeric ID. Returns `partial: false`. |
|
|
340
|
+
| `Profile.build(attrs)` | `Profile::Practitioner \| Profile::Organization` | Factory used internally by `Search::Result`; resolves type from `owner_type` or boolean flags. Returns `partial: true` for search results. |
|
|
341
|
+
| `#id` | `String \| Integer` | Profile identifier. |
|
|
342
|
+
| `#partial` | `Boolean` | `true` when built from a search result, `false` when fetched via `Profile.find`. |
|
|
278
343
|
| `#practitioner?` | `Boolean` | `true` when this is a `Profile::Practitioner`. |
|
|
279
344
|
| `#organization?` | `Boolean` | `true` when this is a `Profile::Organization`. |
|
|
345
|
+
| `#places` | `Array<TocDoc::Place>` | Practice locations (available on full profiles). |
|
|
346
|
+
| `#skills` | `Array<TocDoc::Resource>` | All skills across every practice (available on full profiles). |
|
|
347
|
+
| `#skills_for(practice_id)` | `Array<TocDoc::Resource>` | Skills for a single practice by its ID. |
|
|
348
|
+
|
|
349
|
+
`TocDoc::Profile::Practitioner` and `TocDoc::Profile::Organization` are typed subclasses that inherit dot-notation attribute access from `TocDoc::Resource`.
|
|
350
|
+
|
|
351
|
+
### `TocDoc::Place`
|
|
352
|
+
|
|
353
|
+
Represents a practice location returned inside a full profile response. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
354
|
+
|
|
355
|
+
| Method | Type | Description |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `#id` | `String` | Practice identifier (e.g. `"practice-125055"`). |
|
|
358
|
+
| `#address` | `String` | Street address. |
|
|
359
|
+
| `#zipcode` | `String` | Postal code. |
|
|
360
|
+
| `#city` | `String` | City name. |
|
|
361
|
+
| `#full_address` | `String` | Combined address string. |
|
|
362
|
+
| `#landline_number` | `String \| nil` | Phone number, if available. |
|
|
363
|
+
| `#latitude` | `Float` | Latitude. |
|
|
364
|
+
| `#longitude` | `Float` | Longitude. |
|
|
365
|
+
| `#elevator` | `Boolean` | Whether the practice has elevator access. |
|
|
366
|
+
| `#handicap` | `Boolean` | Whether the practice is handicap-accessible. |
|
|
367
|
+
| `#formal_name` | `String \| nil` | Formal practice name, if available. |
|
|
368
|
+
| `#coordinates` | `Array<Float>` | Convenience method returning `[latitude, longitude]`. |
|
|
369
|
+
|
|
370
|
+
### `TocDoc::BookingInfo`
|
|
371
|
+
|
|
372
|
+
Returned by `TocDoc::BookingInfo.find`; also accessible via the `TocDoc.booking_info` module-level shortcut.
|
|
373
|
+
|
|
374
|
+
| Method | Type | Description |
|
|
375
|
+
|---|---|---|
|
|
376
|
+
| `#profile` | `Profile::Practitioner \| Profile::Organization` | Typed profile built via `Profile.build`. |
|
|
377
|
+
| `#specialities` | `Array<TocDoc::Speciality>` | Specialities associated with the booking context. |
|
|
378
|
+
| `#visit_motives` | `Array<TocDoc::VisitMotive>` | Available visit motives (reasons for consultation). |
|
|
379
|
+
| `#agendas` | `Array<TocDoc::Agenda>` | Agendas, each pre-resolved with their matching `VisitMotive` objects. |
|
|
380
|
+
| `#places` | `Array<TocDoc::Place>` | Practice locations. |
|
|
381
|
+
| `#practitioners` | `Array<TocDoc::Profile::Practitioner>` | Practitioners associated with this booking context (`partial: true`). |
|
|
382
|
+
| `#organization?` | `Boolean` | Delegates to the inner profile. |
|
|
383
|
+
|
|
384
|
+
### `TocDoc::VisitMotive`
|
|
280
385
|
|
|
281
|
-
|
|
386
|
+
Represents a visit motive (reason for consultation) returned inside a `BookingInfo` response. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
387
|
+
|
|
388
|
+
| Method | Type | Description |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| `#id` | `Integer` | Visit motive identifier. |
|
|
391
|
+
| `#name` | `String` | Human-readable name of the visit motive. |
|
|
392
|
+
|
|
393
|
+
### `TocDoc::Agenda`
|
|
394
|
+
|
|
395
|
+
Represents an agenda (calendar) returned inside a `BookingInfo` response. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
396
|
+
|
|
397
|
+
| Method | Type | Description |
|
|
398
|
+
|---|---|---|
|
|
399
|
+
| `#id` | `Integer` | Agenda identifier. |
|
|
400
|
+
| `#practice_id` | `Integer` | ID of the associated practice. |
|
|
401
|
+
| `#visit_motives` | `Array<TocDoc::VisitMotive>` | Visit motives pre-resolved via `visit_motive_ids` when built through `BookingInfo`. |
|
|
282
402
|
|
|
283
403
|
### `TocDoc::Speciality`
|
|
284
404
|
|
data/TODO.md
CHANGED
|
@@ -2,17 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
[POTENTIAL_ENDPOINTS][POTENTIAL_ENDPOINTS.md]
|
|
4
4
|
|
|
5
|
-
## 1.4
|
|
6
|
-
|
|
7
|
-
- [ ] Profile
|
|
8
|
-
- slug : https://www.doctolib.fr/profiles/mathilde-devun-lesparre-medoc.json
|
|
9
|
-
- id : https://www.doctolib.fr/profiles/926388.json
|
|
10
|
-
|
|
11
|
-
## 1.5
|
|
12
|
-
|
|
13
|
-
- [ ] Booking context
|
|
14
|
-
- https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388
|
|
15
|
-
|
|
16
5
|
## 1.6
|
|
17
6
|
|
|
18
7
|
### Better API usage
|
|
@@ -33,6 +22,18 @@
|
|
|
33
22
|
|
|
34
23
|
# DONE & RELEASED
|
|
35
24
|
|
|
25
|
+
## 1.5
|
|
26
|
+
|
|
27
|
+
- [x] Booking context
|
|
28
|
+
- practitioner : https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388
|
|
29
|
+
- organization : https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=325629
|
|
30
|
+
|
|
31
|
+
## 1.4
|
|
32
|
+
|
|
33
|
+
- [x] Profile
|
|
34
|
+
- from slug : https://www.doctolib.fr/profiles/jane-doe-bordeaux.json
|
|
35
|
+
- from id : https://www.doctolib.fr/profiles/926388.json
|
|
36
|
+
|
|
36
37
|
## 1.3
|
|
37
38
|
|
|
38
39
|
- [x] Search (autocomplete)
|
data/lib/toc_doc/core/version.rb
CHANGED
|
@@ -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,128 @@
|
|
|
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
|
+
PATH = '/online_booking/api/slot_selection_funnel/v1/info.json'
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Fetches booking info for a profile slug or numeric ID.
|
|
29
|
+
#
|
|
30
|
+
# @param identifier [String, Integer] profile slug or numeric ID,
|
|
31
|
+
# forwarded as the +profile_slug+ query parameter
|
|
32
|
+
# @return [BookingInfo]
|
|
33
|
+
# @raise [ArgumentError] if +identifier+ is +nil+
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# TocDoc::BookingInfo.find('jane-doe-bordeaux')
|
|
37
|
+
# TocDoc::BookingInfo.find(325629)
|
|
38
|
+
def find(identifier)
|
|
39
|
+
raise ArgumentError, 'identifier cannot be nil' if identifier.nil?
|
|
40
|
+
|
|
41
|
+
data = TocDoc.client.get(PATH, query: { profile_slug: identifier })['data']
|
|
42
|
+
new(data)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param data [Hash] the +data+ value from the API response body
|
|
47
|
+
def initialize(data)
|
|
48
|
+
@data = data
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The profile associated with this booking context, typed via
|
|
52
|
+
# {TocDoc::Profile.build}.
|
|
53
|
+
#
|
|
54
|
+
# @return [TocDoc::Profile::Practitioner, TocDoc::Profile::Organization]
|
|
55
|
+
def profile
|
|
56
|
+
@profile ||= Profile.build(@data['profile'])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# All specialities for this booking context.
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<TocDoc::Speciality>]
|
|
62
|
+
def specialities
|
|
63
|
+
@specialities ||= Array(@data['specialities']).map do |speciality_attrs|
|
|
64
|
+
Speciality.new(speciality_attrs)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# All visit motives for this booking context.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<TocDoc::VisitMotive>]
|
|
71
|
+
def visit_motives
|
|
72
|
+
@visit_motives ||= Array(@data['visit_motives']).map do |visit_motive_attrs|
|
|
73
|
+
VisitMotive.new(visit_motive_attrs)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# All agendas for this booking context.
|
|
78
|
+
#
|
|
79
|
+
# @return [Array<TocDoc::Agenda>]
|
|
80
|
+
def agendas
|
|
81
|
+
@agendas ||= Array(@data['agendas']).map do |agenda_attrs|
|
|
82
|
+
agenda_visit_motives = visit_motives.select do |vm|
|
|
83
|
+
agenda_attrs['visit_motive_ids'].include?(vm.id)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Agenda.new(agenda_attrs.merge('visit_motives' => agenda_visit_motives))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# All practice locations for this booking context.
|
|
91
|
+
#
|
|
92
|
+
# @return [Array<TocDoc::Place>]
|
|
93
|
+
def places
|
|
94
|
+
@places ||= Array(@data['places']).map do |place_attrs|
|
|
95
|
+
Place.new(place_attrs)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# All practitioners associated with this booking context.
|
|
100
|
+
#
|
|
101
|
+
# Always constructed as {TocDoc::Profile::Practitioner} since the
|
|
102
|
+
# +practitioners+ array in this endpoint exclusively contains practitioners.
|
|
103
|
+
# Marked as partial since the data is a summary, not a full profile page.
|
|
104
|
+
#
|
|
105
|
+
# @return [Array<TocDoc::Profile::Practitioner>]
|
|
106
|
+
def practitioners
|
|
107
|
+
@practitioners ||= Array(@data['practitioners']).map do |practitioner_attrs|
|
|
108
|
+
Profile::Practitioner.new(practitioner_attrs.merge('partial' => true))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns +true+ when the top-level profile is an organization.
|
|
113
|
+
#
|
|
114
|
+
# Delegates to {TocDoc::Profile#organization?}.
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def organization?
|
|
118
|
+
profile.organization?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns the raw data hash.
|
|
122
|
+
#
|
|
123
|
+
# @return [Hash]
|
|
124
|
+
def to_h
|
|
125
|
+
@data
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# Represents a practice location returned inside a profile response.
|
|
5
|
+
#
|
|
6
|
+
# All fields are accessible via dot-notation inherited from {TocDoc::Resource}.
|
|
7
|
+
# Nested arrays (+opening_hours+, +stations+) are returned as raw arrays of hashes.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# place = TocDoc::Place.new(
|
|
11
|
+
# 'id' => 'practice-125055',
|
|
12
|
+
# 'address' => '1 Rue Anonyme',
|
|
13
|
+
# 'zipcode' => '33000',
|
|
14
|
+
# 'city' => 'Bordeaux',
|
|
15
|
+
# 'full_address' => '1 Rue Anonyme, 33000 Bordeaux',
|
|
16
|
+
# 'landline_number' => '05 23 45 67 89',
|
|
17
|
+
# 'latitude' => 44.8386722,
|
|
18
|
+
# 'longitude' => -0.5780466,
|
|
19
|
+
# 'elevator' => true,
|
|
20
|
+
# 'handicap' => true,
|
|
21
|
+
# 'formal_name' => 'Centre de santé - Anonyme'
|
|
22
|
+
# )
|
|
23
|
+
# place.id #=> "practice-125055"
|
|
24
|
+
# place.city #=> "Bordeaux"
|
|
25
|
+
# place.full_address #=> "1 Rue Anonyme, 33000 Bordeaux"
|
|
26
|
+
# place.landline_number #=> "05 23 45 67 89"
|
|
27
|
+
# place.latitude #=> 44.8386722
|
|
28
|
+
# place.elevator #=> true
|
|
29
|
+
class Place < Resource
|
|
30
|
+
def coordinates
|
|
31
|
+
[latitude, longitude]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
module TocDoc
|
|
4
4
|
class Profile
|
|
5
5
|
# A practitioner profile (raw +owner_type: "Account"+).
|
|
6
|
-
class Practitioner < Profile
|
|
6
|
+
class Practitioner < Profile
|
|
7
|
+
main_attrs :name_with_title
|
|
8
|
+
|
|
9
|
+
def to_s
|
|
10
|
+
(respond_to?(:name_with_title) && name_with_title) || name
|
|
11
|
+
end
|
|
12
|
+
end
|
|
7
13
|
end
|
|
8
14
|
end
|
|
@@ -12,26 +12,154 @@ module TocDoc
|
|
|
12
12
|
# profile.practitioner? #=> true
|
|
13
13
|
# profile.name #=> "Dr Smith"
|
|
14
14
|
class Profile < Resource
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
PATH = '/profiles/%<identifier>s.json'
|
|
16
|
+
|
|
17
|
+
main_attrs :id, :partial
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Factory — returns a +Profile::Practitioner+ or +Profile::Organization+.
|
|
21
|
+
#
|
|
22
|
+
# Resolves type via +owner_type+ first (autocomplete context), then falls back
|
|
23
|
+
# to the boolean flags present on profile-page responses.
|
|
24
|
+
#
|
|
25
|
+
# @param attrs [Hash] raw attribute hash from the API response
|
|
26
|
+
# @return [Profile::Practitioner, Profile::Organization]
|
|
27
|
+
# @raise [ArgumentError] if +attrs+ contains an unknown +owner_type+ or the
|
|
28
|
+
# profile type cannot be determined from the available flags
|
|
29
|
+
#
|
|
30
|
+
# @example Build from an autocomplete result
|
|
31
|
+
# TocDoc::Profile.build('owner_type' => 'Account', 'name' => 'Dr Smith')
|
|
32
|
+
# @example Build from a full profile response
|
|
33
|
+
# TocDoc::Profile.build('is_practitioner' => true, 'name' => 'Dr Smith')
|
|
34
|
+
def build(attrs = {})
|
|
35
|
+
attrs = normalize_attrs(attrs)
|
|
36
|
+
|
|
37
|
+
return find(attrs['value']) if attrs['force_full_profile']
|
|
38
|
+
|
|
39
|
+
build_from_autocomplete(attrs) ||
|
|
40
|
+
build_from_booking_info(attrs) ||
|
|
41
|
+
build_from_full_profile(attrs)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Fetches a full profile page by slug or numeric ID.
|
|
45
|
+
#
|
|
46
|
+
# @param identifier [String, Integer] profile slug or numeric ID
|
|
47
|
+
# @return [Profile::Practitioner, Profile::Organization]
|
|
48
|
+
# @raise [ArgumentError] if +identifier+ is +nil+
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# TocDoc::Profile.find('jane-doe-bordeaux')
|
|
52
|
+
# TocDoc::Profile.find(1542899)
|
|
53
|
+
def find(identifier)
|
|
54
|
+
raise ArgumentError, 'identifier cannot be nil' if identifier.nil?
|
|
55
|
+
|
|
56
|
+
data = TocDoc.client.get(format(PATH, identifier: identifier))['data']
|
|
57
|
+
build(profile_attrs(data))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Autocomplete results carry an +owner_type+ key that tells us the
|
|
63
|
+
# profile type directly. When +force_full_profile+ is set, fetches
|
|
64
|
+
# the full profile page instead of building a partial.
|
|
65
|
+
#
|
|
66
|
+
# @param attrs [Hash] normalized attribute hash
|
|
67
|
+
# @return [Profile::Practitioner, Profile::Organization, nil] +nil+ when
|
|
68
|
+
# +owner_type+ is absent
|
|
69
|
+
# @raise [ArgumentError] if +owner_type+ is present but unrecognised
|
|
70
|
+
def build_from_autocomplete(attrs)
|
|
71
|
+
return unless attrs['owner_type']
|
|
72
|
+
|
|
73
|
+
case attrs['owner_type']
|
|
74
|
+
when 'Account' then Practitioner.new(attrs.merge('partial' => true))
|
|
75
|
+
when 'Organization' then Organization.new(attrs.merge('partial' => true))
|
|
76
|
+
else raise ArgumentError, "Unknown owner_type: #{attrs['owner_type']}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Builds a profile from a full profile-page response.
|
|
81
|
+
#
|
|
82
|
+
# @param attrs [Hash] normalized attribute hash from a full profile page
|
|
83
|
+
# @return [Profile::Practitioner, Profile::Organization]
|
|
84
|
+
# @raise [ArgumentError] if neither +is_practitioner+ nor +organization+
|
|
85
|
+
# flag is present in +attrs+
|
|
86
|
+
def build_from_full_profile(attrs)
|
|
87
|
+
if attrs['is_practitioner']
|
|
88
|
+
Practitioner.new(attrs.merge('partial' => false))
|
|
89
|
+
elsif attrs['organization']
|
|
90
|
+
Organization.new(attrs.merge('partial' => false))
|
|
91
|
+
else
|
|
92
|
+
raise ArgumentError, "Unable to determine profile type from attributes: #{attrs.inspect}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Booking-info profiles carry `organization` (true/false) but lack
|
|
97
|
+
# `is_practitioner`. Returns nil when the shape doesn't match so the
|
|
98
|
+
# caller can fall through to build_from_full_profile.
|
|
99
|
+
#
|
|
100
|
+
# @param attrs [Hash] normalized attribute hash
|
|
101
|
+
# @return [Profile::Practitioner, Profile::Organization, nil] +nil+ when
|
|
102
|
+
# the booking-info shape is not matched
|
|
103
|
+
def build_from_booking_info(attrs)
|
|
104
|
+
return unless attrs.key?('organization') && !attrs.key?('is_practitioner')
|
|
105
|
+
|
|
106
|
+
return Organization.new(attrs.merge('partial' => true)) if attrs['organization']
|
|
107
|
+
|
|
108
|
+
Practitioner.new(attrs.merge('partial' => true))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Extracts and coerces profile attributes from the raw API response.
|
|
112
|
+
#
|
|
113
|
+
# @param data [Hash] the +data+ key from the API response body
|
|
114
|
+
# @return [Hash] merged attribute hash with +speciality+ and +places+
|
|
115
|
+
# coerced into model objects
|
|
116
|
+
def profile_attrs(data)
|
|
117
|
+
data['profile'].merge(
|
|
118
|
+
'speciality' => TocDoc::Speciality.new(data['profile']['speciality'] || {}),
|
|
119
|
+
'places' => Array(data['places']).map { |p| TocDoc::Place.new(p) },
|
|
120
|
+
'legals' => data['legals'],
|
|
121
|
+
'details' => data['details'],
|
|
122
|
+
'fees' => data['fees'],
|
|
123
|
+
'bookable' => data['bookable']
|
|
124
|
+
)
|
|
26
125
|
end
|
|
27
126
|
end
|
|
28
127
|
|
|
128
|
+
# Returns all skills across all practices as an array of {TocDoc::Resource}.
|
|
129
|
+
#
|
|
130
|
+
# @return [Array<TocDoc::Resource>]
|
|
131
|
+
#
|
|
132
|
+
# @example
|
|
133
|
+
# profile.skills #=> [#<TocDoc::Resource ...>, ...]
|
|
134
|
+
def skills
|
|
135
|
+
hash = self['skills_by_practice'] || {}
|
|
136
|
+
hash.values.flatten.map { |s| TocDoc::Resource.new(s) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Returns skills for a single practice as an array of {TocDoc::Resource}.
|
|
140
|
+
#
|
|
141
|
+
# @param practice_id [Integer, String] the practice ID
|
|
142
|
+
# @return [Array<TocDoc::Resource>]
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# profile.skills_for(123) #=> [#<TocDoc::Resource ...>, ...]
|
|
146
|
+
def skills_for(practice_id)
|
|
147
|
+
hash = self['skills_by_practice'] || {}
|
|
148
|
+
Array(hash[practice_id.to_s]).map { |s| TocDoc::Resource.new(s) }
|
|
149
|
+
end
|
|
150
|
+
|
|
29
151
|
# @return [Boolean] true when this profile is a practitioner
|
|
152
|
+
#
|
|
153
|
+
# @example
|
|
154
|
+
# profile.practitioner? #=> true
|
|
30
155
|
def practitioner?
|
|
31
156
|
is_a?(Practitioner)
|
|
32
157
|
end
|
|
33
158
|
|
|
34
159
|
# @return [Boolean] true when this profile is an organization
|
|
160
|
+
#
|
|
161
|
+
# @example
|
|
162
|
+
# profile.organization? #=> false
|
|
35
163
|
def organization?
|
|
36
164
|
is_a?(Organization)
|
|
37
165
|
end
|
|
@@ -14,9 +14,43 @@ module TocDoc
|
|
|
14
14
|
# resource[:date] #=> "2026-02-28"
|
|
15
15
|
# resource.to_h #=> { "date" => "2026-02-28", "slots" => [] }
|
|
16
16
|
class Resource
|
|
17
|
+
class << self
|
|
18
|
+
# Normalises a raw attribute hash to string keys, mirroring what
|
|
19
|
+
# {#initialize} does internally. Useful in class-level factory methods
|
|
20
|
+
# that need to inspect attrs before wrapping them in a Resource instance.
|
|
21
|
+
#
|
|
22
|
+
# @param attrs [Hash] raw hash with string or symbol keys
|
|
23
|
+
# @return [Hash{String => Object}]
|
|
24
|
+
def normalize_attrs(attrs)
|
|
25
|
+
attrs.transform_keys(&:to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Declares which attribute keys are shown in +#inspect+.
|
|
29
|
+
# When called with arguments, sets the list for this class.
|
|
30
|
+
# When called with no arguments, returns the list (or +nil+ if unset),
|
|
31
|
+
# walking the ancestor chain so subclasses inherit the declaration.
|
|
32
|
+
#
|
|
33
|
+
# Subclasses that never call +main_attrs+ fall back to showing all attrs.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# main_attrs :id, :name, :slug
|
|
37
|
+
#
|
|
38
|
+
# @param keys [Array<Symbol, String>]
|
|
39
|
+
# @return [Array<String>, nil]
|
|
40
|
+
def main_attrs(*keys)
|
|
41
|
+
if keys.empty?
|
|
42
|
+
ancestor = ancestors.find { |a| a.instance_variable_defined?(:@main_attrs) }
|
|
43
|
+
ancestor&.instance_variable_get(:@main_attrs)
|
|
44
|
+
else
|
|
45
|
+
inherited = superclass.respond_to?(:main_attrs) ? (superclass.main_attrs || []) : []
|
|
46
|
+
@main_attrs = (inherited + keys.map(&:to_s)).uniq
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
17
51
|
# @param attrs [Hash] the raw attribute hash (string or symbol keys)
|
|
18
52
|
def initialize(attrs = {})
|
|
19
|
-
@attrs =
|
|
53
|
+
@attrs = self.class.normalize_attrs(attrs)
|
|
20
54
|
end
|
|
21
55
|
|
|
22
56
|
# Read an attribute by name.
|
|
@@ -56,7 +90,7 @@ module TocDoc
|
|
|
56
90
|
def ==(other)
|
|
57
91
|
case other
|
|
58
92
|
when Resource then @attrs == other.to_h
|
|
59
|
-
when Hash then @attrs ==
|
|
93
|
+
when Hash then @attrs == self.class.normalize_attrs(other)
|
|
60
94
|
else false
|
|
61
95
|
end
|
|
62
96
|
end
|
|
@@ -78,5 +112,12 @@ module TocDoc
|
|
|
78
112
|
key = method_name.to_s
|
|
79
113
|
@attrs.key?(key) ? @attrs[key] : super
|
|
80
114
|
end
|
|
115
|
+
|
|
116
|
+
# @!visibility private
|
|
117
|
+
def inspect
|
|
118
|
+
keys = self.class.main_attrs || @attrs.keys
|
|
119
|
+
pairs = keys.map { |k| "@#{k}=#{@attrs[k.to_s].inspect}" }.join(', ')
|
|
120
|
+
"#<#{self.class} #{pairs}>"
|
|
121
|
+
end
|
|
81
122
|
end
|
|
82
123
|
end
|
|
@@ -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
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'toc_doc/models/resource'
|
|
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
|
+
require 'toc_doc/models/booking_info'
|
|
4
9
|
require 'toc_doc/models/search'
|
|
5
10
|
|
|
6
11
|
require 'toc_doc/models/availability'
|
|
7
12
|
require 'toc_doc/models/availability/collection'
|
|
8
13
|
|
|
9
14
|
require 'toc_doc/models/profile'
|
|
10
|
-
require 'toc_doc/models/speciality'
|
data/lib/toc_doc.rb
CHANGED
|
@@ -89,6 +89,28 @@ module TocDoc
|
|
|
89
89
|
TocDoc::Search.where(**)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# Fetches a full profile page by slug or numeric ID.
|
|
93
|
+
#
|
|
94
|
+
# Delegates to {TocDoc::Profile.find} — see that method for full
|
|
95
|
+
# parameter documentation.
|
|
96
|
+
#
|
|
97
|
+
# @param identifier [String, Integer] profile slug or numeric ID
|
|
98
|
+
# @return [TocDoc::Profile::Practitioner, TocDoc::Profile::Organization]
|
|
99
|
+
def profile(identifier)
|
|
100
|
+
TocDoc::Profile.find(identifier)
|
|
101
|
+
end
|
|
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
|
+
|
|
92
114
|
# @!visibility private
|
|
93
115
|
def method_missing(method_name, ...)
|
|
94
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 01max
|
|
@@ -74,8 +74,11 @@ files:
|
|
|
74
74
|
- lib/toc_doc/http/response/base_middleware.rb
|
|
75
75
|
- lib/toc_doc/middleware/.keep
|
|
76
76
|
- lib/toc_doc/models.rb
|
|
77
|
+
- lib/toc_doc/models/agenda.rb
|
|
77
78
|
- lib/toc_doc/models/availability.rb
|
|
78
79
|
- lib/toc_doc/models/availability/collection.rb
|
|
80
|
+
- lib/toc_doc/models/booking_info.rb
|
|
81
|
+
- lib/toc_doc/models/place.rb
|
|
79
82
|
- lib/toc_doc/models/profile.rb
|
|
80
83
|
- lib/toc_doc/models/profile/organization.rb
|
|
81
84
|
- lib/toc_doc/models/profile/practitioner.rb
|
|
@@ -83,6 +86,7 @@ files:
|
|
|
83
86
|
- lib/toc_doc/models/search.rb
|
|
84
87
|
- lib/toc_doc/models/search/result.rb
|
|
85
88
|
- lib/toc_doc/models/speciality.rb
|
|
89
|
+
- lib/toc_doc/models/visit_motive.rb
|
|
86
90
|
- sig/toc_doc.rbs
|
|
87
91
|
homepage: https://github.com/01max/toc_doc
|
|
88
92
|
licenses:
|