lms-api 1.17.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 (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