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.
- 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
|