lms-api 1.0.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.
@@ -0,0 +1,3 @@
1
+ module LMS
2
+ VERSION = "1.0.0"
3
+ end
data/lib/lms.rb ADDED
@@ -0,0 +1,4 @@
1
+ module LMS
2
+ end
3
+
4
+ require 'lms/api'
@@ -0,0 +1,10 @@
1
+ <% parameters_doc = parameters_doc(operation) -%>
2
+ // <%=@summary%>
3
+ // <%=@notes%>
4
+ //
5
+ // API Docs: https://canvas.instructure.com/doc/api/<%=@resource_name%>.html
6
+ // API Url: <%=@api_url%>
7
+ //
8
+ // Example:<%=parameters_doc%>
9
+ // return canvasRequest(<%=@nickname%>, {<%=@args.join(', ')%>}<%= parameters_doc.present? ? ", query" : ""%>);
10
+ export const <%=@nickname%> = { type: "<%=@nickname.upcase%>", method: "<%=@method.downcase%>", key: "<%=reducer_key(@nickname, @args)%>", required: <%=js_args(@args)%> };
@@ -0,0 +1,4 @@
1
+ //
2
+ // <%= @description %>
3
+ //
4
+ <%=@content.join("\n\n")%>
@@ -0,0 +1,7 @@
1
+ const <%=@model['id']%> = new GraphQLObjectType({
2
+ name: "<%=@model['id']%>",
3
+ description: "<%=@description%>. API Docs: https://canvas.instructure.com/doc/api/<%=@name%>.html",
4
+ fields: function(){
5
+ <%=fields(@model, @resource_name).join(",\n ")%>
6
+ }
7
+ });
File without changes
@@ -0,0 +1,6 @@
1
+ const Mutuation = new GraphQLObjectType({
2
+ name: 'Lms Api Mutations',
3
+ fields: {
4
+ <%=@content.join("\n\n")%>
5
+ }
6
+ });
@@ -0,0 +1,7 @@
1
+ const Query = new GraphQLObjectType({
2
+ name: 'Lms Api Queries',
3
+ description: "Root of the Lms Api",
4
+ fields: () => ({
5
+ <%=@content.join("\n\n")%>
6
+ })
7
+ });
@@ -0,0 +1,7 @@
1
+ <%=@nickname%>: {
2
+ type: new GraphQLList(Post),
3
+ description: "<%=@description%>",
4
+ resolve: function(context, args) {
5
+ return canvasRequest(context, args, urls.<%=@nickname.upcase%>);
6
+ }
7
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ GraphQLBoolean,
3
+ GraphQLFloat,
4
+ GraphQLID,
5
+ GraphQLInt,
6
+ GraphQLList,
7
+ GraphQLNonNull,
8
+ GraphQLObjectType,
9
+ GraphQLSchema,
10
+ GraphQLString
11
+ } from 'graphql';
12
+
13
+ import {
14
+ connectionArgs,
15
+ connectionDefinitions,
16
+ connectionFromArray,
17
+ fromGlobalId,
18
+ globalIdField,
19
+ mutationWithClientMutationId,
20
+ nodeDefinitions,
21
+ connectionFromPromisedArray
22
+ } from 'graphql-relay';
23
+
24
+ import {
25
+ GraphQLLimitedString,
26
+ GraphQLDateTime
27
+ } from 'graphql-custom-types';
28
+
29
+ /**
30
+ * We get the node interface and field from the Relay library.
31
+ *
32
+ * The first method defines the way we resolve an ID to its object.
33
+ * The second defines the way we resolve an object to its GraphQL type.
34
+ */
35
+ var {nodeInterface, nodeField} = nodeDefinitions(
36
+ (globalId) => {
37
+ var {type, id} = fromGlobalId(globalId);
38
+ if (type === 'User') {
39
+ return getUser(id);
40
+ } else if (type === 'Widget') {
41
+ return getWidget(id);
42
+ } else {
43
+ return null;
44
+ }
45
+ },
46
+ (obj) => {
47
+ if (obj instanceof User) {
48
+ return userType;
49
+ } else if (obj instanceof Widget) {
50
+ return widgetType;
51
+ } else {
52
+ return null;
53
+ }
54
+ }
55
+ );
56
+
57
+ <%=@content.join("\n\n")%>
58
+
59
+ const Schema = new GraphQLSchema({
60
+ query: Query,
61
+ mutation: Mutuation
62
+ });
63
+
64
+ export default Schema;
@@ -0,0 +1 @@
1
+ <%=@nickname.upcase%>: { uri: function(args){return <%=js_url_parts(@api_url).join(' + ')%>}, method: "<%=@method%>", parameters: <%=@parameters.to_json%> }
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ <%=@content.join(",\n ")%>
3
+ };
@@ -0,0 +1 @@
1
+ "<%=@nickname.upcase%>" => { uri: ->(<%=@args.map{|a| "#{a}:"}.join(', ')%>) { "<%=ruby_api_url(@api_url)%>" }, method: "<%=@method%>", parameters: <%=@parameters%> }
@@ -0,0 +1,5 @@
1
+ module LMS
2
+ URLs = {
3
+ <%=@content.join(",\n ")%>
4
+ }
5
+ end
@@ -0,0 +1,265 @@
1
+ namespace :lms do
2
+
3
+ module GraphQLHelpers
4
+
5
+ def graphQLType(name, property, resource_name)
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
+ graphQLPrimitive(type, property['format'])
13
+ when "array"
14
+ begin
15
+ if property["items"]["$ref"]
16
+ type = property["items"]["$ref"]
17
+ else
18
+ type = graphQLPrimitive(property["items"]["type"], property["items"]["format"])
19
+ end
20
+ rescue => ex
21
+ type = "GraphQLString"
22
+ end
23
+ "new GraphQLList(#{type})"
24
+ when "object"
25
+ # TODO handle objects
26
+ nil
27
+ else
28
+ "unknown - #{property}"
29
+ end
30
+ end
31
+ end
32
+
33
+ def graphQLResolve(name, property, resource_name)
34
+ if property["$ref"]
35
+ "function(model){ return model.#{name}; }"
36
+ elsif property['type'] == "array" && property["items"] && property["items"]["$ref"]
37
+ "function(model){ return model.#{name}; }"
38
+ end
39
+ end
40
+
41
+ def graphQLPrimitive(type, format)
42
+ case type
43
+ when "integer"
44
+ "GraphQLInt"
45
+ when "number"
46
+ if format == "float64"
47
+ "GraphQLFloat"
48
+ else
49
+ # TODO many of the LMS types with 'number' don't indicate a type so we have to guess
50
+ # Hopefully that changes. For now we go with float
51
+ "GraphQLFloat"
52
+ end
53
+ when "string"
54
+ "GraphQLString"
55
+ when "boolean"
56
+ "GraphQLBoolean"
57
+ when "datetime"
58
+ "GraphQLDateTime"
59
+ end
60
+ end
61
+
62
+ def fields(model, resource_name)
63
+ model['properties'].map do |name, property|
64
+
65
+ # HACK. This property doesn't have any metadata. Throw in a couple lines of code specific to this field.
66
+ if name == "created_source" && property == "manual|sis|api"
67
+ "#{name}: new GraphQLEnumType({ name: '#{name}', values: { manual: { value: 'manual' }, sis: { value: 'sis' }, api: { value: 'api' } } })"
68
+ else
69
+
70
+ description = ""
71
+ if property["description"].present? && property["example"].present?
72
+ description << "#{safeJs(property["description"])}. Example: #{safeJs(property["example"])}".gsub("..", "").gsub("\n", " ")
73
+ end
74
+
75
+ if type = graphQLType(name, property, resource_name)
76
+ resolve = graphQLResolve(name, property, resource_name)
77
+ resolve = "resolve: #{resolve}, " if resolve.present?
78
+ "#{name}: { type: #{type}, #{resolve}description: \"#{description}\" }"
79
+ end
80
+
81
+ end
82
+
83
+ end.compact
84
+ end
85
+
86
+ def safeJs(str)
87
+ str = str.join(', ') if str.is_a?(Array)
88
+ str = str.map{|k, v| v}.join(', ') if str.is_a?(Hash)
89
+ return str unless str.is_a?(String)
90
+ str.gsub('"', "'")
91
+ end
92
+ end
93
+
94
+ module JsHelpers
95
+ def js_url_parts(api_url)
96
+ api_url.split(/(\{[a-z_]+\})/).map do |part|
97
+ if part[0] == "{"
98
+ arg = part.gsub(/[\{\}]/, "")
99
+ "args['#{arg}']"
100
+ else
101
+ %Q{"#{part}"}
102
+ end
103
+ end
104
+ end
105
+
106
+ def js_args(args)
107
+ if args.present?
108
+ "[\"#{args.join('","')}\"]"
109
+ else
110
+ "[]"
111
+ end
112
+ end
113
+
114
+ def parameters_doc(operation)
115
+ if operation["parameters"].present?
116
+ parameters = operation["parameters"]
117
+ .reject{|p| p["paramType"] == "path"}
118
+ .map{|p| "#{p['name']}#{p['required'] ? ' (required)' : ''}" }
119
+ .compact
120
+ if parameters.length > 0
121
+ "\n// const query = {\n// #{parameters.join("\n// ")}\n// }"
122
+ else
123
+ ''
124
+ end
125
+ else
126
+ ''
127
+ end
128
+ end
129
+
130
+ def key_args(args)
131
+ if args.blank?
132
+ ""
133
+ elsif args.length > 1
134
+ "#{nickname}_{#{args.join('}_{')}}"
135
+ else
136
+ "#{nickname}_#{args[0]}"
137
+ end
138
+ end
139
+
140
+ def reducer_key(nickname, args)
141
+ "#{nickname}#{key_args(args)}"
142
+ end
143
+
144
+ end
145
+
146
+ module RubyHelpers
147
+
148
+ def ruby_api_url(api_url)
149
+ api_url.gsub("{", "#\{")
150
+ end
151
+
152
+ end
153
+
154
+ class Render
155
+ include GraphQLHelpers
156
+ include JsHelpers
157
+ include RubyHelpers
158
+ attr_accessor :template, :description, :resource, :api_url, :operation,
159
+ :args, :method, :api, :name, :resource_name, :resource_api,
160
+ :nickname, :notes, :content, :summary, :model, :model_name
161
+
162
+ def initialize(template, api, resource, resource_api, operation, parameters, content, model)
163
+ @template = File.read(File.expand_path(template, __dir__))
164
+ if api
165
+ @api = api
166
+ @name = @api["path"].gsub("/", "").gsub(".json", "")
167
+ @description = @api["description"]
168
+ end
169
+ if resource
170
+ @resource = resource
171
+ @resource_name = resource["resourcePath"].gsub("/", "")
172
+ end
173
+ if resource_api
174
+ @resource_api = resource_api
175
+ @api_url = resource_api['path'].gsub("/v1/", "")
176
+ @args = args(@api_url)
177
+ end
178
+ if operation
179
+ nickname = operation["nickname"]
180
+ nickname = "#{@name}_#{nickname}" if ["upload_file", "query_by_course", "preview_processed_html", "create_peer_review_courses", "create_peer_review_sections", "set_extensions_for_student_quiz_submissions"].include?(nickname)
181
+
182
+ @method = operation["method"]
183
+ @operation = operation
184
+ @nickname = nickname
185
+ @notes = operation['notes'].gsub("\n", "\n// ")
186
+ @summary = operation["summary"]
187
+ end
188
+ if parameters
189
+ @parameters = parameters.map{|p| p.delete("description"); p}
190
+ end
191
+ @content = content
192
+ @model = model
193
+ end
194
+
195
+ def args(api_url)
196
+ api_url.split('/').map do |part|
197
+ if part[0] == "{"
198
+ part.gsub(/[\{\}]/, "")
199
+ end
200
+ end.compact
201
+ end
202
+
203
+ def render()
204
+ ERB.new(@template, nil, '-').result(binding).strip
205
+ end
206
+
207
+ def save(file)
208
+ File.write(file, render)
209
+ end
210
+
211
+ end
212
+
213
+ class LMSApiBuilder
214
+
215
+ def self.build
216
+ endpoint = "https://canvas.instructure.com/doc/api"
217
+ directory = HTTParty.get("#{endpoint}/api-docs.json")
218
+ lms_urls_rb = []
219
+ lms_urls_js = []
220
+ models = []
221
+ queries = []
222
+ mutations = []
223
+ directory["apis"].each do |api|
224
+ puts "Generating #{api['description']}"
225
+ resource = HTTParty.get("#{endpoint}#{api["path"]}")
226
+ constants = []
227
+ resource['apis'].each do |resource_api|
228
+ resource_api["operations"].each do |operation|
229
+ parameters = operation["parameters"]
230
+ constants << Render.new("./lms_api/constant.erb", api, resource, resource_api, operation, parameters, nil, nil).render
231
+ lms_urls_rb << Render.new("./lms_api/rb_url.erb", api, resource, resource_api, operation, parameters, nil, nil).render
232
+ lms_urls_js << Render.new("./lms_api/js_url.erb", api, resource, resource_api, operation, parameters, nil, nil).render
233
+ if "GET" == operation["method"].upcase
234
+ queries << Render.new("./lms_api/graphql_query.erb", api, resource, resource_api, operation, parameters, nil, nil).render
235
+ else
236
+ mutations << Render.new("./lms_api/graphql_mutation.erb", api, resource, resource_api, operation, parameters, nil, nil).render
237
+ end
238
+ end
239
+ end
240
+ resource['models'].map do |name, model|
241
+ if model['properties'] # Don't generate models without properties
242
+ models << Render.new("./lms_api/graphql_model.erb", api, resource, nil, nil, nil, nil, model).render
243
+ end
244
+ end
245
+ # Generate one file of constants for every LMS API
246
+ constants_renderer = Render.new("./lms_api/constants.erb", api, resource, nil, nil, nil, constants, nil)
247
+ constants_renderer.save("#{Rails.root}/../atomic-client/client/js/libs/lms/constants/#{constants_renderer.name}.js")
248
+ end
249
+
250
+ Render.new("./lms_api/rb_urls.erb", nil, nil, nil, nil, nil, lms_urls_rb, nil).save("#{Rails.root}/lib/lms/urls.rb")
251
+ Render.new("./lms_api/js_urls.erb", nil, nil, nil, nil, nil, lms_urls_js, nil).save("#{Rails.root}/../atomic-lti/apps/lib/lms/urls.js")
252
+
253
+ Render.new("./lms_api/graphql_types.erb", nil, nil, nil, nil, nil, models.compact, nil).save("#{Rails.root}/../atomic-lti/apps/lib/lms/graphql_types.js")
254
+ Render.new("./lms_api/graphql_queries.erb", nil, nil, nil, nil, nil, queries, nil).save("#{Rails.root}/../atomic-lti/apps/lib/lms/graphql_queries.js")
255
+ Render.new("./lms_api/graphql_mutations.erb", nil, nil, nil, nil, nil, mutations, nil).save("#{Rails.root}/../atomic-lti/apps/lib/lms/graphql_mutations.js")
256
+ end
257
+
258
+ end
259
+
260
+ desc "Scrape the LMS api"
261
+ task :api => [:environment] do
262
+ LMSApiBuilder.build
263
+ end
264
+
265
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lms-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamis Buck
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.7
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: httparty
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Wrapper for LMS API
84
+ email:
85
+ - jamis@jamisbuck.org
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - MIT-LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/lms.rb
94
+ - lib/lms/api.rb
95
+ - lib/lms/urls.rb
96
+ - lib/lms/version.rb
97
+ - lib/tasks/lms_api.rake
98
+ - lib/tasks/lms_api/constant.erb
99
+ - lib/tasks/lms_api/constants.erb
100
+ - lib/tasks/lms_api/graphql_model.erb
101
+ - lib/tasks/lms_api/graphql_mutation.erb
102
+ - lib/tasks/lms_api/graphql_mutations.erb
103
+ - lib/tasks/lms_api/graphql_queries.erb
104
+ - lib/tasks/lms_api/graphql_query.erb
105
+ - lib/tasks/lms_api/graphql_types.erb
106
+ - lib/tasks/lms_api/js_url.erb
107
+ - lib/tasks/lms_api/js_urls.erb
108
+ - lib/tasks/lms_api/rb_url.erb
109
+ - lib/tasks/lms_api/rb_urls.erb
110
+ homepage: https://github.com/atomicjolt/lms_api
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.5.1
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Wrapper for LMS API
134
+ test_files: []