bearcat 1.4.12 → 1.5.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/bearcat.gemspec +9 -3
  3. data/lib/bearcat/api_array.rb +2 -0
  4. data/lib/bearcat/client/account_reports.rb +6 -14
  5. data/lib/bearcat/client/accounts.rb +20 -56
  6. data/lib/bearcat/client/analytics.rb +4 -7
  7. data/lib/bearcat/client/assignment_groups.rb +7 -19
  8. data/lib/bearcat/client/assignments.rb +15 -39
  9. data/lib/bearcat/client/blueprint_courses.rb +14 -20
  10. data/lib/bearcat/client/calendar_events.rb +9 -17
  11. data/lib/bearcat/client/canvas_files.rb +0 -2
  12. data/lib/bearcat/client/conferences.rb +3 -8
  13. data/lib/bearcat/client/conversations.rb +3 -8
  14. data/lib/bearcat/client/courses.rb +15 -45
  15. data/lib/bearcat/client/custom_gradebook_columns.rb +11 -25
  16. data/lib/bearcat/client/discussions.rb +11 -37
  17. data/lib/bearcat/client/enrollments.rb +9 -29
  18. data/lib/bearcat/client/external_tools.rb +9 -36
  19. data/lib/bearcat/client/file_helper.rb +3 -3
  20. data/lib/bearcat/client/files.rb +2 -4
  21. data/lib/bearcat/client/folders.rb +13 -59
  22. data/lib/bearcat/client/group_categories.rb +8 -17
  23. data/lib/bearcat/client/group_memberships.rb +6 -14
  24. data/lib/bearcat/client/groups.rb +9 -24
  25. data/lib/bearcat/client/module_items.rb +9 -17
  26. data/lib/bearcat/client/modules.rb +10 -20
  27. data/lib/bearcat/client/outcome_groups.rb +2 -4
  28. data/lib/bearcat/client/outcomes.rb +4 -7
  29. data/lib/bearcat/client/pages.rb +8 -23
  30. data/lib/bearcat/client/progresses.rb +2 -3
  31. data/lib/bearcat/client/quizzes.rb +15 -35
  32. data/lib/bearcat/client/reports.rb +8 -19
  33. data/lib/bearcat/client/roles.rb +1 -1
  34. data/lib/bearcat/client/rubric.rb +8 -20
  35. data/lib/bearcat/client/rubric_assessment.rb +5 -11
  36. data/lib/bearcat/client/rubric_association.rb +6 -12
  37. data/lib/bearcat/client/search.rb +2 -4
  38. data/lib/bearcat/client/sections.rb +11 -26
  39. data/lib/bearcat/client/submissions.rb +48 -44
  40. data/lib/bearcat/client/tabs.rb +4 -7
  41. data/lib/bearcat/client/users.rb +17 -44
  42. data/lib/bearcat/client.rb +72 -44
  43. data/lib/bearcat/client_module.rb +101 -0
  44. data/lib/bearcat/rate_limiting/redis_script.rb +164 -0
  45. data/lib/bearcat/rate_limiting.rb +70 -0
  46. data/lib/bearcat/spec_helpers.rb +62 -31
  47. data/lib/bearcat/version.rb +1 -1
  48. data/lib/bearcat.rb +8 -21
  49. data/lib/catalogcat/api_array.rb +18 -0
  50. data/lib/catalogcat/client/catalogs.rb +21 -0
  51. data/lib/catalogcat/client/certificates.rb +9 -0
  52. data/lib/catalogcat/client/courses.rb +25 -0
  53. data/lib/catalogcat/client/orders.rb +9 -0
  54. data/lib/catalogcat/client.rb +39 -0
  55. data/lib/catalogcat/version.rb +3 -0
  56. data/lib/catalogcat.rb +14 -0
  57. data/spec/bearcat/client/canvas_files_spec.rb +1 -2
  58. data/spec/bearcat/client/content_migrations_spec.rb +2 -2
  59. data/spec/bearcat/client/courses_spec.rb +2 -4
  60. data/spec/bearcat/{group_memberships_spec.rb → client/group_memberships_spec.rb} +0 -0
  61. data/spec/bearcat/client/learning_outcomes_spec.rb +2 -2
  62. data/spec/bearcat/client/submissions_spec.rb +14 -0
  63. data/spec/bearcat/client_spec.rb +1 -4
  64. data/spec/bearcat/rate_limiting_spec.rb +62 -0
  65. data/spec/bearcat/stub_bearcat_spec.rb +15 -0
  66. data/spec/helper.rb +1 -0
  67. metadata +196 -181
