calendlyr 0.7.5 → 1.0.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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -5
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +98 -0
  5. data/README.md +209 -73
  6. data/calendlyr.gemspec +8 -6
  7. data/docs/resources/activity_log/list_activity_log_entries.md +2 -1
  8. data/docs/resources/availabilities/user_availability_schedule.md +3 -2
  9. data/docs/resources/availabilities/user_busy_time.md +3 -2
  10. data/docs/resources/data_compliance.md +1 -1
  11. data/docs/resources/event_types/availability_schedule.md +28 -0
  12. data/docs/resources/event_types/available_time.md +15 -11
  13. data/docs/resources/event_types/event_type.md +21 -9
  14. data/docs/resources/event_types/membership.md +15 -16
  15. data/docs/resources/events/cancellation.md +1 -1
  16. data/docs/resources/events/event.md +6 -5
  17. data/docs/resources/events/invitee.md +18 -0
  18. data/docs/resources/events/invitee_no_show.md +2 -1
  19. data/docs/resources/groups/group.md +2 -1
  20. data/docs/resources/locations/location.md +16 -0
  21. data/docs/resources/organizations/membership.md +3 -2
  22. data/docs/resources/organizations/organization.md +13 -9
  23. data/docs/resources/outgoing_communications/outgoing_communication.md +28 -0
  24. data/docs/resources/routing_forms/routing_form.md +2 -1
  25. data/docs/resources/routing_forms/submission.md +2 -1
  26. data/docs/resources/scheduling_links/scheduling_link.md +26 -0
  27. data/docs/resources/share.md +4 -10
  28. data/docs/resources/webhooks/invitee_payload.md +10 -16
  29. data/docs/resources/webhooks/payload.md +32 -3
  30. data/docs/resources/webhooks/sample.md +23 -0
  31. data/docs/resources/webhooks/subscription.md +10 -6
  32. data/lib/calendlyr/client.rb +7 -2
  33. data/lib/calendlyr/collection.rb +42 -9
  34. data/lib/calendlyr/configuration.rb +24 -0
  35. data/lib/calendlyr/error.rb +65 -27
  36. data/lib/calendlyr/object.rb +85 -13
  37. data/lib/calendlyr/objects/event_type.rb +0 -4
  38. data/lib/calendlyr/objects/event_types/availability_schedule.rb +8 -0
  39. data/lib/calendlyr/objects/event_types/available_time.rb +5 -1
  40. data/lib/calendlyr/objects/event_types/membership.rb +4 -7
  41. data/lib/calendlyr/objects/events/invitee.rb +1 -1
  42. data/lib/calendlyr/objects/location.rb +4 -0
  43. data/lib/calendlyr/objects/organization.rb +0 -4
  44. data/lib/calendlyr/objects/outgoing_communication.rb +6 -0
  45. data/lib/calendlyr/objects/scheduling_link.rb +2 -5
  46. data/lib/calendlyr/objects/share.rb +0 -5
  47. data/lib/calendlyr/objects/webhooks/payload.rb +30 -0
  48. data/lib/calendlyr/resource.rb +84 -12
  49. data/lib/calendlyr/resources/availability.rb +14 -2
  50. data/lib/calendlyr/resources/event_types.rb +45 -5
  51. data/lib/calendlyr/resources/events.rb +20 -2
  52. data/lib/calendlyr/resources/groups.rb +14 -2
  53. data/lib/calendlyr/resources/locations.rb +13 -0
  54. data/lib/calendlyr/resources/organizations.rb +25 -3
  55. data/lib/calendlyr/resources/outgoing_communications.rb +11 -3
  56. data/lib/calendlyr/resources/routing_forms.rb +14 -2
  57. data/lib/calendlyr/resources/scheduling_links.rb +5 -2
  58. data/lib/calendlyr/resources/shares.rb +1 -0
  59. data/lib/calendlyr/resources/webhooks.rb +11 -3
  60. data/lib/calendlyr/version.rb +1 -1
  61. data/lib/calendlyr/webhook.rb +105 -0
  62. data/lib/calendlyr.rb +50 -0
  63. data/logos/calendlyr.png +0 -0
  64. data/logos/calendlyr_bg_white.png +0 -0
  65. data/test/calendlyr/client_test.rb +29 -0
  66. data/test/calendlyr/collection_test.rb +168 -0
  67. data/test/calendlyr/configuration_test.rb +157 -0
  68. data/test/calendlyr/object_test.rb +82 -1
  69. data/test/calendlyr/objects/event_type_test.rb +0 -15
  70. data/test/calendlyr/objects/event_types/availability_schedule_test.rb +20 -0
  71. data/test/calendlyr/objects/events/cancellation_test.rb +1 -1
  72. data/test/calendlyr/objects/events/guest_test.rb +1 -1
  73. data/test/calendlyr/objects/events/invitee_no_show_test.rb +1 -1
  74. data/test/calendlyr/objects/events/invitee_test.rb +10 -3
  75. data/test/calendlyr/objects/location_test.rb +22 -0
  76. data/test/calendlyr/objects/organization_test.rb +0 -8
  77. data/test/calendlyr/objects/organizations/invitation_test.rb +1 -1
  78. data/test/calendlyr/objects/share_test.rb +3 -9
  79. data/test/calendlyr/objects/webhooks/payload_test.rb +15 -0
  80. data/test/calendlyr/resource_test.rb +456 -2
  81. data/test/calendlyr/resources/availabilities/user_busy_times_test.rb +26 -0
  82. data/test/calendlyr/resources/availabilities/user_schedules_test.rb +25 -0
  83. data/test/calendlyr/resources/data_compliance_test.rb +1 -4
  84. data/test/calendlyr/resources/event_types_test.rb +132 -0
  85. data/test/calendlyr/resources/events_test.rb +87 -0
  86. data/test/calendlyr/resources/groups_test.rb +54 -0
  87. data/test/calendlyr/resources/locations_test.rb +30 -0
  88. data/test/calendlyr/resources/organizations_test.rb +96 -2
  89. data/test/calendlyr/resources/outgoing_communications_test.rb +34 -8
  90. data/test/calendlyr/resources/routing_forms_test.rb +57 -0
  91. data/test/calendlyr/resources/scheduling_links_test.rb +31 -6
  92. data/test/calendlyr/resources/shares_test.rb +15 -0
  93. data/test/calendlyr/resources/webhooks_test.rb +63 -5
  94. data/test/calendlyr/webhook_test.rb +292 -0
  95. data/test/fixtures/activity_log/list_page2.json +30 -0
  96. data/test/fixtures/event_invitees/list_page2.json +35 -0
  97. data/test/fixtures/event_invitees/retrieve.json +11 -1
  98. data/test/fixtures/event_type_availability_schedules/list.json +17 -0
  99. data/test/fixtures/event_type_availability_schedules/update.json +3 -0
  100. data/test/fixtures/event_type_available_times/list.json +0 -12
  101. data/test/fixtures/event_type_memberships/list.json +43 -0
  102. data/test/fixtures/event_type_memberships/list_page2.json +33 -0
  103. data/test/fixtures/event_types/create.json +30 -0
  104. data/test/fixtures/event_types/list_page2.json +37 -0
  105. data/test/fixtures/event_types/update.json +30 -0
  106. data/test/fixtures/events/create_invitee.json +37 -0
  107. data/test/fixtures/events/list_page2.json +29 -0
  108. data/test/fixtures/events/retrieve.json +12 -2
  109. data/test/fixtures/group_relationships/list_page2.json +35 -0
  110. data/test/fixtures/groups/list_page2.json +16 -0
  111. data/test/fixtures/locations/list.json +16 -0
  112. data/test/fixtures/locations/list_page2.json +16 -0
  113. data/test/fixtures/objects/event.json +10 -2
  114. data/test/fixtures/objects/event_types/availability_schedule.json +6 -0
  115. data/test/fixtures/objects/location.json +5 -0
  116. data/test/fixtures/organizations/list_invitations_page2.json +18 -0
  117. data/test/fixtures/organizations/list_memberships_page2.json +26 -0
  118. data/test/fixtures/organizations/retrieve.json +11 -0
  119. data/test/fixtures/outgoing_communications/list.json +4 -6
  120. data/test/fixtures/outgoing_communications/list_page2.json +21 -0
  121. data/test/fixtures/routing_forms/list_page2.json +17 -0
  122. data/test/fixtures/routing_forms/list_routing_form_submission_page2.json +29 -0
  123. data/test/fixtures/user_availability_schedules/list_page1.json +16 -0
  124. data/test/fixtures/user_availability_schedules/list_page2.json +16 -0
  125. data/test/fixtures/user_busy_times/list_page1.json +18 -0
  126. data/test/fixtures/user_busy_times/list_page2.json +13 -0
  127. data/test/fixtures/webhooks/list_page2.json +23 -0
  128. data/test/fixtures/webhooks/sample.json +55 -94
  129. data/test/test_helper.rb +19 -7
  130. metadata +70 -27
  131. data/docs/resources/scheduling_link.md +0 -26
  132. data/test/calendlyr/objects/event_types/available_time_test.rb +0 -20
  133. data/test/calendlyr/objects/event_types/membership_test.rb +0 -32
  134. data/test/calendlyr/objects/scheduling_link_test.rb +0 -17
  135. data/test/calendlyr/resources/event_types/membership_test.rb +0 -22
  136. data/test/fixtures/objects/event_types/available_time.json +0 -6
  137. data/test/fixtures/objects/event_types/membership.json +0 -65
  138. data/test/fixtures/objects/scheduling_links/event_type.json +0 -5
@@ -1,54 +1,92 @@
1
1
  module Calendlyr
2
- class Error < StandardError; end
3
-
4
- class PermissionDenied < StandardError; end
5
-
6
- class BadRequest < StandardError; end
7
-
8
- class PaymentRequired < StandardError; end
2
+ class Error < StandardError
3
+ attr_reader :status, :http_method, :path, :response_body
4
+
5
+ def initialize(message = nil, status: nil, http_method: nil, path: nil, response_body: nil)
6
+ @status = status
7
+ @http_method = http_method
8
+ @path = path
9
+ @response_body = response_body
10
+ super(message)
11
+ end
12
+ end
9
13
 
10
- class Unauthenticated < StandardError; end
14
+ class WebhookSignatureError < Error; end
11
15
 
12
- class NotFound < StandardError; end
16
+ class WebhookTimestampError < Error; end
13
17
 
14
- class ExternalCalendarError < StandardError; end
18
+ class PaymentRequired < Error; end
15
19
 
16
- class InternalServerError < StandardError; end
20
+ ERROR_TYPES = {
21
+ "400" => "BadRequest",
22
+ "401" => "Unauthenticated",
23
+ "403" => "PermissionDenied",
24
+ "404" => "NotFound",
25
+ "424" => "ExternalCalendarError",
26
+ "429" => "TooManyRequests",
27
+ "500" => "InternalServerError"
28
+ }
17
29
 
18
- class TooManyRequests < StandardError; end
30
+ ERROR_TYPES.values.each do |error_class|
31
+ Calendlyr.const_set(error_class, Class.new(Error))
32
+ end
19
33
 
20
34
  class ResponseErrorHandler
21
- ERROR_TYPES = {
22
- "400" => BadRequest,
23
- "401" => Unauthenticated,
24
- "403" => PermissionDenied,
25
- "404" => NotFound,
26
- "424" => ExternalCalendarError,
27
- "429" => TooManyRequests,
28
- "500" => InternalServerError
29
- }
30
-
31
- def initialize(code, body)
35
+ def initialize(code, body, method: nil, path: nil)
32
36
  @code = code
33
37
  @body = body
38
+ @method = method
39
+ @path = path
34
40
  end
35
41
 
36
42
  def error
37
43
  return too_many_requests_error if @code == "429"
38
44
 
39
- error_type.new("[Error #{@code}] #{@body["title"]}. #{@body["message"]}")
45
+ error_type.new(
46
+ message,
47
+ status: @code,
48
+ http_method: @method,
49
+ path: @path,
50
+ response_body: @body
51
+ )
40
52
  end
41
53
 
42
54
  private
43
55
 
44
56
  def error_type
45
- return PaymentRequired if @code == "403" && @body["message"].include?("upgrade")
57
+ return PaymentRequired if @code == "403" && @body.fetch("message", "").include?("upgrade")
46
58
 
47
- ERROR_TYPES[@code]
59
+ klass = "Calendlyr::#{Calendlyr::ERROR_TYPES[@code]}"
60
+ Calendlyr.const_get(klass)
48
61
  end
49
62
 
50
63
  def too_many_requests_error
51
- error_type.new("[Error #{@code}] Too many requests, please try again later.")
64
+ error_type.new(
65
+ contextual_message("Too many requests, please try again later."),
66
+ status: @code,
67
+ http_method: @method,
68
+ path: @path,
69
+ response_body: @body
70
+ )
71
+ end
72
+
73
+ def message
74
+ contextual_message(body_message)
75
+ end
76
+
77
+ def body_message
78
+ [@body["title"], @body["message"]].compact.reject(&:empty?).join(". ")
79
+ end
80
+
81
+ def contextual_message(body_message)
82
+ base = "[Error #{@code}]"
83
+ return [base, body_message].reject(&:empty?).join(" ") unless @method && @path
84
+
85
+ path = @path.start_with?("/") ? @path : "/#{@path}"
86
+ context = "#{@method} #{path}"
87
+ return "#{base} #{context}" if body_message.empty?
88
+
89
+ "#{base} #{context} — #{body_message}"
52
90
  end
53
91
  end
54
92
  end
@@ -1,31 +1,103 @@
1
- require "ostruct"
1
+ require "json"
2
2
 
3
3
  module Calendlyr
4
- class Object < OpenStruct
4
+ class Object
5
+ FILTERED_KEYS = %w[client].freeze
6
+ private_constant :FILTERED_KEYS
7
+
5
8
  def self.get_slug(path)
6
9
  path.split("/").last
7
10
  end
8
11
 
9
- def initialize(attributes)
10
- super(to_ostruct(attributes.merge(uuid: extract_uuid(attributes))))
12
+ private_class_method :get_slug
13
+
14
+ def initialize(attributes = nil, add_uuid: true, **kwargs)
15
+ attrs = (attributes || {}).merge(kwargs).dup
16
+ if add_uuid && !attrs.key?(:uuid) && !attrs.key?("uuid")
17
+ attrs = attrs.merge(uuid: extract_uuid(attrs))
18
+ end
19
+
20
+ @attributes = attrs.each_with_object({}) do |(key, value), hash|
21
+ hash[key.to_s] = wrap(value)
22
+ end
23
+ end
24
+
25
+ def method_missing(name, *args, &block)
26
+ if args.empty? && block.nil?
27
+ return @attributes[name.to_s] if @attributes.key?(name.to_s)
28
+
29
+ return nil
30
+ end
31
+
32
+ super
33
+ end
34
+
35
+ def respond_to_missing?(name, include_private = false)
36
+ @attributes.key?(name.to_s) || super
11
37
  end
12
38
 
13
- def to_ostruct(obj)
14
- if obj.is_a?(Hash)
15
- OpenStruct.new(obj.map { |key, val| [key, to_ostruct(val)] }.to_h)
16
- elsif obj.is_a?(Array)
17
- obj.map { |o| to_ostruct(o) }
18
- else # Assumed to be a primitive value
19
- obj
39
+ def to_h
40
+ @attributes.except(*FILTERED_KEYS).each_with_object({}) do |(key, value), hash|
41
+ hash[key.to_sym] = unwrap(value)
20
42
  end
21
43
  end
22
44
 
45
+ def to_json(*)
46
+ to_h.to_json(*)
47
+ end
48
+
49
+ def inspect
50
+ attributes = @attributes.map do |key, value|
51
+ "#{key}=#{value.inspect}"
52
+ end.join(" ")
53
+ "#<#{self.class} #{attributes}>"
54
+ end
55
+
56
+ def ==(other)
57
+ other.is_a?(self.class) && to_h == other.to_h
58
+ end
59
+
60
+ def eql?(other)
61
+ self == other
62
+ end
63
+
64
+ def hash
65
+ to_h.hash
66
+ end
67
+
68
+ private
69
+
23
70
  def extract_uuid(attrs)
24
- attrs["uri"] ? get_slug(attrs["uri"]) : nil
71
+ uri = attrs["uri"] || attrs[:uri]
72
+ uri ? get_slug(uri) : nil
25
73
  end
26
74
 
75
+ protected
76
+
27
77
  def get_slug(path)
28
- Calendlyr::Object.get_slug(path)
78
+ self.class.send(:get_slug, path)
79
+ end
80
+
81
+ private
82
+
83
+ def wrap(value)
84
+ if value.is_a?(Hash)
85
+ self.class.new(value, add_uuid: false)
86
+ elsif value.is_a?(Array)
87
+ value.map { |item| wrap(item) }
88
+ else
89
+ value
90
+ end
91
+ end
92
+
93
+ def unwrap(value)
94
+ if value.is_a?(self.class)
95
+ value.to_h
96
+ elsif value.is_a?(Array)
97
+ value.map { |item| unwrap(item) }
98
+ else
99
+ value
100
+ end
29
101
  end
30
102
  end
31
103
  end
@@ -7,9 +7,5 @@ module Calendlyr
7
7
  def create_share(**params)
8
8
  client.shares.create(**params.merge(event_type: uri))
9
9
  end
10
-
11
- def available_times(start_time:, end_time:, **params)
12
- client.event_types.list_available_times(**params.merge(event_type: uri, start_time: start_time, end_time: end_time))
13
- end
14
10
  end
15
11
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calendlyr
4
+ module EventTypes
5
+ class AvailabilitySchedule < Object
6
+ end
7
+ end
8
+ end
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Calendlyr
2
- class EventTypes::AvailableTime < Object
4
+ module EventTypes
5
+ class AvailableTime < Object
6
+ end
3
7
  end
4
8
  end
@@ -1,11 +1,8 @@
1
- module Calendlyr
2
- class EventTypes::Membership < Object
3
- def associated_event_type
4
- client.event_types.retrieve(uuid: get_slug(event_type.uri))
5
- end
1
+ # frozen_string_literal: true
6
2
 
7
- def associated_member
8
- client.users.retrieve(uuid: get_slug(member.uri))
3
+ module Calendlyr
4
+ module EventTypes
5
+ class Membership < Object
9
6
  end
10
7
  end
11
8
  end
@@ -1,7 +1,7 @@
1
1
  module Calendlyr
2
2
  class Events::Invitee < Object
3
3
  def cancel(reason: nil)
4
- client.events.cancel(uuid: uuid, reason: reason)
4
+ client.events.cancel(uuid: get_slug(event), reason: reason)
5
5
  end
6
6
 
7
7
  def create_no_shows
@@ -0,0 +1,4 @@
1
+ module Calendlyr
2
+ class Location < Object
3
+ end
4
+ end
@@ -43,10 +43,6 @@ module Calendlyr
43
43
  client.webhooks.create(**params.merge(organization: uri, url: url, events: events, scope: scope))
44
44
  end
45
45
 
46
- def sample_webhook_data(event:, scope:, **params)
47
- client.webhooks.sample_webhook_data(event: event, scope: scope, organization: uri, **params)
48
- end
49
-
50
46
  # Invitations
51
47
  def invite_user(email:, **params)
52
48
  client.organizations.invite(**params.merge(organization_uuid: uuid, email: email))
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calendlyr
4
+ class OutgoingCommunication < Object
5
+ end
6
+ end
@@ -1,9 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Calendlyr
2
4
  class SchedulingLink < Object
3
- def event_type
4
- return unless owner_type == "EventType"
5
-
6
- client.event_types.retrieve(uuid: get_slug(owner))
7
- end
8
5
  end
9
6
  end
@@ -1,9 +1,4 @@
1
1
  module Calendlyr
2
2
  class Share < Object
3
- def associated_scheduling_links
4
- scheduling_links.map do |scheduling_link|
5
- SchedulingLink.new(scheduling_link.to_h.merge(client: client))
6
- end
7
- end
8
3
  end
9
4
  end
@@ -1,4 +1,34 @@
1
1
  module Calendlyr
2
2
  class Webhooks::Payload < Object
3
+ INVITEE_EVENTS = %w[
4
+ invitee.created
5
+ invitee.canceled
6
+ invitee_no_show.created
7
+ invitee_no_show.deleted
8
+ ].freeze
9
+
10
+ def initialize(attributes = nil, add_uuid: true, **kwargs)
11
+ attrs = (attributes || {}).merge(kwargs)
12
+ attrs = attrs.merge("payload" => payload_object(attrs))
13
+
14
+ super(attrs, add_uuid: add_uuid)
15
+ end
16
+
17
+ private
18
+
19
+ def payload_object(attrs)
20
+ payload = attrs["payload"] || attrs[:payload]
21
+ return payload unless payload.is_a?(Hash)
22
+
23
+ return Webhooks::InviteePayload.new(payload, add_uuid: false) if INVITEE_EVENTS.include?(attrs["event"] || attrs[:event])
24
+
25
+ payload
26
+ end
27
+
28
+ def wrap(value)
29
+ return value unless value.is_a?(Hash)
30
+
31
+ Calendlyr::Object.new(value, add_uuid: false)
32
+ end
3
33
  end
4
34
  end
@@ -8,6 +8,8 @@ module Calendlyr
8
8
  attr_reader :client
9
9
 
10
10
  ERROR_CODES = %w[400 401 403 404 424 429 500]
11
+ MAX_RETRIES = 3
12
+ RETRY_BACKOFF = [1, 2, 4]
11
13
 
12
14
  def initialize(client)
13
15
  @client = client
@@ -16,15 +18,23 @@ module Calendlyr
16
18
  private
17
19
 
18
20
  def get_request(url, params: {})
19
- handle_response request(url, Net::HTTP::Get, params: params)
21
+ handle_response request(url, Net::HTTP::Get, params: params), method: "GET", path: url
20
22
  end
21
23
 
22
24
  def post_request(url, body:)
23
- handle_response request(url, Net::HTTP::Post, body: body)
25
+ handle_response request(url, Net::HTTP::Post, body: body), method: "POST", path: url
26
+ end
27
+
28
+ def patch_request(url, body:)
29
+ handle_response request(url, Net::HTTP::Patch, body: body), method: "PATCH", path: url
30
+ end
31
+
32
+ def put_request(url, body:)
33
+ handle_response request(url, Net::HTTP::Put, body: body), method: "PUT", path: url
24
34
  end
25
35
 
26
36
  def delete_request(url, params: {})
27
- handle_response request(url, Net::HTTP::Delete, params: params)
37
+ handle_response request(url, Net::HTTP::Delete, params: params), method: "DELETE", path: url
28
38
  end
29
39
 
30
40
  def request(url, req_type, body: {}, params: {}, base_url: Client::BASE_URL)
@@ -37,30 +47,92 @@ module Calendlyr
37
47
 
38
48
  http = Net::HTTP.new(uri.host, uri.port)
39
49
  http.use_ssl = true
40
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
50
+ http.open_timeout = client.open_timeout
51
+ http.read_timeout = client.read_timeout
41
52
 
42
53
  request = req_type.new(uri)
43
54
  request["Content-Type"] = "application/json"
44
55
  request["Authorization"] = "Bearer #{client.token}"
45
56
  request.body = body.to_json if body.any?
46
57
 
47
- http.request(request)
58
+ logging_enabled = !logger.nil?
59
+ start_time = monotonic_now if logging_enabled
60
+ request_method = request.method
61
+ request_url = uri.to_s if logging_enabled
62
+ attempts = 0
63
+ response = nil
64
+
65
+ loop do
66
+ response = http.request(request)
67
+ break unless response.code == "429"
68
+ break if attempts >= MAX_RETRIES
69
+
70
+ backoff_seconds = retry_after_seconds(response, attempts)
71
+ log(:warn, "retry_attempt=#{attempts + 1} method=#{request_method} url=#{request_url} status=429 backoff_seconds=#{backoff_seconds}")
72
+ sleep backoff_seconds
73
+ attempts += 1
74
+ end
75
+
76
+ if logging_enabled
77
+ log(:info, "method=#{request_method} url=#{request_url} status=#{response.code} duration_ms=#{elapsed_milliseconds(start_time)}")
78
+ log(:debug, "response_body=#{truncated_body(response.body.to_s)}")
79
+ end
80
+
81
+ response
48
82
  end
49
83
 
50
- def handle_response(response)
51
- return true unless response.read_body
84
+ def handle_response(response, method: nil, path: nil)
85
+ body_string = response.body.to_s
52
86
 
53
87
  body = begin
54
- JSON.parse(response.read_body)
55
- rescue
88
+ body_string.empty? ? {} : JSON.parse(body_string)
89
+ rescue JSON::ParserError
56
90
  {}
57
91
  end
58
92
 
59
93
  if ERROR_CODES.include? response.code
60
- raise ResponseErrorHandler.new(response.code, body).error
61
- else
62
- body
94
+ log(:error, "method=#{method} path=/#{path} status=#{response.code} response_body=#{truncated_body(body_string)}")
95
+ raise ResponseErrorHandler.new(response.code, body, method: method, path: path).error
63
96
  end
97
+
98
+ body
99
+ end
100
+
101
+ def retry_after_seconds(response, attempt)
102
+ retry_after = response["Retry-After"]
103
+ return retry_after.to_i if retry_after&.match?(/^\d+$/)
104
+
105
+ RETRY_BACKOFF.fetch(attempt)
106
+ end
107
+
108
+ def expand_uri(value, resource_type)
109
+ return value if value.nil? || value.start_with?("https://")
110
+
111
+ "#{Client::BASE_URL}/#{resource_type}/#{value}"
112
+ end
113
+
114
+ def logger
115
+ client.logger
116
+ end
117
+
118
+ def log(level, message)
119
+ return unless logger
120
+
121
+ logger.public_send(level, "[calendlyr] #{message}")
122
+ end
123
+
124
+ def truncated_body(body_string)
125
+ return body_string if body_string.length <= 1000
126
+
127
+ "#{body_string[0, 1000]}... (truncated)"
128
+ end
129
+
130
+ def monotonic_now
131
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
132
+ end
133
+
134
+ def elapsed_milliseconds(start_time)
135
+ ((monotonic_now - start_time) * 1000).round(1)
64
136
  end
65
137
  end
66
138
  end
@@ -2,14 +2,26 @@ module Calendlyr
2
2
  class AvailabilityResource < Resource
3
3
  # User Busy Time
4
4
  def list_user_busy_times(user:, start_time:, end_time:, **params)
5
+ next_page_caller = ->(page_token:) { list_user_busy_times(user: user, start_time: start_time, end_time: end_time, **params, page_token: page_token) }
6
+ user = expand_uri(user, "users")
5
7
  response = get_request("user_busy_times", params: {user: user, start_time: start_time, end_time: end_time}.merge(params).compact)
6
- Collection.from_response(response, type: Availabilities::UserBusyTime, client: client)
8
+ Collection.from_response(response, type: Availabilities::UserBusyTime, client: client, next_page_caller: next_page_caller)
9
+ end
10
+
11
+ def list_all_user_busy_times(user:, start_time:, end_time:, **params)
12
+ list_user_busy_times(user: user, start_time: start_time, end_time: end_time, **params).auto_paginate.to_a
7
13
  end
8
14
 
9
15
  # User Schedule
10
16
  def list_user_schedules(user:, **params)
17
+ next_page_caller = ->(page_token:) { list_user_schedules(user: user, **params, page_token: page_token) }
18
+ user = expand_uri(user, "users")
11
19
  response = get_request("user_availability_schedules", params: {user: user}.merge(params).compact)
12
- Collection.from_response(response, type: Availabilities::UserSchedule, client: client)
20
+ Collection.from_response(response, type: Availabilities::UserSchedule, client: client, next_page_caller: next_page_caller)
21
+ end
22
+
23
+ def list_all_user_schedules(user:, **params)
24
+ list_user_schedules(user: user, **params).auto_paginate.to_a
13
25
  end
14
26
 
15
27
  def retrieve_user_schedule(uuid:)
@@ -1,8 +1,15 @@
1
1
  module Calendlyr
2
2
  class EventTypesResource < Resource
3
3
  def list(**params)
4
+ next_page_caller = ->(page_token:) { list(**params, page_token: page_token) }
5
+ params[:user] = expand_uri(params[:user], "users") if params[:user]
6
+ params[:organization] = expand_uri(params[:organization], "organizations") if params[:organization]
4
7
  response = get_request("event_types", params: params)
5
- Collection.from_response(response, type: EventType, client: client)
8
+ Collection.from_response(response, type: EventType, client: client, next_page_caller: next_page_caller)
9
+ end
10
+
11
+ def list_all(**params)
12
+ list(**params).auto_paginate.to_a
6
13
  end
7
14
 
8
15
  def retrieve(uuid:)
@@ -14,15 +21,48 @@ module Calendlyr
14
21
  EventType.new post_request("one_off_event_types", body: body).dig("resource").merge(client: client)
15
22
  end
16
23
 
17
- def list_memberships(event_type:, **params)
18
- response = get_request("event_type_memberships", params: {event_type: event_type}.merge(params))
19
- Collection.from_response(response, type: EventTypes::Membership, client: client)
24
+ def create(name:, duration:, pooling_type:, **params)
25
+ body = {name: name, duration: duration, pooling_type: pooling_type}.merge(params)
26
+ EventType.new post_request("event_types", body: body).dig("resource").merge(client: client)
27
+ end
28
+
29
+ def update(uuid:, **params)
30
+ EventType.new patch_request("event_types/#{uuid}", body: params).dig("resource").merge(client: client)
31
+ end
32
+
33
+ # Availability Schedules
34
+ def list_availability_schedules(event_type_uuid:, **params)
35
+ next_page_caller = ->(page_token:) { list_availability_schedules(event_type_uuid: event_type_uuid, **params, page_token: page_token) }
36
+ response = get_request("event_type_availability_schedules", params: {event_type_uuid: event_type_uuid}.merge(params))
37
+ Collection.from_response(response, type: EventTypes::AvailabilitySchedule, client: client, next_page_caller: next_page_caller)
38
+ end
39
+
40
+ def list_all_availability_schedules(event_type_uuid:, **params)
41
+ list_availability_schedules(event_type_uuid: event_type_uuid, **params).auto_paginate.to_a
42
+ end
43
+
44
+ def update_availability_schedule(event_type_uuid:, availability_schedules:, **params)
45
+ body = {event_type_uuid: event_type_uuid, availability_schedules: availability_schedules}.merge(params)
46
+ patch_request("event_type_availability_schedules", body: body)
20
47
  end
