lms-api 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +84 -0
  4. data/Rakefile +27 -0
  5. data/lib/canvas_api.rb +4 -0
  6. data/lib/canvas_api/builder.rb +121 -0
  7. data/lib/canvas_api/js_graphql_helpers.rb +98 -0
  8. data/lib/canvas_api/js_helpers.rb +58 -0
  9. data/lib/canvas_api/rb_graphql_helpers.rb +241 -0
  10. data/lib/canvas_api/render.rb +74 -0
  11. data/lib/canvas_api/ruby_helpers.rb +9 -0
  12. data/lib/canvas_api/templates/constant.erb +10 -0
  13. data/lib/canvas_api/templates/constants.erb +4 -0
  14. data/lib/canvas_api/templates/course_id_required.erb +1 -0
  15. data/lib/canvas_api/templates/course_ids_required.erb +5 -0
  16. data/lib/canvas_api/templates/ex_action.erb +1 -0
  17. data/lib/canvas_api/templates/ex_default_action.erb +1 -0
  18. data/lib/canvas_api/templates/ex_url.erb +18 -0
  19. data/lib/canvas_api/templates/ex_urls.erb +3 -0
  20. data/lib/canvas_api/templates/js_graphql_model.erb +9 -0
  21. data/lib/canvas_api/templates/js_graphql_mutation.erb +0 -0
  22. data/lib/canvas_api/templates/js_graphql_mutations.erb +6 -0
  23. data/lib/canvas_api/templates/js_graphql_queries.erb +7 -0
  24. data/lib/canvas_api/templates/js_graphql_query.erb +7 -0
  25. data/lib/canvas_api/templates/js_graphql_types.erb +100 -0
  26. data/lib/canvas_api/templates/js_url.erb +1 -0
  27. data/lib/canvas_api/templates/js_urls.erb +3 -0
  28. data/lib/canvas_api/templates/rb_forward_declarations.erb +14 -0
  29. data/lib/canvas_api/templates/rb_graphql_field.erb +3 -0
  30. data/lib/canvas_api/templates/rb_graphql_input_type.erb +14 -0
  31. data/lib/canvas_api/templates/rb_graphql_mutation.erb +20 -0
  32. data/lib/canvas_api/templates/rb_graphql_mutation_include.erb +1 -0
  33. data/lib/canvas_api/templates/rb_graphql_mutations.erb +15 -0
  34. data/lib/canvas_api/templates/rb_graphql_resolver.erb +27 -0
  35. data/lib/canvas_api/templates/rb_graphql_root_query.erb +14 -0
  36. data/lib/canvas_api/templates/rb_graphql_type.erb +14 -0
  37. data/lib/canvas_api/templates/rb_url.erb +1 -0
  38. data/lib/canvas_api/templates/rb_urls.erb +5 -0
  39. data/lib/lms/canvas.rb +447 -0
  40. data/lib/lms/canvas_urls.rb +812 -0
  41. data/lib/lms/course_ids_required.rb +309 -0
  42. data/lib/lms/helper_urls.rb +8 -0
  43. data/lib/lms/utils.rb +6 -0
  44. data/lib/lms/version.rb +3 -0
  45. data/lib/lms_api.rb +4 -0
  46. data/lib/tasks/canvas_api.rake +23 -0
  47. metadata +175 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cfafd60f403921db6037838554ff73e249aba5b3d19946a6a001c73598534ee8