@@ -1,19 +1,13 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module RubricAssessment
4
+ extend ClientModule
4
5
 
5
- def create_course_rubric_assessment(course, rubric_association, params={})
6
- post("api/v1/courses/#{course}/rubric_associations/#{rubric_association}/rubric_assessments", params)
6
+ prefix "/api/v1/courses/:course/rubric_associations/:rubric_association/rubric_assessments/" do
7
+ post :create_course_rubric_assessment
8
+ put :update_course_rubric_assessment, ":rubric_assessment"
9
+ delete :delete_course_rubric_assessment, ":rubric_assessment"
7
10
  end
8
-
9
- def update_course_rubric_assessment(course, rubric_association, rubric_assessment, params={})
10
- put("api/v1/courses/#{course}/rubric_associations/#{rubric_association}/rubric_assessments/#{rubric_assessment}", params)
11
- end
12
-
13
- def delete_course_rubric_assessment(course, rubric_association, rubric_assessment)
14
- delete("api/v1/courses/#{course}/rubric_associations/#{rubric_association}/rubric_assessments/#{rubric_assessment}")
15
- end
16
-
17
11
  end
18
12
  end
19
13
  end
@@ -1,19 +1,13 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module RubricAssociation
4
+ extend ClientModule
4
5
 
5
- def create_course_rubric_association(course, params={})
6
- post("api/v1/courses/#{course}/rubric_associations/", params)
6
+ prefix "/api/v1/courses/:course/rubric_associations/" do
7
+ post :create_course_rubric_association
8
+ put :update_course_rubric_association, ":rubric_association"
9
+ delete :delete_course_rubric_association, ":rubric_association"
7
10
  end
8
-
9
- def update_course_rubric_association(course, rubric_association, params={})
10
- put("api/v1/courses/#{course}/rubric_associations/#{rubric_association}", params)
11
- end
12
-
13
- def delete_course_rubric_association(course, rubric_association)
14
- delete("api/v1/courses/#{course}/rubric_associations/#{rubric_association}")
15
- end
16
-
17
11
  end
18
12
  end
19
- end
13
+ end
@@ -1,11 +1,9 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Search
4
+ extend ClientModule
4
5
 
5
- def find_recipients(params={})
6
- get('/api/v1/conversations/find_recipients', params)
7
- end
8
-
6
+ get :find_recipients, '/api/v1/conversations/find_recipients'
9
7
  end
10
8
  end
11
9
  end
@@ -1,35 +1,20 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Sections
4
-
5
- def course_sections(course, params={})
6
- get("/api/v1/courses/#{course.to_s}/sections", params)
7
- end
8
-
9
- def section(section)
10
- get("/api/v1/sections/#{section.to_s}")
11
- end
12
-
13
- def create_section(course, params)
14
- post("/api/v1/courses/#{course.to_s}/sections", params)
15
- end
16
-
17
- def update_section(section, params)
18
- put("/api/v1/sections/#{section.to_s}", params)
19
- end
20
-
21
- def delete_section(section)
22
- delete("/api/v1/sections/#{section.to_s}")
4
+ extend ClientModule
5
+
6
+ prefix "/api/v1/sections/:section/" do
7
+ get :section
8
+ put :update_section
9
+ delete :delete_section
10
+ post :crosslist_section, "crosslist/:new_course_id"
11
+ delete :decrosslist_section, "crosslist"
23
12
  end
