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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcac91743cd8f8d50d032638c3a690f88c786a369960d8b5df8bdead6006149c
4
- data.tar.gz: d8df90b16b2ff3e2311af4751396ff1ae54173575e4507989df0e7c910bd82eb
3
+ metadata.gz: 97e03e2d5bbc3fffa25a74e1dc6367d27befc2c25009889ff55b25c2f0abad46
4
+ data.tar.gz: 847c1832c1a927024404004c11c3adb966142d6e93df3de2692d9734f400b2d2
5
5
  SHA512:
6
- metadata.gz: e8886203725b1dd9bbd59e62ee56aaf4f0965c908b31d478568c9d0149a7062f7059509f5692f18273e08b4a05d90d8a6d6e8bb02e89342ab2d1a18e92b2b16a
7
- data.tar.gz: fd9271676246e8da0e7e626ebc3c10316f5b86b2ab6fc811a67986bb276aa1e817c74f7d17fec64372cfb15048638b1aac32e08b2dce981142646ed4a1289977
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.2.0` | `User-Agent` header sent with every request. |
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
@@ -4,5 +4,5 @@ module TocDoc
4
4
  # The current version of the TocDoc gem.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '1.2.0'
7
+ VERSION = '1.4.0'
8
8
  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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ class Profile
5
+ # An organization profile.
6
+ class Organization < Profile; end
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ class Profile
5
+ # A practitioner profile (raw +owner_type: "Account"+).
6
+ class Practitioner < Profile
7
+ main_attrs :name_with_title
8
+
9
+ def to_s
10
+ name_with_title || name
11
+ end
12
+ end
13
+ end
14
+ 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 = attrs.transform_keys(&:to_s)
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 == other.transform_keys(&:to_s)
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
@@ -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.2.0
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: