lms-api 1.21.0 → 1.24.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.
@@ -0,0 +1,608 @@
1
+ module CanvasApi
2
+ module GoHelpers
3
+ def struct_fields(model, resource_name)
4
+ if !model["properties"]
5
+ puts "NO properties for #{resource_name} !!!!!!!!!!!!!!!!!!!!!"
6
+ return []
7
+ end
8
+
9
+ time_required = false
10
+ fields = model["properties"].map do |name, property|
11
+ description = ""
12
+ description << "#{safe_rb(property['description'].gsub("\n", "\n //"))}." if property["description"].present?
13
+ description << "Example: #{safe_rb(property['example'])}".gsub("..", "").gsub("\n", " ") if property["example"].present?
14
+
15
+ # clean up name
16
+ name = nested_arg(name)
17
+
18
+ if type = go_type(name, property, false, model, "")
19
+ if type.include? "time.Time"
20
+ time_required = true
21
+ end
22
+ go_declaration(name, type) + " // #{description}"
23
+ else
24
+ raise "Unable to determine type for #{name}"
25
+ end
26
+ end.compact
27
+
28
+ [fields, time_required]
29
+ end
30
+
31
+ # go_struct_fields handles the various forms of parameters documented in the Canvas API
32
+ # Examples:
33
+ #
34
+ # notification_preferences[<X>][frequency]
35
+ # Generates:
36
+ # NotificationPreferences map[string]string `json:"notification_preferences"`
37
+ #
38
+ # calendar_event[child_event_data][X][start_at]
39
+ # Generates:
40
+ # type ChildEventData struct {
41
+ # StartAt time.Time `json:"start_at"` // (Optional)
42
+ # EndAt time.Time `json:"end_at"` // (Optional)
43
+ # ContextCode string `json:"context_code"` // (Optional)
44
+ # }
45
+ # CalendarEvent struct {
46
+ # ChildEventData map[string]*ChildEventData
47
+ # }
48
+ #
49
+ # module_item[completion_requirement][type]
50
+ #
51
+ # events[start_at]
52
+ #
53
+ # Nested param
54
+ # account_notification[subject]
55
+ # Generates:
56
+ # AccountNotification struct {
57
+ # Subject string `json:"subject"` // (Required)
58
+ # Message string `json:"message"` // (Required)
59
+ # StartAt time.Time `json:"start_at"` // (Required)
60
+ # EndAt time.Time `json:"end_at"` // (Required)
61
+ # Icon string `json:"icon"` // (Optional) . Must be one of warning, information, question, error, calendar
62
+ # } `json:"account_notification"`
63
+ #
64
+ def go_struct_fields(nickname, params)
65
+ nested = {}
66
+ params.each do |p|
67
+ structs, name = split_nested(p)
68
+ go_to_tree(nickname, nested, structs, name, p)
69
+ end
70
+ go_render_params(nested)
71
+ end
72
+
73
+ def go_render_params(nested)
74
+ out = ""
75
+ nested.each do |name, val|
76
+ if val["paramType"]
77
+ out << "\n" + go_param_to_field(val, name)
78
+ elsif val[:array_of]
79
+ out << "\n#{struct_name(name)} #{val[:array_of]}"
80
+ elsif val[:map_of]
81
+ out << "\n#{struct_name(name)} #{val[:map_of]}"
82
+ else
83
+ out << "\n#{struct_name(name)} struct {"
84
+ out << go_render_params(val)
85
+ out << "\n} `json:\"#{name.underscore.gsub("`", "")}\" url:\"#{name.underscore.gsub("`", "")},omitempty\"`\n"
86
+ end
87
+ end
88
+ out
89
+ end
90
+
91
+ def go_render_child_structs
92
+ out = ""
93
+ @child_structs&.each do |name, params|
94
+ out << "\ntype #{struct_name(name)} struct {"
95
+ params.each do |n, p|
96
+ out << "\n" + go_param_to_field(p, n)
97
+ end
98
+ out << "\n}\n"
99
+ end
100
+ out
101
+ end
102
+
103
+ # HACK for https://canvas.instructure.com/doc/api/quiz_submissions.html
104
+ # update_student_question_scores_and_comments has a param with the following form
105
+ # {"paramType"=>"form", "name"=>"quiz_submissions[questions]", "type"=>"array", "format"=>nil, "required"=>false, "deprecated"=>false, "items"=>{"$ref"=>"Hash"}}
106
+ QuizSubmissionOverrides = "QuizSubmissionOverrides"
107
+
108
+ def go_to_tree(nickname, nested, structs, name, param)
109
+ @child_structs ||= {}
110
+
111
+ # HACK for https://canvas.instructure.com/doc/api/quiz_submissions.html
112
+ if nickname == "update_student_question_scores_and_comments"
113
+ @child_structs[QuizSubmissionOverrides] = {
114
+ "score" => {"name"=>"score", "type"=>"float"},
115
+ "comment" => {"name"=>"comment", "type"=>"string"},
116
+ }
117
+ end
118
+
119
+ if structs.length > 0
120
+ struct, rest = structs.first, structs[1..-1]
121
+ nested[struct] ||= {}
122
+ if rest
123
+ if is_x_param?(rest[1])
124
+ type = go_property_type(name, param)
125
+ child_name = rest[0]
126
+ child_struct = "#{struct_name(nickname)}#{struct_name(child_name)}"
127
+ @child_structs[child_struct] ||= {}
128
+ @child_structs[child_struct][name] = param
129
+ nested[struct][child_name] = {
130
+ "name" => "#{struct}[#{child_name}]",
131
+ "type" => "map[string]#{child_struct}",
132
+ "paramType" => param["paramType"],
133
+ "keep_type" => true,
134
+ }
135
+ rest.shift
136
+ rest.shift
137
+ elsif is_x_param?(rest[0])
138
+ if rest[0] == structs[1]
139
+ child_name = structs[0]
140
+ child_struct = "#{struct_name(nickname)}#{struct_name(child_name)}"
141
+ nested[struct][:map_of] = "map[string]#{child_struct}"
142
+ @child_structs[child_struct] ||= {}
143
+ @child_structs[child_struct][name] = param
144
+ else
145
+ type = go_property_type(name, param)
146
+ nested[struct][:map_of] = "map[string]#{type}"
147
+ end
148
+ rest.shift
149
+ end
150
+ end
151
+
152
+ if rest && rest.length > 0
153
+ go_to_tree(nickname, nested[struct], rest, name, param)
154
+ else
155
+ if @child_structs && child_struct = nested[struct][:array_of]
156
+ @child_structs[child_struct][name] = param
157
+ else
158
+ nested[struct][name] = param
159
+ end
160
+ end
161
+ else
162
+ nested[name] = param
163
+ if param["type"] == "array" && ["events"].include?(param["name"])
164
+ child_struct = "#{struct_name(nickname)}#{struct_name(param["name"])}"
165
+ @child_structs ||= {}
166
+ @child_structs[child_struct] ||= {}
167
+ nested[name][:array_of] = child_struct
168
+ puts "******** Using custom struct #{child_struct}"
169
+ end
170
+ end
171
+ end
172
+
173
+ def go_param_path(param)
174
+ if is_x_param?(param["name"])
175
+ "#{go_param_kind(param)}.#{go_name(param["name"])}"
176
+ elsif is_nested?(param)
177
+ structs, name = split_nested(param)
178
+ "#{go_param_kind(param)}.#{structs.map{|s| struct_name(s)}.join('.')}.#{go_name(name)}"
179
+ else
180
+ "#{go_param_kind(param)}.#{go_name(param["name"])}"
181
+ end
182
+ end
183
+
184
+ def is_x_param?(name)
185
+ if name
186
+ name.include?("[<X>]") ||
187
+ name.include?("<X>") ||
188
+ name.include?("X") ||
189
+ name.include?("<student_id>") ||
190
+ name.include?("0")
191
+ end
192
+ end
193
+
194
+ def is_nested?(param)
195
+ param["name"].include?("[")
196
+ end
197
+
198
+ def is_array(param)
199
+ param["type"] == "array"
200
+ end
201
+
202
+ def split_nested(param)
203
+ parts = param["name"].split("[").map{|p| p.gsub("]", "")}
204
+ [
205
+ parts[0...-1],
206
+ parts.last,
207
+ ]
208
+ end
209
+
210
+ def go_param_to_field(parameter, name = nil)
211
+ name ||= parameter["name"]
212
+ type = go_type(name, parameter)
213
+ go_declaration(name, type) + " // " + go_comments(parameter, false)
214
+ end
215
+
216
+ def go_comments(parameter, include_description = true)
217
+ out = " (#{parameter["required"] ? 'Required' : 'Optional'}) "
218
+ if parameter["enum"]
219
+ out << ". Must be one of #{parameter["enum"].join(', ')}"
220
+ end
221
+ if include_description && parameter["description"]
222
+ out << parameter["description"].gsub("\n", "\n// ")
223
+ end
224
+ out
225
+ end
226
+
227
+ def go_parameter_doc(parameter)
228
+ name = parameter["name"]
229
+ out = "# #{go_param_path(parameter)}"
230
+ out << go_comments(parameter)
231
+ out
232
+ end
233
+
234
+ def go_param_empty_value(parameter)
235
+ name = parameter["name"]
236
+ if is_x_param?(name)
237
+ return "nil"
238
+ end
239
+ type = go_type(name, parameter)
240
+ case type
241
+ when "int64"
242
+ when "int"
243
+ when "float64"
244
+ "0"
245
+ when "string"
246
+ '""'
247
+ when "time.Time"
248
+ "nil"
249
+ when "map[string](interface{})"
250
+ "nil"
251
+ else
252
+ if type.include?("[]")
253
+ "nil"
254
+ else
255
+ "need empty value for #{type}"
256
+ end
257
+ end
258
+ end
259
+
260
+ def is_required_field(parameter)
261
+ parameter["required"] && !["bool", "int64", "int", "float64"].include?(go_type(parameter["name"], parameter))
262
+ end
263
+
264
+ def go_declaration(name, type)
265
+ json = name.underscore.split("[")[0].gsub("`rlid`", "rlid")
266
+ out = "#{go_name(name)} #{type} `json:\"#{json}\" url:\"#{json},omitempty\"`"
267
+ end
268
+
269
+ def go_name(name)
270
+ parts = name.split("[")
271
+ parts[0].camelize.gsub("-", "").gsub("_", "")
272
+ .gsub("`rlid`", "RLID")
273
+ .gsub("Id", "ID")
274
+ .gsub("url", "URL")
275
+ .gsub("Sis", "SIS")
276
+ .gsub("MediaTrackk", "MediaTrack")
277
+ .gsub("Https:::::Canvas.instructure.com::Lti::Submission", "CanvasLTISubmission")
278
+ end
279
+
280
+ def struct_name(type)
281
+ # Remove chars and fix spelling errors
282
+ cleaned = type.split('|').first.strip.gsub(" ", "_")
283
+ go_name(cleaned)
284
+ end
285
+
286
+ def go_require_models(parameters, nickname, return_type)
287
+ parameters.any? { |p| go_type(p["name"], p).include?("models") } ||
288
+ ["assign_unassigned_members"].include?(@nickname) ||
289
+ (return_type &&
290
+ return_type != "bool" &&
291
+ !return_type.include?("string") &&
292
+ !return_type.include?("SuccessResponse") &&
293
+ !return_type.include?("UnreadCount")
294
+ )
295
+ end
296
+
297
+ def time_required?(parameters)
298
+ parameters.any? { |p| go_type(p["name"], p).include?("time.Time") }
299
+ end
300
+
301
+ def go_type(name, property, return_type = false, model = nil, namespace = "models.")
302
+ if property["$ref"]
303
+ "*#{namespace}#{struct_name(property['$ref'])}"
304
+ else
305
+ go_property_type(name, property, return_type, model, namespace)
306
+ end
307
+ end
308
+
309
+ def go_property_type(name, property, return_type = false, model = nil, namespace = "models.")
310
+ return property["type"] if property["keep_type"]
311
+ return property[:array_of] if property[:array_of]
312
+
313
+ # Canvas API docs are wrong for these so we HACK in the right type
314
+ return "float64" if name.downcase == "points_possible"
315
+
316
+ type = property["type"].downcase
317
+ case type
318
+ when "{success: true}"
319
+ "canvasapi.SuccessResponse"
320
+ when "integer", "string", "boolean", "datetime", "number", "date"
321
+ go_primitive(name, type, property["format"])
322
+ when "void"
323
+ "bool" # TODO this doesn't seem right?
324
+ when "array"
325
+ go_ref_property_type(property, namespace)
326
+ when "object"
327
+ puts "Using string type for '#{name}' ('#{property}') of type object."
328
+ "map[string](interface{})"
329
+ else
330
+ if property["type"] == "array of outcome ids"
331
+ "[]string"
332
+ elsif property["type"] == "list of content items"
333
+ # HACK There's no list of content items object so we return an array of string
334
+ "[]string"
335
+ elsif property["type"].include?('{ "unread_count": "integer" }')
336
+ "canvasapi.UnreadCount"
337
+ elsif return_type
338
+ "*#{namespace}#{struct_name(property["type"])}"
339
+ elsif property["type"] == "Hash"
340
+ "map[string](interface{})"
341
+ elsif property["type"] == "String[]"
342
+ "[]string"
343
+ elsif property["type"] == "[Answer]"
344
+ "[]*models.Answer"
345
+ elsif property["type"] == "QuizUserConversation"
346
+ "canvasapi.QuizUserConversation"
347
+ elsif [
348
+ "Assignment",
349
+ "BlueprintRestriction",
350
+ "RubricAssessment",
351
+ ].include?(property["type"])
352
+ "*models.#{property["type"]}"
353
+ elsif property["type"] == "multiple BlueprintRestrictions"
354
+ "[]*models.BlueprintRestriction"
355
+ elsif property["type"] == "File"
356
+ # This won't work. If we ever need to use this type we'll need to do some refactoring
357
+ "string"
358
+ elsif property["type"] == "Deprecated"
359
+ "string"
360
+ elsif property["type"] == "SerializedHash"
361
+ # Not sure this will work
362
+ "map[string](interface{})"
363
+ elsif property["type"].downcase == "json"
364
+ "map[string](interface{})"
365
+ elsif ["Numeric", "float"].include?(property["type"])
366
+ "float64"
367
+ elsif property["type"] == "GroupMembership | Progress"
368
+ "no-op" # this is handled further up the stack
369
+ elsif property["type"] == "URL"
370
+ "string"
371
+ elsif property["type"] == "uuid"
372
+ "string"
373
+ else
374
+ raise "Unable to match '#{name}' requested property '#{property}' to Go Type."
375
+ end
376
+ end
377
+ end
378
+
379
+ def go_ref_property_type(property, namespace)
380
+ ref_type = property.dig("items", "$ref")
381
+ if ref_type == nil
382
+ if property["type"] == "array"
383
+ "[]string"
384
+ else
385
+ "string"
386
+ end
387
+ elsif ref_type == "Hash"
388
+ # HACK for https://canvas.instructure.com/doc/api/quiz_submissions.html
389
+ if property["name"] == "quiz_submissions[questions]"
390
+ "map[string]QuizSubmissionOverrides"
391
+ else
392
+ raise "No type available for #{property}"
393
+ end
394
+ elsif ref_type == "[Integer]"
395
+ "[]int"
396
+ elsif ref_type == "Array"
397
+ "[]string"
398
+ elsif ref_type == "[String]"
399
+ "[]string"
400
+ elsif ref_type == "DateTime" || ref_type == "Date"
401
+ "[]time.Time"
402
+ elsif ref_type == "object"
403
+ "map[string](interface{})"
404
+ elsif ref_type
405
+ # HACK on https://canvas.instructure.com/doc/api/submissions.json
406
+ # the ref value is set to a full sentence rather than a
407
+ # simple type, so we look for that specific value
408
+ if ref_type.include?("UserDisplay if anonymous grading is not enabled")
409
+ "[]*#{namespace}UserDisplay"
410
+ elsif ref_type.include?("Url String The url to the result that was created")
411
+ "string"
412
+ else
413
+ "[]*#{namespace}#{struct_name(ref_type)}"
414
+ end
415
+ else
416
+ "[]#{go_primitive(name, property["items"]["type"].downcase, property["items"]["format"])}"
417
+ end
418
+ rescue
419
+ raise "Unable to discover Go list type for '#{name}' ('#{property}')."
420
+ end
421
+
422
+ def go_primitive(name, type, format)
423
+ case type
424
+ when "integer"
425
+ if name.end_with?("_ids")
426
+ "[]int64"
427
+ else
428
+ "int64"
429
+ end
430
+ when "number"
431
+ if format == "Float"
432
+ "float64"
433
+ else
434
+ # TODO many of the LMS types with 'number' don't indicate a type so we have to guess
435
+ # Hopefully that changes. For now we go with float
436
+ "float64"
437
+ end
438
+ when "string"
439
+ "string"
440
+ when "boolean"
441
+ "bool"
442
+ when "datetime"
443
+ "time.Time"
444
+ when "date"
445
+ "time.Time"
446
+ else
447
+ raise "Unable to match requested primitive '#{type}' to Go Type."
448
+ end
449
+ end
450
+
451
+ def go_field_validation(model)
452
+ return nil unless model["properties"]
453
+ allowable = {}
454
+ model["properties"].each do |name, property|
455
+ if property["allowableValues"]
456
+ values = property["allowableValues"]["values"].map do |value|
457
+ "\"#{value}\""
458
+ end
459
+ allowable[name] = {
460
+ values: values,
461
+ type: property["type"],
462
+ }
463
+ end
464
+ end
465
+ allowable
466
+ end
467
+
468
+ def go_param_kind(parmeter)
469
+ case parmeter["paramType"]
470
+ when "path"
471
+ "Path"
472
+ when "query"
473
+ "Query"
474
+ when "form"
475
+ "Form"
476
+ else
477
+ "Unknown paramType"
478
+ end
479
+ end
480
+
481
+ def go_path_params(params)
482
+ select_params("path", params)
483
+ end
484
+
485
+ def go_query_params(params)
486
+ select_params("query", params)
487
+ end
488
+
489
+ def go_form_params(params)
490
+ select_params("form", params)
491
+ end
492
+
493
+ def select_params(type, parameters)
494
+ params = parameters.select{|p| p["paramType"] == type}
495
+ if params && !params.nil? && params.length > 0
496
+ params
497
+ else
498
+ nil
499
+ end
500
+ end
501
+
502
+ def go_api_url
503
+ url = @api_url
504
+ @args.each do |arg|
505
+ url.gsub(arg, "+\"#{go_name(arg)}\"+")
506
+ end
507
+ url
508
+ end
509
+
510
+ def is_paged?(operation)
511
+ operation["type"] == "array"
512
+ end
513
+
514
+ def next_param(operation)
515
+ if is_paged?(operation)
516
+ ", next *url.URL"
517
+ end
518
+ end
519
+
520
+ def go_do_return_statement(operation, nickname)
521
+ if nickname == "assign_unassigned_members" || is_paged?(operation)
522
+ "return nil, nil, err"
523
+ elsif type = go_return_type(operation)
524
+ if type == "bool"
525
+ "return false, err"
526
+ elsif type == "string"
527
+ 'return "", err'
528
+ elsif type == "integer"
529
+ "return 0, err"
530
+ else
531
+ "return nil, err"
532
+ end
533
+ else
534
+ "return err"
535
+ end
536
+ end
537
+
538
+ def go_do_final_return_statement(operation, nickname)
539
+ if nickname == "assign_unassigned_members"
540
+ "return &groupMembership, &progress, nil"
541
+ elsif go_return_type(operation)
542
+ if is_paged?(operation)
543
+ "return ret, pagedResource, nil"
544
+ elsif operation["type"] == "boolean" || operation["type"] == "string" || operation["type"] == "integer"
545
+ "return ret, nil"
546
+ else
547
+ "return &ret, nil"
548
+ end
549
+ else
550
+ "return nil"
551
+ end
552
+ end
553
+
554
+ def go_do_return_value(operation, nickname)
555
+ if nickname == "assign_unassigned_members"
556
+ # HACK. harded coded because Assign unassigned members returns different values based on input
557
+ # see https://canvas.instructure.com/doc/api/group_categories.html#method.group_categories.assign_unassigned_members
558
+ "(*models.GroupMembership, *models.Progress, error)"
559
+ elsif type = go_return_type(operation)
560
+ if is_paged?(operation)
561
+ "(#{type}, *canvasapi.PagedResource, error)"
562
+ else
563
+ "(#{type}, error)"
564
+ end
565
+ else
566
+ "error"
567
+ end
568
+ end
569
+
570
+ def go_return_type(operation, is_decl = false)
571
+ prefix = is_decl ? "" : "*"
572
+ suffix = is_decl ? "{}" : ""
573
+ if operation["type"] == "void"
574
+ nil
575
+ elsif is_paged?(operation)
576
+ model = operation.dig("items", "$ref")
577
+ if model.include?(" ")
578
+ # Handle cases with spaces using go_property_type
579
+ type = go_property_type(operation["nickname"], operation)
580
+ if type == "string"
581
+ type = "[]#{type}"
582
+ end
583
+ "#{type}#{suffix}"
584
+ else
585
+ "[]*models.#{go_name(model)}#{suffix}"
586
+ end
587
+ elsif operation["type"] == "boolean"
588
+ "bool"
589
+ elsif operation["type"] == "integer"
590
+ "int64"
591
+ elsif model = operation["type"]
592
+ if model.include?(" ")
593
+ # Handle cases with spaces using go_property_type
594
+ type = go_property_type(operation["nickname"], operation)
595
+ if type == "string"
596
+ type
597
+ else
598
+ "#{prefix}#{type}#{suffix}"
599
+ end
600
+ else
601
+ "#{prefix}models.#{go_name(model)}#{suffix}"
602
+ end
603
+ else
604
+ raise "No return type found for #{operation}"
605
+ end
606
+ end
607
+ end
608
+ end
@@ -0,0 +1,24 @@
1
+ module CanvasApi
2
+ def nested_arg(str)
3
+ # update_list_of_blackout_dates has a parameter named 'blackout_dates:'
4
+ # This results in a parameter in the resolver named 'blackout_dates::'
5
+ # which causes a syntax error.
6
+ if str[str.length() - 1] == ":"
7
+ str = str.chop
8
+ end
9
+
10
+ # TODO/HACK we are replacing values from the string here to get things to work for now.
11
+ # However, removing these symbols means that the methods that use the arguments
12
+ # generated herein will have bugs and be unusable.
13
+ str.gsub("[", "_").
14
+ gsub("]", "").
15
+ gsub("*", "star").
16
+ gsub("<", "_").
17
+ gsub(">", "_").
18
+ gsub("`", "").
19
+ gsub("https://canvas.instructure.com/lti/", "lti_").
20
+ gsub("https://www.instructure.com/", "").
21
+ gsub("https://purl.imsglobal.org/spec/lti/claim/", "lti_claim_").
22
+ gsub(".", "")
23
+ end
24
+ end
@@ -93,6 +93,13 @@ module CanvasApi
93
93
  return str unless str.is_a?(String)
94
94
  str.gsub('"', "'")
95
95
  end
96
+
97
+ def graphql_resolver_class(name)
98
+ # HACK Some resolvers have both singular and plural versions, so keep the plural on those
99
+ return name.camelize if name == "get_custom_colors"
100
+
101
+ name.classify
102
+ end
96
103
  end
97
104
 
98
- end
105
+ end