24
13
 
25
- def crosslist_section(section_id, new_course_id)
26
- post("/api/v1/sections/#{section_id.to_s}/crosslist/#{new_course_id.to_s}")
14
+ prefix "/api/v1/courses/:course/sections/" do
15
+ get :course_sections
16
+ post :create_section
27
17
  end
28
-
29
- def decrosslist_section(section_id)
30
- delete("/api/v1/sections/#{section_id.to_s}/crosslist")
31
- end
32
-
33
18
  end
34
19
  end
35
20
  end
@@ -1,65 +1,69 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Submissions
4
+ extend ClientModule
4
5
 
5
- def course_submissions(course, params={})
6
- get("/api/v1/courses/#{course.to_s}/students/submissions", params)
6
+ prefix "/api/v1/courses/:course/assignments/:assignment/submissions/" do
7
+ get :get_course_submissions
8
+ get :user_course_assignment_submission, ":user"
7
9
  end
8
10
 
9
- def section_submissions(section, params={})
10
- get("/api/v1/sections/#{section.to_s}/students/submissions", params)
11
- end
12
-
13
- def get_course_submissions(course, assignment, params={})
14
- get("/api/v1/courses/#{course.to_s}/assignments/#{assignment}/submissions", params)
15
- end
16
-
17
- def user_course_assignment_submission(course, assignment, user, params={})
18
- get("/api/v1/courses/#{course.to_s}/assignments/#{assignment.to_s}/submissions/#{user.to_s}", params)
19
- end
11
+ context_types %i[course section] do |ct|
12
+ prefix "/api/v1/#{ct}s/:#{ct}/" do
13
+ get :"#{ct}_submissions", "students/submissions"
20
14
 
21
- def course_submission(course, assignment, params)
22
- post("/api/v1/courses/#{course}/assignments/#{assignment}/submissions", params)
15
+ prefix "assignments/:assignment/submissions/" do
16
+ post :"#{ct}_submission"
17
+ put :"grade_#{ct}_submission", ":user"
18
+ post :"#{ct}_update_grades", "update_grades"
19
+ end
20
+ end
23
21
  end
24
22
 
25
- def section_submission(section, assignment, params)
26
- post("/api/v1/sections/#{section}/assignments/#{assignment}/submissions", params)
23
+ def course_file_upload_submission(course, assignment, user, file_data, params={})
24
+ file_upload_submission(course, assignment, user, file_data, params, type: :course)
27
25
  end
28
26
 
29
- def grade_course_submission(course, assignment, user, params)
30
- put("/api/v1/courses/#{course}/assignments/#{assignment}/submissions/#{user}", params)
27
+ def section_file_upload_submission(section, assignment, user, file_data, params={})
28
+ file_upload_submission(section, assignment, user, file_data, params, type: :section)
31
29
  end
32
30
 
33
- def grade_section_submission(section, assignment, user, params)
34
- put("/api/v1/sections/#{section}/assignments/#{assignment}/submissions/#{user}", params)
35
- end
31
+ protected
36
32
 
37
- def course_file_upload_submission(course, assignment, user, file_path, params={})
38
- response = upload_file("/api/v1/courses/#{course}/assignments/#{assignment}/submissions/#{user}/files", file_path, params)
39
- params['submission'] = {
40
- 'submission_type' => 'online_upload',
41
- 'file_ids'=> [response['id']]
42
- }
43
- course_submission(course, assignment, params)
44
- end
33
+ # @param file_data One of an array of file_path strings or an array of Hashes, each being upload file params plus the file's path
34
+ def file_upload_submission(type_id, assignment, user, file_data=nil, params={}, type:)
35
+ raise ArgumentError, 'Invalid type' unless [:course, :section].include?(type)
36
+ raise ArgumentError, 'Must provide either file paths or fully formed submission params' if params.dig(:submission, :file_ids).nil? && file_data.nil?
45
37
 
