toc_doc 1.1.0 → 1.3.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 +31 -0
- data/POTENTIAL_ENDPOINTS.md +31 -0
- data/README.md +144 -57
- data/TODO.md +33 -37
- data/lib/toc_doc/client.rb +0 -6
- data/lib/toc_doc/core/configurable.rb +0 -3
- data/lib/toc_doc/core/connection.rb +10 -8
- data/lib/toc_doc/core/default.rb +0 -17
- data/lib/toc_doc/core/uri_utils.rb +5 -5
- data/lib/toc_doc/core/version.rb +1 -1
- data/lib/toc_doc/models/availability/collection.rb +103 -0
- data/lib/toc_doc/models/availability.rb +70 -1
- data/lib/toc_doc/models/profile/organization.rb +8 -0
- data/lib/toc_doc/models/profile/practitioner.rb +8 -0
- data/lib/toc_doc/models/profile.rb +42 -0
- 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 +5 -1
- data/lib/toc_doc.rb +23 -4
- metadata +9 -3
- data/lib/toc_doc/client/availabilities.rb +0 -118
- data/lib/toc_doc/models/response/availability.rb +0 -79
|
@@ -5,13 +5,13 @@ module TocDoc
|
|
|
5
5
|
#
|
|
6
6
|
# Doctolib expects certain ID list parameters to be dash-joined strings
|
|
7
7
|
# rather than standard repeated/bracket array notation. Include this module
|
|
8
|
-
#
|
|
8
|
+
# and call +dashed_ids+ explicitly for each such param:
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# class TocDoc::Availability
|
|
11
|
+
# extend TocDoc::UriUtils
|
|
12
12
|
#
|
|
13
|
-
# def
|
|
14
|
-
# get('/availabilities.json', query: {
|
|
13
|
+
# def self.where(visit_motive_ids:, agenda_ids:, **opts)
|
|
14
|
+
# client.get('/availabilities.json', query: {
|
|
15
15
|
# visit_motive_ids: dashed_ids(visit_motive_ids),
|
|
16
16
|
# agenda_ids: dashed_ids(agenda_ids),
|
|
17
17
|
# **opts
|
data/lib/toc_doc/core/version.rb
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'toc_doc/models/availability'
|
|
5
|
+
|
|
6
|
+
module TocDoc
|
|
7
|
+
class Availability
|
|
8
|
+
# An Enumerable collection of {TocDoc::Availability} instances returned
|
|
9
|
+
# by {TocDoc::Availability.where}.
|
|
10
|
+
#
|
|
11
|
+
# @example Iterate over available slots
|
|
12
|
+
# collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
|
|
13
|
+
# collection.each { |avail| puts avail.date }
|
|
14
|
+
#
|
|
15
|
+
# @example Access metadata
|
|
16
|
+
# collection.total #=> 5
|
|
17
|
+
# collection.next_slot #=> "2026-02-28T10:00:00.000+01:00"
|
|
18
|
+
class Collection
|
|
19
|
+
include Enumerable
|
|
20
|
+
|
|
21
|
+
attr_reader :path, :query
|
|
22
|
+
|
|
23
|
+
# @param data [Hash] parsed first-page response body
|
|
24
|
+
# @param query [Hash] original query params (used to build next-page requests)
|
|
25
|
+
# @param path [String] API path for subsequent requests
|
|
26
|
+
def initialize(data, query: {}, path: '/availabilities.json')
|
|
27
|
+
@data = data.dup
|
|
28
|
+
@query = query
|
|
29
|
+
@path = path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Iterates over {TocDoc::Availability} instances that have at least one slot.
|
|
33
|
+
#
|
|
34
|
+
# @yieldparam availability [TocDoc::Availability]
|
|
35
|
+
# @return [Enumerator] if no block given
|
|
36
|
+
def each(&)
|
|
37
|
+
filtered_entries.each(&)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The total number of available slots in the collection.
|
|
41
|
+
#
|
|
42
|
+
# @return [Integer]
|
|
43
|
+
def total
|
|
44
|
+
@data['total']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The nearest available appointment slot.
|
|
48
|
+
#
|
|
49
|
+
# Returns the +next_slot+ value from the API when present (which only
|
|
50
|
+
# occurs when none of the loaded dates have any slots). Otherwise
|
|
51
|
+
# returns the first slot of the first date that has one.
|
|
52
|
+
#
|
|
53
|
+
# @return [String, nil] ISO 8601 datetime string, or +nil+ when unavailable
|
|
54
|
+
def next_slot
|
|
55
|
+
return @data['next_slot'] if @data.key?('next_slot')
|
|
56
|
+
|
|
57
|
+
Array(@data['availabilities']).each do |entry|
|
|
58
|
+
slots = Array(entry['slots'])
|
|
59
|
+
return slots.first unless slots.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# All date entries — including those with no slots — as {TocDoc::Availability}
|
|
66
|
+
# objects.
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<TocDoc::Availability>]
|
|
69
|
+
def raw_availabilities
|
|
70
|
+
Array(@data['availabilities']).map { |entry| TocDoc::Availability.new(entry) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns a plain Hash representation of the collection.
|
|
74
|
+
#
|
|
75
|
+
# The +availabilities+ key contains only dates with slots (filtered),
|
|
76
|
+
# serialised back to plain Hashes.
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash{String => Object}]
|
|
79
|
+
def to_h
|
|
80
|
+
@data.merge('availabilities' => filtered_entries.map(&:to_h))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Fetches the next window of availabilities (starting the day after the
|
|
84
|
+
# last date in the current collection) and merges them in.
|
|
85
|
+
#
|
|
86
|
+
# @param page_data [Hash] parsed response body to merge into this collection
|
|
87
|
+
# @return [self]
|
|
88
|
+
def merge_page!(page_data)
|
|
89
|
+
@data['availabilities'] = @data.fetch('availabilities', []) + page_data.fetch('availabilities', [])
|
|
90
|
+
@data['total'] = @data.fetch('total', 0) + page_data.fetch('total', 0)
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def filtered_entries
|
|
97
|
+
Array(@data['availabilities'])
|
|
98
|
+
.select { |entry| Array(entry['slots']).any? }
|
|
99
|
+
.map { |entry| TocDoc::Availability.new(entry) }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'date'
|
|
4
|
+
require 'toc_doc/core/uri_utils'
|
|
4
5
|
|
|
5
6
|
module TocDoc
|
|
6
7
|
# Represents a single availability date entry returned by the Doctolib API.
|
|
@@ -12,9 +13,77 @@ module TocDoc
|
|
|
12
13
|
# avail.slots #=> [#<DateTime: 2026-02-28T10:00:00.000+01:00>]
|
|
13
14
|
# avail.raw_slots #=> ["2026-02-28T10:00:00.000+01:00"]
|
|
14
15
|
class Availability < Resource
|
|
16
|
+
extend TocDoc::UriUtils
|
|
17
|
+
|
|
15
18
|
attr_reader :date, :slots
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
PATH = '/availabilities.json'
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Fetches availabilities from the API and returns an {Availability::Collection}.
|
|
24
|
+
#
|
|
25
|
+
# When the API response contains a +next_slot+ key — indicating that no
|
|
26
|
+
# date in the current window has available slots — a second request is
|
|
27
|
+
# made automatically from that date before the collection is returned.
|
|
28
|
+
#
|
|
29
|
+
# @param visit_motive_ids [Integer, String, Array<Integer>]
|
|
30
|
+
# one or more visit-motive IDs (dash-joined for the API)
|
|
31
|
+
# @param agenda_ids [Integer, String, Array<Integer>]
|
|
32
|
+
# one or more agenda IDs (dash-joined for the API)
|
|
33
|
+
# @param start_date [Date, String]
|
|
34
|
+
# earliest date to search from (default: +Date.today+)
|
|
35
|
+
# @param limit [Integer]
|
|
36
|
+
# maximum availability dates per page (default: +TocDoc.per_page+)
|
|
37
|
+
# @param options [Hash]
|
|
38
|
+
# additional query params forwarded verbatim to the API
|
|
39
|
+
# @return [TocDoc::Availability::Collection]
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# TocDoc::Availability.where(
|
|
43
|
+
# visit_motive_ids: 7_767_829,
|
|
44
|
+
# agenda_ids: [1_101_600],
|
|
45
|
+
# start_date: Date.today
|
|
46
|
+
# ).each { |avail| puts avail.date }
|
|
47
|
+
def where(visit_motive_ids:, agenda_ids:, start_date: Date.today,
|
|
48
|
+
limit: TocDoc.per_page, **options)
|
|
49
|
+
client = TocDoc.client
|
|
50
|
+
query = build_query(visit_motive_ids, agenda_ids, start_date, limit, options)
|
|
51
|
+
data = client.get(PATH, query: query)
|
|
52
|
+
|
|
53
|
+
merge_next_page(client, query, data) if data['next_slot']
|
|
54
|
+
|
|
55
|
+
Collection.new(data, query: query, path: PATH)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def merge_next_page(client, query, current_page)
|
|
61
|
+
next_date = Date.parse(current_page['next_slot']).to_s
|
|
62
|
+
next_page = client.get(PATH, query: query.merge(start_date: next_date))
|
|
63
|
+
merge_page_data(current_page, next_page)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def merge_page_data(current_page, next_page)
|
|
67
|
+
current_page['availabilities'] =
|
|
68
|
+
current_page.fetch('availabilities', []) + next_page.fetch('availabilities', [])
|
|
69
|
+
current_page['total'] = current_page.fetch('total', 0) + next_page.fetch('total', 0)
|
|
70
|
+
current_page.delete('next_slot')
|
|
71
|
+
current_page['next_slot'] = next_page['next_slot'] if next_page.key?('next_slot')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_query(visit_motive_ids, agenda_ids, start_date, limit, extra)
|
|
75
|
+
{
|
|
76
|
+
visit_motive_ids: dashed_ids(visit_motive_ids),
|
|
77
|
+
agenda_ids: dashed_ids(agenda_ids),
|
|
78
|
+
start_date: start_date.to_s,
|
|
79
|
+
limit: [limit.to_i, TocDoc::Default::MAX_PER_PAGE].min,
|
|
80
|
+
**extra
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param attrs [Hash] raw attributes from the API response; expected to include
|
|
86
|
+
# a +date+ key (ISO 8601 date string) and a +slots+ key (Array of ISO 8601 datetime strings)
|
|
18
87
|
def initialize(*attrs)
|
|
19
88
|
super
|
|
20
89
|
raw = build_raw(@attrs)
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
# Factory — returns a +Profile::Practitioner+ or +Profile::Organization+
|
|
16
|
+
# depending on the +owner_type+ field of the raw attribute hash.
|
|
17
|
+
#
|
|
18
|
+
# @param attrs [Hash] raw attribute hash from the API response
|
|
19
|
+
# @return [Profile::Practitioner, Profile::Organization]
|
|
20
|
+
def self.build(attrs = {})
|
|
21
|
+
case attrs['owner_type'] || attrs[:owner_type]
|
|
22
|
+
when 'Account'
|
|
23
|
+
Practitioner.new(attrs)
|
|
24
|
+
else
|
|
25
|
+
Organization.new(attrs)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Boolean] true when this profile is a practitioner
|
|
30
|
+
def practitioner?
|
|
31
|
+
is_a?(Practitioner)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] true when this profile is an organization
|
|
35
|
+
def organization?
|
|
36
|
+
is_a?(Organization)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
require 'toc_doc/models/profile/practitioner'
|
|
42
|
+
require 'toc_doc/models/profile/organization'
|
|
@@ -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,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'toc_doc/models/resource'
|
|
4
|
+
require 'toc_doc/models/search'
|
|
5
|
+
|
|
4
6
|
require 'toc_doc/models/availability'
|
|
7
|
+
require 'toc_doc/models/availability/collection'
|
|
5
8
|
|
|
6
|
-
require 'toc_doc/models/
|
|
9
|
+
require 'toc_doc/models/profile'
|
|
10
|
+
require 'toc_doc/models/speciality'
|
data/lib/toc_doc.rb
CHANGED
|
@@ -16,13 +16,9 @@ require 'toc_doc/client'
|
|
|
16
16
|
# Configuration can be set at the module level and will be inherited by every
|
|
17
17
|
# {TocDoc::Client} instance created via {.client} or {.setup}.
|
|
18
18
|
#
|
|
19
|
-
# Any method available on {TocDoc::Client} can be called directly on `TocDoc`
|
|
20
|
-
# and will be forwarded to the memoized {.client}.
|
|
21
|
-
#
|
|
22
19
|
# @example Quick start
|
|
23
20
|
# TocDoc.setup do |config|
|
|
24
21
|
# config.api_endpoint = 'https://www.doctolib.de'
|
|
25
|
-
# config.auto_paginate = true
|
|
26
22
|
# end
|
|
27
23
|
#
|
|
28
24
|
# TocDoc.availabilities(
|
|
@@ -70,6 +66,29 @@ module TocDoc
|
|
|
70
66
|
client
|
|
71
67
|
end
|
|
72
68
|
|
|
69
|
+
# Returns available appointment slots.
|
|
70
|
+
#
|
|
71
|
+
# Delegates to {TocDoc::Availability.where} — see that method for full
|
|
72
|
+
# parameter documentation.
|
|
73
|
+
#
|
|
74
|
+
# @return [TocDoc::Availability::Collection]
|
|
75
|
+
def availabilities(**)
|
|
76
|
+
TocDoc::Availability.where(**)
|
|
77
|
+
end
|
|
78
|
+
|
|
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
|
+
|
|
73
92
|
# @!visibility private
|
|
74
93
|
def method_missing(method_name, ...)
|
|
75
94
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 01max
|
|
@@ -57,12 +57,12 @@ files:
|
|
|
57
57
|
- CHANGELOG.md
|
|
58
58
|
- CODE_OF_CONDUCT.md
|
|
59
59
|
- LICENSE.md
|
|
60
|
+
- POTENTIAL_ENDPOINTS.md
|
|
60
61
|
- README.md
|
|
61
62
|
- Rakefile
|
|
62
63
|
- TODO.md
|
|
63
64
|
- lib/toc_doc.rb
|
|
64
65
|
- lib/toc_doc/client.rb
|
|
65
|
-
- lib/toc_doc/client/availabilities.rb
|
|
66
66
|
- lib/toc_doc/core/authentication.rb
|
|
67
67
|
- lib/toc_doc/core/configurable.rb
|
|
68
68
|
- lib/toc_doc/core/connection.rb
|
|
@@ -75,8 +75,14 @@ files:
|
|
|
75
75
|
- lib/toc_doc/middleware/.keep
|
|
76
76
|
- lib/toc_doc/models.rb
|
|
77
77
|
- lib/toc_doc/models/availability.rb
|
|
78
|
+
- lib/toc_doc/models/availability/collection.rb
|
|
79
|
+
- lib/toc_doc/models/profile.rb
|
|
80
|
+
- lib/toc_doc/models/profile/organization.rb
|
|
81
|
+
- lib/toc_doc/models/profile/practitioner.rb
|
|
78
82
|
- lib/toc_doc/models/resource.rb
|
|
79
|
-
- lib/toc_doc/models/
|
|
83
|
+
- lib/toc_doc/models/search.rb
|
|
84
|
+
- lib/toc_doc/models/search/result.rb
|
|
85
|
+
- lib/toc_doc/models/speciality.rb
|
|
80
86
|
- sig/toc_doc.rbs
|
|
81
87
|
homepage: https://github.com/01max/toc_doc
|
|
82
88
|
licenses:
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'date'
|
|
4
|
-
|
|
5
|
-
module TocDoc
|
|
6
|
-
class Client
|
|
7
|
-
# Endpoint module for the Doctolib availabilities API.
|
|
8
|
-
#
|
|
9
|
-
# Included into {TocDoc::Client}; methods delegate to
|
|
10
|
-
# {TocDoc::Connection#get} via the enclosing client instance.
|
|
11
|
-
#
|
|
12
|
-
# @see https://www.doctolib.fr/availabilities.json Doctolib availability endpoint
|
|
13
|
-
module Availabilities
|
|
14
|
-
# Returns available appointment slots for the given visit motives and
|
|
15
|
-
# agendas.
|
|
16
|
-
#
|
|
17
|
-
# When +auto_paginate+ is enabled, all pages of results are
|
|
18
|
-
# fetched and merged automatically.
|
|
19
|
-
#
|
|
20
|
-
# @param visit_motive_ids [Integer, String, Array<Integer>]
|
|
21
|
-
# one or more visit-motive IDs (dash-joined for the API)
|
|
22
|
-
# @param agenda_ids [Integer, String, Array<Integer>]
|
|
23
|
-
# one or more agenda IDs (dash-joined for the API)
|
|
24
|
-
# @param start_date [Date, String]
|
|
25
|
-
# earliest date to search from (default: +Date.today+)
|
|
26
|
-
# @param limit [Integer]
|
|
27
|
-
# maximum number of availability dates per page
|
|
28
|
-
# (default: +per_page+ config)
|
|
29
|
-
# @param options [Hash]
|
|
30
|
-
# additional query params forwarded verbatim to the API
|
|
31
|
-
# (e.g. +practice_ids:+, +telehealth:+)
|
|
32
|
-
# @return [TocDoc::Response::Availability] structured response object
|
|
33
|
-
#
|
|
34
|
-
# @example Fetch availabilities for a single practitioner
|
|
35
|
-
# client.availabilities(
|
|
36
|
-
# visit_motive_ids: 7_767_829,
|
|
37
|
-
# agenda_ids: [1_101_600],
|
|
38
|
-
# practice_ids: 377_272,
|
|
39
|
-
# telehealth: false
|
|
40
|
-
# )
|
|
41
|
-
#
|
|
42
|
-
# @example Via the module-level shortcut
|
|
43
|
-
# TocDoc.availabilities(visit_motive_ids: 123, agenda_ids: 456)
|
|
44
|
-
def availabilities(visit_motive_ids:, agenda_ids:, start_date: Date.today, limit: per_page, **options)
|
|
45
|
-
base_query = build_availability_query(visit_motive_ids, agenda_ids, start_date, limit, options)
|
|
46
|
-
|
|
47
|
-
response = paginate('/availabilities.json', query: base_query) do |acc, last_resp|
|
|
48
|
-
paginate_availability_page(acc, last_resp, base_query)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
TocDoc::Response::Availability.new(response)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
# Builds the query hash sent to the availabilities endpoint.
|
|
57
|
-
#
|
|
58
|
-
# @param visit_motive_ids [Integer, String, Array] raw motive IDs
|
|
59
|
-
# @param agenda_ids [Integer, String, Array] raw agenda IDs
|
|
60
|
-
# @param start_date [Date, String] earliest search date
|
|
61
|
-
# @param limit [Integer] page size
|
|
62
|
-
# @param extra [Hash] additional query params
|
|
63
|
-
# @return [Hash{Symbol => Object}] ready-to-send query hash
|
|
64
|
-
def build_availability_query(visit_motive_ids, agenda_ids, start_date, limit, extra)
|
|
65
|
-
{
|
|
66
|
-
visit_motive_ids: dashed_ids(visit_motive_ids),
|
|
67
|
-
agenda_ids: dashed_ids(agenda_ids),
|
|
68
|
-
start_date: start_date.to_s,
|
|
69
|
-
limit: [limit.to_i, TocDoc::Default::MAX_PER_PAGE].min,
|
|
70
|
-
**extra
|
|
71
|
-
}
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Merges the latest page body into the accumulator and returns options
|
|
75
|
-
# for the next page, or +nil+ to halt pagination.
|
|
76
|
-
#
|
|
77
|
-
# On the first yield +acc+ *is* the first-page body (identical object),
|
|
78
|
-
# so the merge step is skipped.
|
|
79
|
-
#
|
|
80
|
-
# @param acc [Hash] growing accumulator
|
|
81
|
-
# @param last_resp [Faraday::Response] the most-recent raw response
|
|
82
|
-
# @param base_query [Hash] the original query hash
|
|
83
|
-
# @return [Hash, nil] options for the next page, or +nil+ to stop
|
|
84
|
-
def paginate_availability_page(acc, last_resp, base_query)
|
|
85
|
-
latest = last_resp.body
|
|
86
|
-
|
|
87
|
-
merge_availability_page(acc, latest) unless acc.equal?(latest)
|
|
88
|
-
availability_next_page_options(latest, base_query)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Merges a new page body into the running accumulator.
|
|
92
|
-
#
|
|
93
|
-
# @param acc [Hash] the accumulator hash
|
|
94
|
-
# @param latest [Hash] the most-recent page body
|
|
95
|
-
# @return [void]
|
|
96
|
-
def merge_availability_page(acc, latest)
|
|
97
|
-
acc['availabilities'] = (acc['availabilities'] || []) + (latest['availabilities'] || [])
|
|
98
|
-
acc['total'] = (acc['total'] || 0) + (latest['total'] || 0)
|
|
99
|
-
acc['next_slot'] = latest['next_slot']
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Determines the options for the next page of availabilities, or +nil+
|
|
103
|
-
# if pagination should stop.
|
|
104
|
-
#
|
|
105
|
-
# @param latest [Hash] the most-recent page body
|
|
106
|
-
# @param base_query [Hash] the original query hash
|
|
107
|
-
# @return [Hash, nil] next-page options, or +nil+ to halt
|
|
108
|
-
def availability_next_page_options(latest, base_query)
|
|
109
|
-
avails = latest['availabilities'] || []
|
|
110
|
-
last_date_str = avails.last&.dig('date')
|
|
111
|
-
return unless last_date_str && latest['next_slot']
|
|
112
|
-
|
|
113
|
-
next_start = (Date.parse(last_date_str) + 1).to_s
|
|
114
|
-
{ query: base_query.merge(start_date: next_start) }
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|