lms-api 1.21.0 → 1.24.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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