lms-api 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +84 -0
- data/Rakefile +27 -0
- data/lib/canvas_api.rb +4 -0
- data/lib/canvas_api/builder.rb +121 -0
- data/lib/canvas_api/js_graphql_helpers.rb +98 -0
- data/lib/canvas_api/js_helpers.rb +58 -0
- data/lib/canvas_api/rb_graphql_helpers.rb +241 -0
- data/lib/canvas_api/render.rb +74 -0
- data/lib/canvas_api/ruby_helpers.rb +9 -0
- data/lib/canvas_api/templates/constant.erb +10 -0
- data/lib/canvas_api/templates/constants.erb +4 -0
- data/lib/canvas_api/templates/course_id_required.erb +1 -0
- data/lib/canvas_api/templates/course_ids_required.erb +5 -0
- data/lib/canvas_api/templates/ex_action.erb +1 -0
- data/lib/canvas_api/templates/ex_default_action.erb +1 -0
- data/lib/canvas_api/templates/ex_url.erb +18 -0
- data/lib/canvas_api/templates/ex_urls.erb +3 -0
- data/lib/canvas_api/templates/js_graphql_model.erb +9 -0
- data/lib/canvas_api/templates/js_graphql_mutation.erb +0 -0
- data/lib/canvas_api/templates/js_graphql_mutations.erb +6 -0
- data/lib/canvas_api/templates/js_graphql_queries.erb +7 -0
- data/lib/canvas_api/templates/js_graphql_query.erb +7 -0
- data/lib/canvas_api/templates/js_graphql_types.erb +100 -0
- data/lib/canvas_api/templates/js_url.erb +1 -0
- data/lib/canvas_api/templates/js_urls.erb +3 -0
- data/lib/canvas_api/templates/rb_forward_declarations.erb +14 -0
- data/lib/canvas_api/templates/rb_graphql_field.erb +3 -0
- data/lib/canvas_api/templates/rb_graphql_input_type.erb +14 -0
- data/lib/canvas_api/templates/rb_graphql_mutation.erb +20 -0
- data/lib/canvas_api/templates/rb_graphql_mutation_include.erb +1 -0
- data/lib/canvas_api/templates/rb_graphql_mutations.erb +15 -0
- data/lib/canvas_api/templates/rb_graphql_resolver.erb +27 -0
- data/lib/canvas_api/templates/rb_graphql_root_query.erb +14 -0
- data/lib/canvas_api/templates/rb_graphql_type.erb +14 -0
- data/lib/canvas_api/templates/rb_url.erb +1 -0
- data/lib/canvas_api/templates/rb_urls.erb +5 -0
- data/lib/lms/canvas.rb +447 -0
- data/lib/lms/canvas_urls.rb +812 -0
- data/lib/lms/course_ids_required.rb +309 -0
- data/lib/lms/helper_urls.rb +8 -0
- data/lib/lms/utils.rb +6 -0
- data/lib/lms/version.rb +3 -0
- data/lib/lms_api.rb +4 -0
- data/lib/tasks/canvas_api.rake +23 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/canvas_api.rb
ADDED
@@ -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
|