toc_doc 1.2.0 → 1.4.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 +29 -0
- data/README.md +132 -1
- data/TODO.md +26 -22
- data/lib/toc_doc/core/version.rb +1 -1
- data/lib/toc_doc/models/place.rb +34 -0
- data/lib/toc_doc/models/profile/organization.rb +8 -0
- data/lib/toc_doc/models/profile/practitioner.rb +14 -0
- data/lib/toc_doc/models/profile.rb +108 -0
- data/lib/toc_doc/models/resource.rb +43 -2
- data/lib/toc_doc/models/search/result.rb +61 -0
- data/lib/toc_doc/models/search.rb +55 -0
- data/lib/toc_doc/models/speciality.rb +16 -0
- data/lib/toc_doc/models.rb +6 -0
- data/lib/toc_doc.rb +24 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97e03e2d5bbc3fffa25a74e1dc6367d27befc2c25009889ff55b25c2f0abad46
|
|
4
|
+
data.tar.gz: 847c1832c1a927024404004c11c3adb966142d6e93df3de2692d9734f400b2d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe6ff537f03f02a3ecc17013558bcbe165667cc2bb1275d9cea12f28e84457672b278bdfc7f6e00a7f894be7ce5f3587bf02f70d2f6e5f7f70c0c2109372a145
|
|
7
|
+
data.tar.gz: c710d06fb3b1137c1ff2bc4a5f8c9a79c527c7c8e777d99bffd4f11f6799ae4c8dad188a558ac6f29a4696bde45eeb70ffb62b396a2e17cc6805005cf381cb41
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.4.0] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`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]`
|
|
8
|
+
- **`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`
|
|
9
|
+
- **`TocDoc::Profile#skills`** — returns all skills across every practice as an array of `TocDoc::Resource` objects
|
|
10
|
+
- **`TocDoc::Profile#skills_for(practice_id)`** — returns skills for a single practice
|
|
11
|
+
- **`TocDoc.profile`** — top-level shortcut delegating to `TocDoc::Profile.find`
|
|
12
|
+
- **`TocDoc::Resource.main_attrs`** — class macro declaring which attribute keys appear in `#inspect`; inheritable by subclasses
|
|
13
|
+
- **`TocDoc::Resource.normalize_attrs`** — extracted as a public class method (string-key normalisation)
|
|
14
|
+
- **`TocDoc::Resource#inspect`** — custom implementation using `main_attrs` when declared, falling back to all keys
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **`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`
|
|
19
|
+
- **`TocDoc::Profile`** — `main_attrs :id, :partial` declared so inspect output stays concise
|
|
20
|
+
|
|
21
|
+
## [1.3.0] - 2026-03-15
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`TocDoc::Speciality`** — new `Resource`-based model representing a speciality returned by the autocomplete endpoint; exposes `#value`, `#slug`, and `#name` via dot-notation
|
|
26
|
+
- **`TocDoc::Profile`** — new `Resource`-based model for search profile results; `Profile.build(attrs)` factory returns a `Profile::Practitioner` or `Profile::Organization` based on the `owner_type` field; provides `#practitioner?` and `#organization?` predicates
|
|
27
|
+
- **`TocDoc::Profile::Practitioner`** and **`TocDoc::Profile::Organization`** — typed profile subclasses
|
|
28
|
+
- **`TocDoc::Search`** — new service class for the autocomplete endpoint (`/api/searchbar/autocomplete.json`); `Search.where(query:, type: nil, **options)` fetches results and returns a `Search::Result`, or a filtered array when `type:` is one of `'profile'`, `'practitioner'`, `'organization'`, or `'speciality'`
|
|
29
|
+
- **`TocDoc::Search::Result`** — envelope returned by `Search.where`; exposes `#profiles` (typed via `Profile.build`) and `#specialities`; `#filter_by_type` narrows to a specific kind
|
|
30
|
+
- **`TocDoc.search`** — top-level shortcut delegating to `TocDoc::Search.where`
|
|
31
|
+
|
|
3
32
|
## [1.2.0] - 2026-03-08
|
|
4
33
|
|
|
5
34
|
### Added
|
data/README.md
CHANGED
|
@@ -27,6 +27,8 @@ A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-b
|
|
|
27
27
|
- [ENV variables](#environment-variable-overrides)
|
|
28
28
|
4. [Endpoints](#endpoints)
|
|
29
29
|
- [Availabilities](#availabilities)
|
|
30
|
+
- [Search](#search)
|
|
31
|
+
- [Profile](#profile)
|
|
30
32
|
5. [Response objects](#response-objects)
|
|
31
33
|
6. [Pagination](#pagination)
|
|
32
34
|
7. [Error handling](#error-handling)
|
|
@@ -127,7 +129,7 @@ client.get('/availabilities.json', query: { visit_motive_ids: '123', agenda_ids:
|
|
|
127
129
|
| Option | Default | Description |
|
|
128
130
|
|---|---|---|
|
|
129
131
|
| `api_endpoint` | `https://www.doctolib.fr` | Base URL. Change to `.de` / `.it` for other countries. |
|
|
130
|
-
| `user_agent` | `TocDoc Ruby Gem 1.
|
|
132
|
+
| `user_agent` | `TocDoc Ruby Gem 1.4.0` | `User-Agent` header sent with every request. |
|
|
131
133
|
| `default_media_type` | `application/json` | `Accept` and `Content-Type` headers. |
|
|
132
134
|
| `per_page` | `15` | Default number of availability dates per request (capped at `15`). |
|
|
133
135
|
| `middleware` | Retry + RaiseError + JSON + adapter | Full Faraday middleware stack. Override to customise completely. |
|
|
@@ -180,6 +182,65 @@ TocDoc::Availability.where(
|
|
|
180
182
|
|
|
181
183
|
**Return value:** a `TocDoc::Availability::Collection` (see [Response objects](#response-objects)).
|
|
182
184
|
|
|
185
|
+
### Search
|
|
186
|
+
|
|
187
|
+
Query the Doctolib autocomplete endpoint to look up practitioners, organizations, and specialities.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
result = TocDoc::Search.where(query: 'dentiste')
|
|
191
|
+
result.profiles # => [#<TocDoc::Profile::Practitioner ...>, ...]
|
|
192
|
+
result.specialities # => [#<TocDoc::Speciality ...>, ...]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Pass `type:` to receive a filtered array directly:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
# Only specialities
|
|
199
|
+
TocDoc::Search.where(query: 'cardio', type: 'speciality')
|
|
200
|
+
# => [#<TocDoc::Speciality name="Cardiologue">, ...]
|
|
201
|
+
|
|
202
|
+
# Only practitioners
|
|
203
|
+
TocDoc::Search.where(query: 'dupont', type: 'practitioner')
|
|
204
|
+
# => [#<TocDoc::Profile::Practitioner ...>, ...]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Valid `type:` values: `'profile'` (all profiles), `'practitioner'`, `'organization'`, `'speciality'`.
|
|
208
|
+
|
|
209
|
+
`TocDoc.search(...)` is a module-level shortcut with the same signature.
|
|
210
|
+
|
|
211
|
+
**Return value:** a `TocDoc::Search::Result` when `type:` is omitted, or a filtered `Array` otherwise (see [Response objects](#response-objects)).
|
|
212
|
+
|
|
213
|
+
### Profile
|
|
214
|
+
|
|
215
|
+
Fetch a full practitioner or organization profile page by slug or numeric ID.
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# by slug
|
|
219
|
+
profile = TocDoc::Profile.find('jane-doe-bordeaux')
|
|
220
|
+
|
|
221
|
+
# by numeric ID
|
|
222
|
+
profile = TocDoc::Profile.find(1_542_899)
|
|
223
|
+
|
|
224
|
+
# module-level shortcut
|
|
225
|
+
profile = TocDoc.profile('jane-doe-bordeaux')
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`Profile.find` returns a typed `TocDoc::Profile::Practitioner` or `TocDoc::Profile::Organization` instance with `partial: false` (i.e. full profile data).
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
profile.name # => "Dr. Jane Doe"
|
|
232
|
+
profile.partial # => false
|
|
233
|
+
profile.practitioner? # => true
|
|
234
|
+
|
|
235
|
+
profile.skills # => [#<TocDoc::Resource ...>, ...]
|
|
236
|
+
profile.skills_for(377_272) # => skills for a specific practice
|
|
237
|
+
|
|
238
|
+
profile.places.first.city # => "Bordeaux"
|
|
239
|
+
profile.places.first.coordinates # => [44.8386722, -0.5780466]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Return value:** a `TocDoc::Profile::Practitioner` or `TocDoc::Profile::Organization` (see [Response objects](#response-objects)).
|
|
243
|
+
|
|
183
244
|
---
|
|
184
245
|
|
|
185
246
|
## Response objects
|
|
@@ -229,6 +290,76 @@ collection.to_h
|
|
|
229
290
|
# }
|
|
230
291
|
```
|
|
231
292
|
|
|
293
|
+
### `TocDoc::Search::Result`
|
|
294
|
+
|
|
295
|
+
Returned by `TocDoc::Search.where` when `type:` is omitted.
|
|
296
|
+
|
|
297
|
+
| Method | Type | Description |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `#profiles` | `Array<TocDoc::Profile::Practitioner, TocDoc::Profile::Organization>` | All profile results, typed via `Profile.build`. |
|
|
300
|
+
| `#specialities` | `Array<TocDoc::Speciality>` | All speciality results. |
|
|
301
|
+
| `#filter_by_type(type)` | `Array` | Narrows results to `'profile'`, `'practitioner'`, `'organization'`, or `'speciality'`. |
|
|
302
|
+
|
|
303
|
+
### `TocDoc::Profile`
|
|
304
|
+
|
|
305
|
+
Represents a practitioner or organization profile. Can be a lightweight search result (`partial: true`) or a full profile page (`partial: false`).
|
|
306
|
+
|
|
307
|
+
| Method | Type | Description |
|
|
308
|
+
|---|---|---|
|
|
309
|
+
| `Profile.find(identifier)` | `Profile::Practitioner \| Profile::Organization` | Fetches a full profile by slug or numeric ID. Returns `partial: false`. |
|
|
310
|
+
| `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. |
|
|
311
|
+
| `#id` | `String \| Integer` | Profile identifier. |
|
|
312
|
+
| `#partial` | `Boolean` | `true` when built from a search result, `false` when fetched via `Profile.find`. |
|
|
313
|
+
| `#practitioner?` | `Boolean` | `true` when this is a `Profile::Practitioner`. |
|
|
314
|
+
| `#organization?` | `Boolean` | `true` when this is a `Profile::Organization`. |
|
|
315
|
+
| `#places` | `Array<TocDoc::Place>` | Practice locations (available on full profiles). |
|
|
316
|
+
| `#skills` | `Array<TocDoc::Resource>` | All skills across every practice (available on full profiles). |
|
|
317
|
+
| `#skills_for(practice_id)` | `Array<TocDoc::Resource>` | Skills for a single practice by its ID. |
|
|
318
|
+
|
|
319
|
+
`TocDoc::Profile::Practitioner` and `TocDoc::Profile::Organization` are typed subclasses that inherit dot-notation attribute access from `TocDoc::Resource`.
|
|
320
|
+
|
|
321
|
+
### `TocDoc::Place`
|
|
322
|
+
|
|
323
|
+
Represents a practice location returned inside a full profile response. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
324
|
+
|
|
325
|
+
| Method | Type | Description |
|
|
326
|
+
|---|---|---|
|
|
327
|
+
| `#id` | `String` | Practice identifier (e.g. `"practice-125055"`). |
|
|
328
|
+
| `#address` | `String` | Street address. |
|
|
329
|
+
| `#zipcode` | `String` | Postal code. |
|
|
330
|
+
| `#city` | `String` | City name. |
|
|
331
|
+
| `#full_address` | `String` | Combined address string. |
|
|
332
|
+
| `#landline_number` | `String \| nil` | Phone number, if available. |
|
|
333
|
+
| `#latitude` | `Float` | Latitude. |
|
|
334
|
+
| `#longitude` | `Float` | Longitude. |
|
|
335
|
+
| `#elevator` | `Boolean` | Whether the practice has elevator access. |
|
|
336
|
+
| `#handicap` | `Boolean` | Whether the practice is handicap-accessible. |
|
|
337
|
+
| `#formal_name` | `String \| nil` | Formal practice name, if available. |
|
|
338
|
+
| `#coordinates` | `Array<Float>` | Convenience method returning `[latitude, longitude]`. |
|
|
339
|
+
|
|
340
|
+
### `TocDoc::Speciality`
|
|
341
|
+
|
|
342
|
+
Represents a speciality returned by the autocomplete endpoint. Inherits dot-notation attribute access from `TocDoc::Resource`.
|
|
343
|
+
|
|
344
|
+
| Method | Type | Description |
|
|
345
|
+
|---|---|---|
|
|
346
|
+
| `#value` | `Integer` | Numeric speciality identifier. |
|
|
347
|
+
| `#slug` | `String` | URL-friendly identifier. |
|
|
348
|
+
| `#name` | `String` | Human-readable speciality name. |
|
|
349
|
+
|
|
350
|
+
**Example:**
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
result = TocDoc::Search.where(query: 'dermato')
|
|
354
|
+
|
|
355
|
+
result.profiles.first.class # => TocDoc::Profile::Practitioner
|
|
356
|
+
result.profiles.first.practitioner? # => true
|
|
357
|
+
result.profiles.first.name # => "Dr. Jane Smith"
|
|
358
|
+
|
|
359
|
+
result.specialities.first.slug # => "dermatologue"
|
|
360
|
+
result.specialities.first.name # => "Dermatologue"
|
|
361
|
+
```
|
|
362
|
+
|
|
232
363
|
---
|
|
233
364
|
|
|
234
365
|
## Pagination
|
data/TODO.md
CHANGED
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
[POTENTIAL_ENDPOINTS][POTENTIAL_ENDPOINTS.md]
|
|
4
4
|
|
|
5
|
-
## 1.3
|
|
6
|
-
|
|
7
|
-
- [ ] Search (autocomplete)
|
|
8
|
-
- [ ] search profile : https://www.doctolib.fr/api/searchbar/autocomplete.json?search=devun
|
|
9
|
-
- [ ] search specialty : https://www.doctolib.fr/api/searchbar/autocomplete.json?search=dentiste
|
|
10
|
-
|
|
11
|
-
## 1.4
|
|
12
|
-
|
|
13
|
-
- [ ] Profile
|
|
14
|
-
- slug : https://www.doctolib.fr/profiles/mathilde-devun-lesparre-medoc.json
|
|
15
|
-
- id : https://www.doctolib.fr/profiles/926388.json
|
|
16
|
-
|
|
17
5
|
## 1.5
|
|
18
6
|
|
|
19
7
|
- [ ] Booking context
|
|
@@ -33,8 +21,33 @@
|
|
|
33
21
|
- [ ] Authentication module + headers
|
|
34
22
|
- [ ] Auth specs
|
|
35
23
|
|
|
24
|
+
# ???
|
|
25
|
+
|
|
26
|
+
- [ ] Figure what is `organization_statuses` in the autocomplete endpoint and what to do with it.
|
|
27
|
+
|
|
36
28
|
# DONE & RELEASED
|
|
37
29
|
|
|
30
|
+
## 1.4
|
|
31
|
+
|
|
32
|
+
- [x] Profile
|
|
33
|
+
- from slug : https://www.doctolib.fr/profiles/jane-doe-bordeaux.json
|
|
34
|
+
- from id : https://www.doctolib.fr/profiles/926388.json
|
|
35
|
+
|
|
36
|
+
## 1.3
|
|
37
|
+
|
|
38
|
+
- [x] Search (autocomplete)
|
|
39
|
+
- [x] search profile : https://www.doctolib.fr/api/searchbar/autocomplete.json?search=devun
|
|
40
|
+
- [x] search specialty : https://www.doctolib.fr/api/searchbar/autocomplete.json?search=dentiste
|
|
41
|
+
|
|
42
|
+
## 1.2
|
|
43
|
+
|
|
44
|
+
- [x] Rework Availability's client, model and collection architecture.
|
|
45
|
+
|
|
46
|
+
## 1.1
|
|
47
|
+
|
|
48
|
+
### Parse raw API data
|
|
49
|
+
- [x] Parse date / datetime
|
|
50
|
+
|
|
38
51
|
## 1.0
|
|
39
52
|
|
|
40
53
|
### 1 – Skeleton & Tooling
|
|
@@ -88,13 +101,4 @@
|
|
|
88
101
|
- [x] on rubygem
|
|
89
102
|
- [x] release on GH
|
|
90
103
|
- [x] gem.coop/@maxime
|
|
91
|
-
- [x] Add test coverage tool
|
|
92
|
-
|
|
93
|
-
## 1.1
|
|
94
|
-
|
|
95
|
-
### Parse raw API data
|
|
96
|
-
- [x] Parse date / datetime
|
|
97
|
-
|
|
98
|
-
## 1.2
|
|
99
|
-
|
|
100
|
-
- [x] Rework Availability's client, model and collection architecture.
|
|
104
|
+
- [x] Add test coverage tool
|
data/lib/toc_doc/core/version.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# Represents a search profile result (practitioner or organization).
|
|
5
|
+
# Inherits dot-notation attribute access from +TocDoc::Resource+.
|
|
6
|
+
#
|
|
7
|
+
# Use +Profile.build+ to obtain the correctly typed subclass instance.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# profile = TocDoc::Profile.build('owner_type' => 'Account', 'name' => 'Dr Smith')
|
|
11
|
+
# profile.class #=> TocDoc::Profile::Practitioner
|
|
12
|
+
# profile.practitioner? #=> true
|
|
13
|
+
# profile.name #=> "Dr Smith"
|
|
14
|
+
class Profile < Resource
|
|
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 (search 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
|
+
def build(attrs = {})
|
|
28
|
+
attrs = normalize_attrs(attrs)
|
|
29
|
+
return find(attrs['value']) if attrs['force_full_profile'] && attrs['owner_type']
|
|
30
|
+
|
|
31
|
+
case attrs['owner_type']
|
|
32
|
+
when 'Account' then Practitioner.new(attrs.merge('partial' => true))
|
|
33
|
+
when 'Organization' then Organization.new(attrs.merge('partial' => true))
|
|
34
|
+
else build_from_flags(attrs)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Fetches a full profile page by slug or numeric ID.
|
|
39
|
+
#
|
|
40
|
+
# @param identifier [String, Integer] profile slug or numeric ID
|
|
41
|
+
# @return [Profile::Practitioner, Profile::Organization]
|
|
42
|
+
# @raise [ArgumentError] if +identifier+ is +nil+
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# TocDoc::Profile.find('jane-doe-bordeaux')
|
|
46
|
+
# TocDoc::Profile.find(1542899)
|
|
47
|
+
def find(identifier)
|
|
48
|
+
raise ArgumentError, 'identifier cannot be nil' if identifier.nil?
|
|
49
|
+
|
|
50
|
+
data = TocDoc.client.get(format(PATH, identifier: identifier))['data']
|
|
51
|
+
build(profile_attrs(data))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def build_from_flags(attrs)
|
|
57
|
+
if attrs['is_practitioner']
|
|
58
|
+
Practitioner.new(attrs.merge('partial' => false))
|
|
59
|
+
elsif attrs['organization']
|
|
60
|
+
Organization.new(attrs.merge('partial' => false))
|
|
61
|
+
else
|
|
62
|
+
raise ArgumentError, "Unable to determine profile type from attributes: #{attrs.inspect}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def profile_attrs(data)
|
|
67
|
+
data['profile'].merge(
|
|
68
|
+
'speciality' => TocDoc::Speciality.new(data['profile']['speciality'] || {}),
|
|
69
|
+
'places' => Array(data['places']).map { |p| TocDoc::Place.new(p) },
|
|
70
|
+
'legals' => data['legals'],
|
|
71
|
+
'details' => data['details'],
|
|
72
|
+
'fees' => data['fees'],
|
|
73
|
+
'bookable' => data['bookable']
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns all skills across all practices as an array of {TocDoc::Resource}.
|
|
79
|
+
#
|
|
80
|
+
# @return [Array<TocDoc::Resource>]
|
|
81
|
+
def skills
|
|
82
|
+
hash = self['skills_by_practice'] || {}
|
|
83
|
+
hash.values.flatten.map { |s| TocDoc::Resource.new(s) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns skills for a single practice as an array of {TocDoc::Resource}.
|
|
87
|
+
#
|
|
88
|
+
# @param practice_id [Integer, String] the practice ID
|
|
89
|
+
# @return [Array<TocDoc::Resource>]
|
|
90
|
+
def skills_for(practice_id)
|
|
91
|
+
hash = self['skills_by_practice'] || {}
|
|
92
|
+
Array(hash[practice_id.to_s]).map { |s| TocDoc::Resource.new(s) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Boolean] true when this profile is a practitioner
|
|
96
|
+
def practitioner?
|
|
97
|
+
is_a?(Practitioner)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Boolean] true when this profile is an organization
|
|
101
|
+
def organization?
|
|
102
|
+
is_a?(Organization)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
require 'toc_doc/models/profile/practitioner'
|
|
108
|
+
require 'toc_doc/models/profile/organization'
|
|
@@ -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,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'toc_doc/models/profile'
|
|
4
|
+
require 'toc_doc/models/speciality'
|
|
5
|
+
|
|
6
|
+
module TocDoc
|
|
7
|
+
class Search
|
|
8
|
+
# Envelope returned by {TocDoc::Search.where} when no +type:+ filter is given.
|
|
9
|
+
#
|
|
10
|
+
# Wraps the raw API response and exposes typed collections for profiles and
|
|
11
|
+
# specialities. Unlike {TocDoc::Availability::Collection} this class does
|
|
12
|
+
# NOT include +Enumerable+ — the dual-type nature of the result does not
|
|
13
|
+
# lend itself to a single iteration interface.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# result = TocDoc::Search.where(query: 'dentiste')
|
|
17
|
+
# result.profiles #=> [#<TocDoc::Profile::Practitioner>, ...]
|
|
18
|
+
# result.specialities #=> [#<TocDoc::Speciality>, ...]
|
|
19
|
+
class Result
|
|
20
|
+
# @param data [Hash] raw parsed response body from the autocomplete endpoint
|
|
21
|
+
def initialize(data)
|
|
22
|
+
@profiles = build_profiles(data['profiles'])
|
|
23
|
+
@specialities = build_specialities(data['specialities'])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# All profile results, typed as {TocDoc::Profile::Practitioner} or
|
|
27
|
+
# {TocDoc::Profile::Organization} via {TocDoc::Profile.build}.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<TocDoc::Profile::Practitioner, TocDoc::Profile::Organization>]
|
|
30
|
+
attr_reader :profiles
|
|
31
|
+
|
|
32
|
+
# All speciality results as {TocDoc::Speciality} instances.
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<TocDoc::Speciality>]
|
|
35
|
+
attr_reader :specialities
|
|
36
|
+
|
|
37
|
+
# Returns a subset of results narrowed to the given type.
|
|
38
|
+
#
|
|
39
|
+
# @param type [String] one of +'profile'+, +'practitioner'+, +'organization'+, +'speciality'+
|
|
40
|
+
# @return [Array<TocDoc::Profile>, Array<TocDoc::Speciality>]
|
|
41
|
+
def filter_by_type(type)
|
|
42
|
+
case type
|
|
43
|
+
when 'profile' then profiles
|
|
44
|
+
when 'practitioner' then profiles.select(&:practitioner?)
|
|
45
|
+
when 'organization' then profiles.select(&:organization?)
|
|
46
|
+
when 'speciality' then specialities
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_profiles(raw)
|
|
53
|
+
Array(raw).map { |attrs| TocDoc::Profile.build(attrs) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_specialities(raw)
|
|
57
|
+
Array(raw).map { |attrs| TocDoc::Speciality.new(attrs) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'toc_doc/models/search/result'
|
|
4
|
+
|
|
5
|
+
module TocDoc
|
|
6
|
+
# Entry point for the autocomplete / search endpoint.
|
|
7
|
+
#
|
|
8
|
+
# Unlike {TocDoc::Availability}, +Search+ is not itself a resource — it is a
|
|
9
|
+
# plain service class that wraps the API call and returns a typed result.
|
|
10
|
+
#
|
|
11
|
+
# @example Fetch everything
|
|
12
|
+
# result = TocDoc::Search.where(query: 'dentiste')
|
|
13
|
+
# result #=> #<TocDoc::Search::Result>
|
|
14
|
+
# result.profiles #=> [#<TocDoc::Profile::Practitioner>, ...]
|
|
15
|
+
#
|
|
16
|
+
# @example Filter by type
|
|
17
|
+
# TocDoc::Search.where(query: 'dentiste', type: 'practitioner')
|
|
18
|
+
# #=> [#<TocDoc::Profile::Practitioner>, ...]
|
|
19
|
+
class Search
|
|
20
|
+
PATH = '/api/searchbar/autocomplete.json'
|
|
21
|
+
VALID_TYPES = %w[profile practitioner organization speciality].freeze
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Queries the autocomplete endpoint and returns a {Search::Result} or a
|
|
25
|
+
# filtered array.
|
|
26
|
+
#
|
|
27
|
+
# The +type:+ keyword is handled client-side only — it is never forwarded
|
|
28
|
+
# to the API. The full response is always fetched; narrowing happens after.
|
|
29
|
+
#
|
|
30
|
+
# @param query [String] the search term
|
|
31
|
+
# @param type [String, nil] optional filter; one of +'profile'+,
|
|
32
|
+
# +'practitioner'+, +'organization'+, +'speciality'+
|
|
33
|
+
# @param options [Hash] additional query params forwarded verbatim to the API
|
|
34
|
+
# @return [Search::Result] when +type:+ is +nil+
|
|
35
|
+
# @return [Array<TocDoc::Profile>] when +type:+ is +'profile'+, +'practitioner'+,
|
|
36
|
+
# or +'organization'+
|
|
37
|
+
# @return [Array<TocDoc::Speciality>] when +type:+ is +'speciality'+
|
|
38
|
+
# @raise [ArgumentError] if +type:+ is not +nil+ and not in {VALID_TYPES}
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# TocDoc::Search.where(query: 'derma', type: 'speciality')
|
|
42
|
+
# #=> [#<TocDoc::Speciality name="Dermatologue">, ...]
|
|
43
|
+
def where(query:, type: nil, **options)
|
|
44
|
+
if !type.nil? && !VALID_TYPES.include?(type)
|
|
45
|
+
raise ArgumentError, "Invalid type #{type.inspect}. Must be one of: #{VALID_TYPES.join(', ')}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
data = TocDoc.client.get(PATH, query: { search: query, **options })
|
|
49
|
+
result = Result.new(data)
|
|
50
|
+
|
|
51
|
+
type.nil? ? result : result.filter_by_type(type)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# Represents a speciality returned by the autocomplete endpoint.
|
|
5
|
+
#
|
|
6
|
+
# All fields (+value+, +slug+, +name+) are primitives and are accessed via
|
|
7
|
+
# dot-notation inherited from {TocDoc::Resource}.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# speciality = TocDoc::Speciality.new('value' => 228, 'slug' => 'homeopathe', 'name' => 'Homéopathe')
|
|
11
|
+
# speciality.value #=> 228
|
|
12
|
+
# speciality.slug #=> "homeopathe"
|
|
13
|
+
# speciality.name #=> "Homéopathe"
|
|
14
|
+
class Speciality < Resource
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/toc_doc/models.rb
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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/search'
|
|
7
|
+
|
|
4
8
|
require 'toc_doc/models/availability'
|
|
5
9
|
require 'toc_doc/models/availability/collection'
|
|
10
|
+
|
|
11
|
+
require 'toc_doc/models/profile'
|
data/lib/toc_doc.rb
CHANGED
|
@@ -76,6 +76,30 @@ module TocDoc
|
|
|
76
76
|
TocDoc::Availability.where(**)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Queries the autocomplete / search endpoint.
|
|
80
|
+
#
|
|
81
|
+
# Delegates to {TocDoc::Search.where} — see that method for full
|
|
82
|
+
# parameter documentation.
|
|
83
|
+
#
|
|
84
|
+
# @return [TocDoc::Search::Result] when called without +type:+
|
|
85
|
+
# @return [Array<TocDoc::Profile>] when +type:+ is +'profile'+,
|
|
86
|
+
# +'practitioner'+, or +'organization'+
|
|
87
|
+
# @return [Array<TocDoc::Speciality>] when +type:+ is +'speciality'+
|
|
88
|
+
def search(**)
|
|
89
|
+
TocDoc::Search.where(**)
|
|
90
|
+
end
|
|
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
|
+
|
|
79
103
|
# @!visibility private
|
|
80
104
|
def method_missing(method_name, ...)
|
|
81
105
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 01max
|
|
@@ -76,7 +76,14 @@ files:
|
|
|
76
76
|
- lib/toc_doc/models.rb
|
|
77
77
|
- lib/toc_doc/models/availability.rb
|
|
78
78
|
- lib/toc_doc/models/availability/collection.rb
|
|
79
|
+
- lib/toc_doc/models/place.rb
|
|
80
|
+
- lib/toc_doc/models/profile.rb
|
|
81
|
+
- lib/toc_doc/models/profile/organization.rb
|
|
82
|
+
- lib/toc_doc/models/profile/practitioner.rb
|
|
79
83
|
- lib/toc_doc/models/resource.rb
|
|
84
|
+
- lib/toc_doc/models/search.rb
|
|
85
|
+
- lib/toc_doc/models/search/result.rb
|
|
86
|
+
- lib/toc_doc/models/speciality.rb
|
|
80
87
|
- sig/toc_doc.rbs
|
|
81
88
|
homepage: https://github.com/01max/toc_doc
|
|
82
89
|
licenses:
|