4
+ data.tar.gz: 5ba978a32528ff25e6139d18ddaf60e3b82bbf1b412d357c6562d9bccfbf4939
5
+ SHA512:
6
+ metadata.gz: 71254525d790ceceb28697bed2871c0b1994586c536e523d4a75f27e3a38bb1a9e61a0d7b099eb2c042ef900238bfd296c8bcae53cb98311c9c6d92ecfb01501
7
+ data.tar.gz: bc99de213a91082613dab0c2cfe82e1eec4c40813271146e7e341aded7bf51a02e07eced2982c8ff4870eb283448d9639820073208e3fee5650e071d78609d9c
@@ -0,0 +1,20 @@
1
+ Copyright 2016-2017 Atomic Jolt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,84 @@
1
+ # LMS API
2
+
3
+ This project provides a wrapper around the Instructure Canvas API.
4
+
5
+
6
+ ## Installation
7
+
8
+ To install, add `lms-api` to your Gemfile:
9
+
10
+ ```ruby
11
+ gem "lms-api"
12
+ ```
13
+
14
+
15
+ ## Configuration
16
+
17
+ Your app must tell the gem which model is used to represent the
18
+ authentication state. For instance, if you're using ActiveRecord, you
19
+ might have an `Authentication` model, which encapsulates a temporary
20
+ API token.
21
+
22
+ ```ruby
23
+ class Authentication < ActiveRecord::Base
24
+ # token: string
25
+ end
26
+ ```
27
+
28
+ Then, you tell the gem about this model:
29
+
30
+ ```ruby
31
+ LMS::Canvas.auth_state_model = Authentication
32
+ ```
33
+
34
+ This allows the gem to transparently refresh the token when the token
35
+ expires, and do so in a way that respects multiple processes all trying
36
+ to do so in parallel.
37
+
38
+
39
+ ## Usage
40
+
41
+ To use the API wrapper, instantiate a `LMS::Canvas` instance with the
42
+ url of the LMS instance you want to communicate with, as well as the
43
+ current authentication object, and (optionally) a hash of options to use
44
+ when refreshing the API token.
45
+
46
+ Require the gem:
47
+ `require "lms_api"`
48
+
49
+ ```ruby
50
+ auth = Authentication.first # or however you are storing global auth state
51
+ api = LMS::Canvas.new("http://your.canvas.instance", auth,
52
+ client_id: "...",
53
+ client_secret: "..."
54
+ redirect_uri: "..."
55
+ refresh_token: "...")
56
+ ```
57
+
58
+ You can get the URL for a given LMS interface via the `::lms_url`
59
+ class method:
60
+
61
+ ```ruby
62
+ params = {
63
+ id: id,
64
+ course_id: course_id,
65
+ controller: "foo",
66
+ account_id: 1,
67
+ all_dates: true,
68
+ other_param: "foobar"}
69
+
70
+ url = LMS::Canvas.lms_url("GET_SINGLE_ASSIGNMENT", params)
71
+ ```
72
+
73
+ Once you have the URL, you can send the request by using `api_*_request`
74
+ methods:
75
+
76
+ * `api_get_request(url, headers={})`
77
+ * `api_post_request(url, payload, headers={})`
78
+ * `api_delete_request(url, headers={})`
79
+ * `api_get_all_request(url, headers={})`
80
+ * `api_get_blocks_request(url, headers={}, &block)`
81
+
82
+ The last two are convenience methods for fetching multiple pages of data.
83
+ The `api_get_all_request` method returns all rows in a single array. The
84
+ `api_get_blocks_request` method yields each "chunk" of data to the block.
@@ -0,0 +1,27 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+ require "rdoc/task"
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "LMS API"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("README.rdoc")
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
+ end
16
+
17
+ load "./lib/tasks/canvas_api.rake"
18
+
19
+ Bundler::GemHelper.install_tasks
20
+
21
+ begin
22
+ require "rspec/core/rake_task"
23
+ RSpec::Core::RakeTask.new(:spec)
24
+
25
+ task default: :spec
26
+ rescue LoadError
27
+ end
@@ -0,0 +1,4 @@
1
+ module CanvasApi
2
+ end
3
+
4
+ require "canvas_api/builder"
@@ -0,0 +1,121 @@
1
+ require "canvas_api/render"
2
+ require "byebug"
3
+ module CanvasApi
4
+
5
+ class Builder
6
+
7
+ #
8
+ # project_root: This is the directory where the canvas_urls.rb file will be written.
9
+ # This file contains all urls and functions for access to the Canvas API from this gem (lms_api).
10
+ # client_app_path: This where all client side Javascript for accessing the Canvas API will be written.
11
+ # server_app_path: This is where all server side Javascript for accessing the Canvas API will be written.
12
+ # Currently, this is generating GraphQL for Javascript and Ruby
13
+ #
14
+ def self.build(project_root, client_app_path, server_app_path, elixir_app_path, rb_graphql_app_path)
15
+ endpoint = "https://canvas.instructure.com/doc/api"
16
+ directory = HTTParty.get("#{endpoint}/api-docs.json")
17
+
18
+ lms_urls_rb = []
19
+ lms_urls_js = []
20
+ lms_urls_ex = []
21
+ course_ids_required_rb = []
22
+ models = []
23
+ js_graphql_queries = []
24
+ js_graphql_mutations = []
25
+
26
+ rb_graphql_fields = []
27
+ rb_graphql_mutations = []
28
+ rb_forward_declarations = []
29
+
30
+ nicknames = []
31
+
32
+ # Elixir has a default action that raises
33
+ lms_urls_ex << CanvasApi::Render.new("./templates/ex_default_action.erb", nil, nil, nil, nil, nil, nil, nil).render
34
+
35
+ directory["apis"].each do |api|
36
+ puts "Generating #{api['description']}"
37
+ resource = HTTParty.get("#{endpoint}#{api['path']}")
38
+ constants = []
39
+ resource["apis"].each do |resource_api|
40
+ resource_api["operations"].each do |operation|
41
+
42
+ # Prevent duplicates
43
+ nickname = operation["nickname"]
44
+ if nicknames.include?(nickname)
45
+ nickname = "#{api["description"].gsub(" ", "_").downcase}_#{nickname}"
46
+ end
47
+ nicknames << nickname
48
+ operation["nickname"] = nickname
49
+
50
+ parameters = operation["parameters"]
51
+ constants << CanvasApi::Render.new("./templates/constant.erb", api, resource, resource_api, operation, parameters, nil, nil).render
52
+ lms_urls_rb << CanvasApi::Render.new("./templates/rb_url.erb", api, resource, resource_api, operation, parameters, nil, nil).render
53
+ lms_urls_js << CanvasApi::Render.new("./templates/js_url.erb", api, resource, resource_api, operation, parameters, nil, nil).render
54
+ lms_urls_ex << CanvasApi::Render.new("./templates/ex_url.erb", api, resource, resource_api, operation, parameters, nil, nil).render
55
+ lms_urls_ex << CanvasApi::Render.new("./templates/ex_action.erb", api, resource, resource_api, operation, parameters, nil, nil).render
56
+
57
+ if parameters.detect{ |param| param["name"] == "course_id" && param["paramType"] == "path" }
58
+ course_ids_required_rb << CanvasApi::Render.new("./templates/course_id_required.erb", api, resource, resource_api, operation, parameters, nil, nil).render
59
+ end
60
+
61
+ if operation["method"].casecmp("GET") == 0
62
+ js_graphql_queries << CanvasApi::Render.new("./templates/js_graphql_query.erb", api, resource, resource_api, operation, parameters, nil, nil).render
63
+
64
+ # One file per Canvas graphql resolver
65
+ canvas_graphql_resolver_renderer = CanvasApi::Render.new("./templates/rb_graphql_resolver.erb", api, resource, resource_api, operation, parameters, nil, nil)
66
+ canvas_graphql_resolver_renderer.save("#{rb_graphql_app_path}/lib/lms_graphql/resolvers/canvas/#{canvas_graphql_resolver_renderer.nickname}.rb")
67
+ rb_graphql_fields << CanvasApi::Render.new("./templates/rb_graphql_field.erb", api, resource, resource_api, operation, parameters, nil, nil).render
68
+ else
69
+ js_graphql_mutations << CanvasApi::Render.new("./templates/js_graphql_mutation.erb", api, resource, resource_api, operation, parameters, nil, nil).render
70
+
71
+ rb_graphql_mutation_renderer = CanvasApi::Render.new("./templates/rb_graphql_mutation.erb", api, resource, resource_api, operation, parameters, nil, nil)
72
+ rb_graphql_mutation_renderer.save("#{rb_graphql_app_path}/lib/lms_graphql/mutations/canvas/#{rb_graphql_mutation_renderer.nickname}.rb")
73
+ rb_graphql_mutations << CanvasApi::Render.new("./templates/rb_graphql_mutation_include.erb", api, resource, resource_api, operation, parameters, nil, nil).render
74
+ end
75
+
76
+ end
77
+ end
78
+
79
+ resource["models"].map do |_name, model|
80
+ if model["properties"] # Don't generate models without properties
81
+ models << CanvasApi::Render.new("./templates/js_graphql_model.erb", api, resource, nil, nil, nil, nil, model).render
82
+ end
83
+
84
+ # Generate one file for each Canvas graphql type
85
+ canvas_graphql_type_render = CanvasApi::Render.new("./templates/rb_graphql_type.erb", api, resource, nil, nil, nil, nil, model)
86
+ canvas_graphql_type_render.save("#{rb_graphql_app_path}/lib/lms_graphql/types/canvas/#{model['id'].underscore.singularize}.rb")
87
+
88
+ canvas_graphql_input_render = CanvasApi::Render.new("./templates/rb_graphql_input_type.erb", api, resource, nil, nil, nil, nil, model)
89
+ canvas_graphql_input_render.save("#{rb_graphql_app_path}/lib/lms_graphql/types/canvas/#{model['id'].underscore.singularize}_input.rb")
90
+
91
+ rb_forward_declarations << "class Canvas#{model['id'].singularize}Input < BaseInputObject;end"
92
+ rb_forward_declarations << "class Canvas#{model['id'].singularize} < BaseType;end"
93
+ end
94
+
95
+ # Generate one file of constants for every LMS API
96
+ constants_renderer = CanvasApi::Render.new("./templates/constants.erb", api, resource, nil, nil, nil, constants, nil)
97
+ constants_renderer.save("#{client_app_path}/#{constants_renderer.name}.js")
98
+ end
99
+
100
+ CanvasApi::Render.new("./templates/rb_urls.erb", nil, nil, nil, nil, nil, lms_urls_rb, nil).save("#{project_root}/lib/lms/canvas_urls.rb")
101
+ CanvasApi::Render.new("./templates/js_urls.erb", nil, nil, nil, nil, nil, lms_urls_js, nil).save("#{server_app_path}/lib/canvas/urls.js")
102
+
103
+ # The elixir urls are sorted, to prevent linter errors
104
+ CanvasApi::Render.new("./templates/ex_urls.erb", nil, nil, nil, nil, nil, lms_urls_ex.sort, nil).save("#{elixir_app_path}/lib/canvas/actions.ex")
105
+
106
+ CanvasApi::Render.new("./templates/course_ids_required.erb", nil, nil, nil, nil, nil, course_ids_required_rb, nil).save("#{project_root}/lib/lms/course_ids_required.rb")
107
+
108
+ # GraphQL Javascript - still not complete
109
+ CanvasApi::Render.new("./templates/js_graphql_types.erb", nil, nil, nil, nil, nil, models.compact, nil).save("#{server_app_path}/lib/canvas/graphql_types.js")
110
+ CanvasApi::Render.new("./templates/js_graphql_queries.erb", nil, nil, nil, nil, nil, js_graphql_queries, nil).save("#{server_app_path}/lib/canvas/graphql_queries.js")
111
+ CanvasApi::Render.new("./templates/js_graphql_mutations.erb", nil, nil, nil, nil, nil, js_graphql_mutations, nil).save("#{server_app_path}/lib/canvas/graphql_mutations.js")
112
+
113
+ # GraphQL Ruby
114
+ CanvasApi::Render.new("./templates/rb_forward_declarations.erb", nil, nil, nil, nil, nil, rb_forward_declarations, nil).save("#{rb_graphql_app_path}/lib/lms_graphql/types/canvas_forward_declarations.rb")
115
+ CanvasApi::Render.new("./templates/rb_graphql_root_query.erb", nil, nil, nil, nil, nil, rb_graphql_fields, nil).save("#{rb_graphql_app_path}/lib/lms_graphql/types/canvas/query_type.rb")
116
+ CanvasApi::Render.new("./templates/rb_graphql_mutations.erb", nil, nil, nil, nil, nil, rb_graphql_mutations, nil).save("#{rb_graphql_app_path}/lib/lms_graphql/mutations/canvas/mutations.rb")
117
+ end
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,98 @@
1
+ module CanvasApi
2
+
3
+ module GraphQLHelpers
4
+
5
+ def graphql_type(name, property)
6
+ if property["$ref"]
7
+ "#{property['$ref']}, resolve: function(model){ return model.#{name}; }"
8
+ else
9
+ type = property["type"]
10
+ case type
11
+ when "integer", "string", "boolean", "datetime", "number"
12
+ graphql_primitive(type, property["format"])
13
+ when "array"
14
+ begin
15
+ type = if property["items"]["$ref"]
16
+ property["items"]["$ref"]
17
+ else
18
+ graphql_primitive(property["items"]["type"], property["items"]["format"])
19
+ end
20
+ rescue
21
+ puts "Unable to discover list type for '#{name}' ('#{property}'). Defaulting to GraphQLString"
22
+ type = "GraphQLString"
23
+ end
24
+ "new GraphQLList(#{type})"
25
+ when "object"
26
+ puts "Using string type for '#{name}' ('#{property}') of type object."
27
+ "GraphQLString"
28
+ else
29
+ puts "Unable to match '#{name}' requested property '#{property}' to GraphQL Type."
30
+ "GraphQLString"
31
+ end
32
+ end
33
+ end
34
+
35
+ def graphql_primitive(type, format)
36
+ case type
37
+ when "integer"
38
+ "GraphQLInt"
39
+ when "number"
40
+ if format == "float64"
41
+ "GraphQLFloat"
42
+ else
43
+ # TODO many of the LMS types with 'number' don't indicate a type so we have to guess
44
+ # Hopefully that changes. For now we go with float
45
+ "GraphQLFloat"
46
+ end
47
+ when "string"
48
+ "GraphQLString"
49
+ when "boolean"
50
+ "GraphQLBoolean"
51
+ when "datetime"
52
+ "GraphQLDateTime"
53
+ else
54
+ raise "Unable to match requested primitive '#{type}' to GraphQL Type."
55
+ end
56
+ end
57
+
58
+ def graphql_resolve(name, property)
59
+ if property["$ref"]
60
+ "resolve(model){ return model.#{name}; }"
61
+ elsif property["type"] == "array" && property["items"] && property["items"]["$ref"]
62
+ "resolve(model){ return model.#{name}; }"
63
+ end
64
+ "resolve(model){ return model.#{name}; }"
65
+ end
66
+
67
+ def graphql_fields(model, resource_name)
68
+ model["properties"].map do |name, property|
69
+ # HACK. This property doesn't have any metadata. Throw in a couple lines of code
70
+ # specific to this field.
71
+ if name == "created_source" && property == "manual|sis|api"
72
+ "#{name}: new GraphQLEnumType({ name: '#{name}', values: { manual: { value: 'manual' }, sis: { value: 'sis' }, api: { value: 'api' } } })"
73
+ else
74
+
75
+ description = ""
76
+ if property["description"].present? && property["example"].present?
77
+ description << "#{safe_js(property['description'])}. Example: #{safe_js(property['example'])}".gsub("..", "").gsub("\n", " ")
78
+ end
79
+
80
+ if type = graphql_type(name, property)
81
+ resolve = graphql_resolve(name, property)
82
+ resolve = "#{resolve}, " if resolve.present?
83
+ "#{name}: { type: #{type}, #{resolve}description: \"#{description}\" }"
84
+ end
85
+
86
+ end
87
+ end.compact
88
+ end
89
+
90
+ def safe_js(str)
91
+ str = str.join(", ") if str.is_a?(Array)
92
+ str = str.map { |_k, v| v }.join(", ") if str.is_a?(Hash)
93
+ return str unless str.is_a?(String)
94
+ str.gsub('"', "'")
95
+ end
96
+ end
97
+
98
+ end
@@ -0,0 +1,58 @@
1
+ module CanvasApi
2
+ module JsHelpers
3
+
4
+ def js_url_parts(api_url)
5
+ api_url.split(/(\{[a-z_]+\})/).map do |part|
6
+ if part[0] == "{"
7
+ arg = part.gsub(/[\{\}]/, "")
8
+ "args['#{arg}']"
9
+ else
10
+ %{"#{part}"}
11
+ end
12
+ end
13
+ end
14
+
15
+ def js_args(args)
16
+ if args.present?
17
+ "['#{args.join("', '")}']"
18
+ else
19
+ "[]"
20
+ end
21
+ end
22
+
23
+ def parameters_doc(operation, method)
24
+ if operation["parameters"].present?
25
+ parameters = operation["parameters"].
26
+ reject { |p| p["paramType"] == "path" }.
27
+ map { |p| "#{p['name']}#{p['required'] ? ' (required)' : ''}" }.
28
+ compact
29
+ if parameters.present?
30
+ if method == "get"
31
+ "\n// const query = {\n// #{parameters.join("\n// ")}\n// }"
32
+ else
33
+ "\n// const body = {\n// #{parameters.join("\n// ")}\n// }"
34
+ end
35
+ else
36
+ ""
37
+ end
38
+ else
39
+ ""
40
+ end
41
+ end
42
+
43
+ def key_args(args)
44
+ if args.blank?
45
+ ""
46
+ elsif args.length > 1
47
+ "#{nickname}_{#{args.join('}_{')}}"
48
+ else
49
+ "#{nickname}_#{args[0]}"
50
+ end
51
+ end
52
+
53
+ def reducer_key(nickname, args)
54
+ "#{nickname}#{key_args(args)}"
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,241 @@
1
+ module CanvasApi
2
+ module GraphQLHelpers
3
+
4
+ def graphql_type(name, property, return_type = false, model = nil, input_type = false)
5
+ if property["$ref"]
6
+ canvas_name(property['$ref'], input_type)
7
+ elsif property["allowableValues"]
8
+ enum_class_name(model, name)
9
+ else
10
+ type = property["type"].downcase
11
+ case type
12
+ when "{success: true}"
13
+ "String"
14
+ when "integer", "string", "boolean", "datetime", "number", "date"
15
+ graphql_primitive(name, type, property["format"])
16
+ when "void"
17
+ "Boolean"
18
+ when "array"
19
+ begin
20
+ type = if property["items"]["$ref"] == "[Integer]"
21
+ "[Int]"
22
+ elsif property["items"]["$ref"] == "Array"
23
+ "[String]"
24
+ elsif property["items"]["$ref"] == "[String]"
25
+ "[String]"
26
+ elsif property["items"]["$ref"] == "DateTime" || property["items"]["$ref"] == "Date"
27
+ "[LMSGraphQL::Types::DateTimeType]"
28
+ elsif property["items"]["$ref"]
29
+ # HACK on https://canvas.instructure.com/doc/api/submissions.json
30
+ # the ref value is set to a full sentence rather than a
31
+ # simple type, so we look for that specific value
32
+ if property["items"]["$ref"].include?("UserDisplay if anonymous grading is not enabled")
33
+ "[LMSGraphQL::Types::Canvas::CanvasUserDisplay]"
34
+ elsif property["items"]["$ref"].include?("Url String The url to the result that was created")
35
+ "String"
36
+ else
37
+ "[#{canvas_name(property["items"]["$ref"], input_type)}]"
38
+ end
39
+ else
40
+ graphql_primitive(name, property["items"]["type"].downcase, property["items"]["format"])
41
+ end
42
+ rescue
43
+ puts "Unable to discover list type for '#{name}' ('#{property}'). Defaulting to String"
44
+ type = "String"
45
+ end
46
+ type
47
+ when "object"
48
+ puts "Using string type for '#{name}' ('#{property}') of type object."
49
+ "String"
50
+ else
51
+ if property["type"] == "TermsOfService"
52
+ # HACK There's no TermsOfService object so we return a string
53
+ "String"
54
+ elsif property["type"] == "list of content items"
55
+ # HACK There's no list of content items object so we return an array of string
56
+ "[String]"
57
+ elsif property["type"].include?('{ "unread_count": "integer" }')
58
+ # HACK TODO this should probably be a different type.
59
+ "Int"
60
+ elsif return_type
61
+ canvas_name(property["type"], input_type)
62
+ else
63
+ puts "Unable to match '#{name}' requested property '#{property}' to GraphQL Type."
64
+ "String"
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def canvas_name(type, input_type = false)
71
+ # Remove chars and fix spelling errors
72
+ name = type.split('|').first.strip.gsub(" ", "_").singularize.gsub("MediaTrackk", "MediaTrack")
73
+ "LMSGraphQL::Types::Canvas::Canvas#{name}#{input_type ? 'Input' : ''}"
74
+ end
75
+
76
+ def graphql_primitive(name, type, format)
77
+ return "[ID]" if name.end_with?("_ids")
78
+ return "ID" if name == "id" || name.end_with?("_id")
79
+ case type
80
+ when "integer"
81
+ "Int"
82
+ when "number"
83
+ if format == "float64"
84
+ "Float"
85
+ else
86
+ # TODO many of the LMS types with 'number' don't indicate a type so we have to guess
87
+ # Hopefully that changes. For now we go with float
88
+ "Float"
89
+ end
90
+ when "string"
91
+ "String"
92
+ when "boolean"
93
+ "Boolean"
94
+ when "datetime"
95
+ "LMSGraphQL::Types::DateTimeType"
96
+ when "date"
97
+ "LMSGraphQL::Types::DateTimeType"
98
+ else
99
+ raise "Unable to match requested primitive '#{type}' to GraphQL Type."
100
+ end
101
+ end
102
+
103
+ def enum_class_name(model, field_name)
104
+ "#{model['id'].classify}#{field_name.classify}Enum"
105
+ end
106
+
107
+ def graphql_field_enums(model)
108
+ return unless model["properties"]
109
+ enums = model["properties"].map do |name, property|
110
+ if property["allowableValues"]
111
+ values = property["allowableValues"]["values"].map do |value|
112
+ "value \"#{value}\""
113
+ end.join("\n ")
114
+ <<-CODE
115
+ class #{enum_class_name(model, name)} < ::GraphQL::Schema::Enum
116
+ #{values}
117
+ end
118
+ CODE
119
+ end
120
+ end.compact
121
+ if enums.length > 0
122
+ enums.join("\n ")
123
+ else
124
+ ""
125
+ end
126
+ end
127
+
128
+ def graphql_fields(model, resource_name, argument = false, input_type = false)
129
+ if !model["properties"]
130
+ puts "NO properties for #{resource_name} !!!!!!!!!!!!!!!!!!!!!"
131
+ return []
132
+ end
133
+ model["properties"].map do |name, property|
134
+ description = ""
135
+ description << "#{safe_rb(property['description'])}." if property["description"].present?
136
+ description << "Example: #{safe_rb(property['example'])}".gsub("..", "").gsub("\n", " ") if property["example"].present?
137
+
138
+ # clean up name
139
+ name = nested_arg(name)
140
+
141
+ if type = graphql_type(name, property, false, model, input_type)
142
+ if argument
143
+ <<-CODE
144
+ argument :#{name.underscore}, #{type}, "#{description}", required: false
145
+ CODE
146
+ else
147
+ <<-CODE
148
+ field :#{name.underscore}, #{type}, "#{description}", null: true
149
+ CODE
150
+ end
151
+ else
152
+ puts "Unable to determine type for #{name}"
153
+ end
154
+ end.compact
155
+ end
156
+
157
+ def type_from_operation(operation)
158
+ type = graphql_type("operation", operation, true)
159
+ end
160
+
161
+ def name_from_operation(operation)
162
+ type = no_brackets_period(type_from_operation(@operation))
163
+ if !is_basic_type(type)
164
+ make_file_name(type)
165
+ else
166
+ "return_value"
167
+ end
168
+ end
169
+
170
+ def is_basic_type(type)
171
+ ["Int", "String", "Boolean", "LMSGraphQL::Types::DateTimeType", "Float", "ID"].include?(type)
172
+ end
173
+
174
+ def no_brackets_period(str)
175
+ str.gsub("[", "").gsub("]", "").gsub(".", "")
176
+ end
177
+
178
+ def make_file_name(str)
179
+ str.underscore.split("/").last.split("|").first.gsub("canvas_", "").gsub(" ", "_").strip.singularize
180
+ end
181
+
182
+ def require_from_operation(operation)
183
+ type = no_brackets_period(type_from_operation(@operation))
184
+ if !is_basic_type(type)
185
+ "require_relative \"../../types/canvas/#{make_file_name(type)}\""
186
+ end
187
+ end
188
+
189
+ def require_from_properties(model)
190
+ return unless model["properties"]
191
+ requires = model["properties"].map do |name, property|
192
+ type = no_brackets_period(graphql_type(name, property, true, model))
193
+ if !is_basic_type(type) && !property["allowableValues"]
194
+ "require_relative \"#{make_file_name(type)}\""
195
+ end
196
+ end.compact
197
+ if requires.length > 0
198
+ requires.join("\n")
199
+ else
200
+ ""
201
+ end
202
+ end
203
+
204
+ def safe_rb(str)
205
+ str = str.join(", ") if str.is_a?(Array)
206
+ str = str.map { |_k, v| v }.join(", ") if str.is_a?(Hash)
207
+ return str unless str.is_a?(String)
208
+ str.gsub('"', "'")
209
+ end
210
+
211
+ def nested_arg(str)
212
+ # TODO/HACK we are replacing values from the string here to get things to work for now.
213
+ # However, removing these symbols means that the methods that use the arguments
214
+ # generated herein will have bugs and be unusable.
215
+ str.gsub("[", "_").
216
+ gsub("]", "").
217
+ gsub("*", "star").
218
+ gsub("<", "_").
219
+ gsub(">", "_").
220
+ gsub("`", "").
221
+ gsub("https://canvas.instructure.com/lti/", "").
222
+ gsub("https://www.instructure.com/", "").
223
+ gsub("https://purl.imsglobal.org/spec/lti/claim/", "").
224
+ gsub(".", "")
225
+ end
226
+
227
+ def params_as_string(parameters, paramTypes)
228
+ filtered = parameters.select{ |p| paramTypes.include?(p["paramType"]) }
229
+ if filtered && !filtered.empty?
230
+ s = filtered.
231
+ map{ |p| " \"#{p['name']}\": #{nested_arg(p['name'])}" }.
232
+ join(",\n")
233
+ " {\n#{s}\n }"
234
+ else
235
+ " {}"
236
+ end
237
+ end
238
+
239
+ end
240
+
241
+ end