21
48
 
22
- # Available Times
49
+ # Available Times (no pagination — endpoint does not support it)
23
50
  def list_available_times(event_type:, start_time:, end_time:, **params)
51
+ event_type = expand_uri(event_type, "event_types")
24
52
  response = get_request("event_type_available_times", params: {event_type: event_type, start_time: start_time, end_time: end_time}.merge(params))
25
53
  Collection.from_response(response, type: EventTypes::AvailableTime, client: client)
26
54
  end
55
+
56
+ # Event Type Memberships
57
+ def list_memberships(event_type:, **params)
58
+ next_page_caller = ->(page_token:) { list_memberships(event_type: event_type, **params, page_token: page_token) }
59
+ event_type = expand_uri(event_type, "event_types")
60
+ response = get_request("event_type_memberships", params: {event_type: event_type}.merge(params))
61
+ Collection.from_response(response, type: EventTypes::Membership, client: client, next_page_caller: next_page_caller)
62
+ end
63
+
64
+ def list_all_memberships(event_type:, **params)
65
+ list_memberships(event_type: event_type, **params).auto_paginate.to_a
66
+ end
27
67
  end
28
68
  end
@@ -1,8 +1,14 @@
1
1
  module Calendlyr
2
2
  class EventsResource < Resource
3
3
  def list(**params)
4
+ next_page_caller = ->(page_token:) { list(**params, page_token: page_token) }
5
+ params[:user] = expand_uri(params[:user], "users") if params[:user]
4
6
  response = get_request("scheduled_events", params: params)
5
- Collection.from_response(response, type: Event, client: client)
7
+ Collection.from_response(response, type: Event, client: client, next_page_caller: next_page_caller)
8
+ end
9
+
10
+ def list_all(**params)
11
+ list(**params).auto_paginate.to_a
6
12
  end
7
13
 
8
14
  def retrieve(uuid:)
@@ -15,20 +21,32 @@ module Calendlyr
15
21
 
16
22
  # Invitee
17
23
  def list_invitees(uuid:, **params)
24
+ next_page_caller = ->(page_token:) { list_invitees(uuid: uuid, **params, page_token: page_token) }
18
25
  response = get_request("scheduled_events/#{uuid}/invitees", params: params)
19
- Collection.from_response(response, type: Events::Invitee, client: client)
26
+ Collection.from_response(response, type: Events::Invitee, client: client, next_page_caller: next_page_caller)
27
+ end
28
+
29
+ def list_all_invitees(uuid:, **params)
30
+ list_invitees(uuid: uuid, **params).auto_paginate.to_a
20
31
  end
21
32
 
22
33
  def retrieve_invitee(event_uuid:, invitee_uuid:)
23
34
  Events::Invitee.new get_request("scheduled_events/#{event_uuid}/invitees/#{invitee_uuid}").dig("resource").merge(client: client)
24
35
  end
25
36
 
37
+ def create_invitee(event_type:, start_time:, invitee:, **params)
38
+ event_type = expand_uri(event_type, "event_types")
39
+ body = {event_type: event_type, start_time: start_time, invitee: invitee}.merge(params)
40
+ Events::Invitee.new post_request("invitees", body: body).dig("invitee").merge(client: client)
41
+ end
42
+
26
43
  # Invitee No Show
27
44
  def retrieve_invitee_no_show(uuid:)
28
45
  Events::InviteeNoShow.new get_request("invitee_no_shows/#{uuid}").dig("resource").merge(client: client)
29
46
  end
30
47
 
31
48
  def create_invitee_no_show(invitee:)
49
+ invitee = expand_uri(invitee, "invitees")
32
50
  body = {invitee: invitee}
33
51
  Events::InviteeNoShow.new post_request("invitee_no_shows", body: body).dig("resource").merge(client: client)
34
52
  end