46
- def section_file_upload_submission(section, assignment, user, file_path, params={})
47
- response = upload_file("/api/v1/sections/#{section}/assignments/#{assignment}/submissions/#{user}/files", file_path, params)
48
- params['submission'] = {
49
- 'submission_type' => 'online_upload',
50
- 'file_ids'=> [response['id']]
51
- }
52
- section_submission(section, assignment, params)
53
- end
38
+ params = params.with_indifferent_access
39
+ sub_params = if params.dig(:submission, :file_ids).present?
40
+ {}
41
+ else
42
+ file_data = Array.wrap(file_data)
43
+ file_ids = file_data.map do |datum|
44
+ if datum.is_a?(Hash) && datum[:file_path].present?
45
+ # datum is a param hash for upload_file API plus the file_path
46
+ path = datum[:file_path]
47
+ upload_params = datum.except(:file_path)
48
+ else
49
+ # treat datum as a file_path string
50
+ path = datum
51
+ upload_params = params
52
+ end
54
53
 
55
- def course_update_grades(course, assignment, params={})
56
- post("/api/v1/courses/#{course}/assignments/#{assignment}/submissions/update_grades", params)
57
- end
54
+ response = upload_file("/api/v1/#{type}s/#{type_id}/assignments/#{assignment}/submissions/#{user}/files", path, upload_params)
55
+ response['id']
56
+ end
58
57
 
59
- def section_update_grades(section, assignment, params={})
60
- post("/api/v1/sections/#{section}/assignments/#{assignment}/submissions/update_grades", params)
58
+ {
59
+ submission_type: 'online_upload',
60
+ file_ids: file_ids,
61
+ }
62
+ end
63
+ sub_params.merge!(params[:submission]) if params[:submission].present?
64
+ params[:submission] = sub_params
65
+ send("#{type}_submission".to_sym, type_id, assignment, params)
61
66
  end
62
-
63
67
  end
64
68
  end
65
69
  end
@@ -1,15 +1,12 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Tabs
4
+ extend ClientModule
4
5
 
5
- def get_tabs(course, params={})
6
- get("/api/v1/courses/#{course}/tabs", params)
6
+ prefix "/api/v1/courses/:course/tabs/" do
7
+ get :get_tabs
8
+ put :update_tab, ":tab"
7
9
  end
8
-
9
- def update_tab(id, course, params={})
10
- put("/api/v1/courses/#{course}/tabs/#{id}", params)
11
- end
12
-
13
10
  end
14
11
  end
15
12
  end
@@ -1,33 +1,26 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Users
4
+ extend ClientModule
4
5
 
5
- def list_users(account, params={})
6
- get("/api/v1/accounts/#{account.to_s}/users", params)
6
+ prefix "/api/v1/accounts/:account/users/" do
7
+ get :list_users
8
+ post :add_user
7
9
  end
8
10
 
9
- def user_avatars(user, params={})
10
- get("/api/v1/users/#{user.to_s}/avatars", params)
11
- end
12
-
13
- def add_user(account, params={})
14
- post("/api/v1/accounts/#{account.to_s}/users", params)
15
- end
16
-
17
- def user_detail(user, params={})
18
- get("/api/v1/users/#{user.to_s}", params)
19
- end
20
-
21
- def user_profile(user, params={})
22
- get("/api/v1/users/#{user.to_s}/profile", params)
23
- end
24
-
25
- def user_logins(user, params={})
26
- get("/api/v1/users/#{user.to_s}/logins", params)
27
- end
28
-
29
- def communication_channels(user, params={})
30
- get("/api/v1/users/#{user.to_s}/communication_channels", params)
11
+ prefix "/api/v1/users/" do
12
+ prefix ":user/" do
13
+ get :user_detail
14
+ get :user_avatars, "avatars"
15
+ get :user_profile, "profile"
16
+ get :user_logins, "logins"
17
+ get :communication_channels, "communication_channels"
18
+ get :page_views, "page_views"
19
+ put :user_merge, "merge_into/:merge_into_user"
20
+ get :user_assignments, "courses/:course/assignments"
21
+ get :dashboard_positions, "dashboard_positions"
22
+ put :update_dashboard_positions, "dashboard_positions"
23
+ end
31
24
  end
32
25
 
33
26
  # scope: food
@@ -52,26 +45,6 @@ module Bearcat
52
45
  delete("/api/v1/users/#{user}/custom_data/#{scope}", params)
53
46
  end
54
47
 
55
- def page_views(user, params = {})
56
- get("/api/v1/users/#{user}/page_views", params)
57
- end
58
-
59
- def user_merge(user, merge_into_user)
60
- put("/api/v1/users/#{user}/merge_into/#{merge_into_user}")
61
- end
62
-
63
- def user_assignments(user, course, params = {})
64
- get("/api/v1/users/#{user}/courses/#{course}/assignments", params)
65
- end
66
-
67
- def dashboard_positions(user, params = {})
68
- get("/api/v1/users/#{user}/dashboard_positions", params)
69
- end
70
-
71
- def update_dashboard_positions(user, params = {})
72
- put("/api/v1/users/#{user}/dashboard_positions", params)
73
- end
74
-
75
48
  def favorite_courses(user, params = {})
76
49
  params.merge!({as_user_id: user})
77
50
  get("/api/v1/users/self/favorites/courses", params)
@@ -1,76 +1,104 @@
1
1
  require 'active_support/core_ext/hash'
2
2
  require 'footrest/client'
3
- require 'paul_walker'
3
+ require_relative 'rate_limiting'
4
+ require_relative 'client_module'
4
5
 
5
6
  module Bearcat
6
7
  class Client < Footrest::Client
7
8
  require 'bearcat/api_array'
8
9
 
10
+ @added_modules = []
11
+
9
12
  Dir[File.join(__dir__, 'client', '*.rb')].each do |mod|
10
13
  mname = File.basename(mod, '.*').camelize
11
14
  mname = 'GraphQL' if mname == 'GraphQl'
12
15
  require mod
13
- include "Bearcat::Client::#{mname}".constantize
16
+ lmod = "Bearcat::Client::#{mname}".constantize
17
+ include lmod
18
+ @added_modules << lmod
19
+ end
20
+
21
+ def self.registered_endpoints
22
+ @registered_endpoints ||= @added_modules.reduce({}) do |h, m|
23
+ h.merge!(m._registered_endpoints) rescue h
24
+ end
25
+ @registered_endpoints
14
26
  end
15
27
 
16
- # Override Footrest request for ApiArray support
17
28
  def request(method, &block)
18
- enforce_rate_limits
19
- response = connection.send(method, &block)
20
- apply_rate_limits(response.headers['x-rate-limit-remaining'])
29
+ response = rate_limited_request do
30
+ connection.send(method, &block)
31
+ end
21
32
  ApiArray.process_response(response, self)
22
33
  end
23
34
 
24
- def enforce_rate_limits
25
- return unless limit_remaining.present?
26
- return unless limit_remaining < Bearcat.rate_limit_min
35
+ protected
27
36
 
28
- tts = ((Bearcat.rate_limit_min - limit_remaining) / 5).ceil
29
- tts = Bearcat.min_sleep_seconds if tts < Bearcat.min_sleep_seconds
30
- tts = Bearcat.max_sleep_seconds if tts > Bearcat.max_sleep_seconds
37
+ def rate_limited_request
38
+ return yield unless rate_limiter
31
39
 
40
+ canvas_rate_limits= 0
41
+ response = nil
32
42
 
33
- message = "Canvas API rate limit minimum #{Bearcat.rate_limit_min} reached. "\
34
- "Sleeping for #{tts} second(s) to catch up ~zzZZ~. "\
35
- "Limit Remaining: #{limit_remaining}"
36
- Bearcat.logger.debug(message)
43
+ begin
44
+ rate_limiter.apply(
45
+ rate_limit_key,
46
+ max_sleep: Bearcat.max_sleep_seconds,
47
+ on_sleep: ->(tts) {
48
+ message = "Canvas API rate limit reached; sleeping for #{tts.to_i} second(s) to catch up."
49
+ Bearcat.logger.debug(message)
50
+ },
51
+ ) do
52
+ response = yield
53
+ 0
54
+ end
55
+ rescue Footrest::HttpError::Forbidden => err
56
+ # Somehow our rate-limiting didn't limit enough and Canvas stopped us.
57
+ response = err.response
58
+ if canvas_rate_limits < 2 && err.message.include?("(Rate Limit Exceeded)")
59
+ canvas_rate_limits += 1
60
+ rate_limiter.checkin_known(rate_limit_key, 0)
37
61
 
38
- sleep(tts)
39
- end
62
+ message = "Canvas API applied rate limit; upticking Bearcat rate-limit avoidance and retrying (Retry #{canvas_rate_limits})."
63
+ Bearcat.logger.debug(message)
40
64
 
41
- def apply_rate_limits(limit)
42
- return if limit.nil?
43
- self.limit_remaining = limit.to_i
44
- end
65
+ retry
66
+ end
67
+ raise
68
+ ensure
69
+ headers = response.try(:response_headers) || response.try(:headers) || {}
70
+ # -50 to provide a little extra leeway and hopefully be more proactive, making sure we don't even get close to Canvas throwing a 403, even if an out-of-band process is involved
71
+ rate_limiter.checkin_known(rate_limit_key, headers['x-rate-limit-remaining'].to_f - 100) if response
72
+ end
45
73
 
46
- def using_master_rate_limit?
47
- config[:master_rate_limit].present? || Bearcat.master_rate_limit.present?
74
+ response
48
75
  end
49
76
 
50
- def limit_remaining
51
- if using_master_rate_limit?
52
- Bearcat.master_mutex.synchronize do
53
- limit = PaulWalker::RateLimit.get(config[:token], config[:token])
54
- if limit.nil?
55
- PaulWalker::RateLimit.add(config[:token], config[:token], 0, Bearcat::rate_limit_min)
56
- limit = { current: 0 }.with_indifferent_access
57
- end
58
- Bearcat.logger.debug limit['current'].to_s
59
- limit['current']
77
+ def rate_limiter
78
+ @rate_limiter ||= begin
79
+ rl = config[:rate_limiter] || Bearcat.rate_limiter
80
+ master_rate_limit = config[:master_rate_limit].present? ? config[:master_rate_limit] : Bearcat.master_rate_limit
81
+
82
+ if rl.nil? && master_rate_limit.nil? && defined?(Rails) && Rails.env.production?
83
+ master_rate_limit = true if defined?(::Sidekiq)
60
84
  end
61
- else
62
- Bearcat.rate_limits[config[:token]]
63
- end
64
- end
65
85
 
66
- def limit_remaining=(value)
67
- if using_master_rate_limit?
68
- Bearcat.master_mutex.synchronize do
69
- PaulWalker::RateLimit.add(config[:token], config[:token], value, Bearcat::rate_limit_min)
86
+ if rl.nil? && master_rate_limit
87
+ rl = RateLimiting::RedisLimiter
88
+ end
89
+
90
+ if rl.is_a?(Class)
91
+ rl.new()
92
+ elsif rl.present?
93
+ rl
70
94
  end
71
- else
72
- Bearcat.rate_limits[config[:token]] = value
73
95
  end
74
96
  end
97
+
98
+ private
99
+
100
+ def rate_limit_key
101
+ Digest::SHA1.hexdigest(config[:token])
102
+ end
75
103
  end
76
104
  end
@@ -0,0 +1,101 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module ClientModule
4
+ ARG_REGEX = /:(\w+)/
5
+ attr_reader :_registered_endpoints
6
+
7
+ def prefix(prefix)
8
+ past_prefix = @current_prefix
9
+ @current_prefix = (past_prefix || '') + prefix
10
+ yield
11
+ ensure
12
+ @current_prefix = past_prefix
13
+ end
14
+
15
+ def endpoint(method, identifier, url = "", defaults: {}, &blk)
16
+ if @current_prefix && !url.start_with?('/')
17
+ url = url[2..] if url.start_with?('./')
18
+ url = @current_prefix + url
19
+ end
20
+
21
+ args = url.to_enum(:scan, ARG_REGEX).map { Regexp.last_match }
22
+ arg_names = args.map{|m| m[1]}
23
+
24
+ @_registered_endpoints ||= {}
25
+ @_registered_endpoints[identifier] = { symbol: identifier, method: method, url: url }
26
+
27
+ # TODO: Consider generating the method using class_eval and a template - this will improve runtime performance
28
+ # signature_bits = []
29
+ # logical_bits = []
30
+ # args.each do |m|
31
+ # name = [1]
32
+ # end
33
+
34
+ # interpolated_url = url.gsub(ARG_REGEX) do |m|
35
+ # '#{' + m[1] + '}'
36
+ # end
37
+
38
+ # class_eval <<~RUBY
39
+ # def #{identifier}(*args, **kwargs)
40
+ # parameters = {
41
+
42
+ # }
43
+
44
+ # #{method}(#{interpolated_url})
45
+ # end
46
+ # RUBY
47
+
48
+ define_method(identifier) do |*args, **kwargs|
49
+ url_arguments = {}
50
+ parameters = { }.with_indifferent_access
51
+ parameters.merge!(defaults)
52
+
53
+ args.each_with_index do |v, i|
54
+ if arg_names[i]
55
+ url_arguments[arg_names[i]] = v
56
+ elsif i == arg_names.count && v.is_a?(Hash)
57
+ parameters.merge!(v)
58
+ else
59
+ raise ArgumentError, "Too many arguments passed to #{identifier}" unless arg_names[i]
60
+ end
61
+ end
62
+
63
+ parameters.merge!(kwargs)
64
+
65
+ preq = BearcatRequest.new(url, url_arguments, parameters)
66
+ yield preq if block_given?
67
+
68
+ arg_names.each do |an|
69
+ preq.arguments[an] = preq.parameters.delete(an) unless preq.arguments.key?(an)
70
+ raise ArgumentError, "Missing argument #{an}" unless preq.arguments.key?(an)
71
+ end
72
+
73
+ send(method, preq.interpoated_url, preq.parameters)
74
+ end
75
+ end
76
+
77
+ def context_types(types, &blk)
78
+ types.each(&blk)
79
+ end
80
+
81
+ %i[get post delete put patch head].each do |mthd|
82
+ define_method(mthd) do |*args, **kwargs, &blk|
83
+ endpoint(mthd, *args, **kwargs, &blk)
84
+ end
85
+ end
86
+
87
+ BearcatRequest = Struct.new(:url, :arguments, :parameters) do
88
+ def interpoated_url
89
+ url.gsub(ARG_REGEX) do |_|
90
+ m = Regexp.last_match
91
+ val = arguments[m[1]]
92
+ val = val.canvas_id if val.respond_to?(:canvas_id)
93
+ val = val.id if val.respond_to?(:id)
94
+ val = val['id'].presence || val[:id].presence || val if val.is_a?(Hash)
95
+ val
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end