lti2 0.0.1
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.rdoc +184 -0
- data/Rakefile +23 -0
- data/app/assets/javascripts/lti2/application.js +13 -0
- data/app/assets/stylesheets/lti2/application.css +15 -0
- data/app/controllers/lti2/application_controller.rb +4 -0
- data/app/helpers/lti2/application_helper.rb +4 -0
- data/app/views/layouts/lti2/application.html.erb +14 -0
- data/config/routes.rb +4 -0
- data/lib/lti2.rb +7 -0
- data/lib/lti2/engine.rb +9 -0
- data/lib/lti2/version.rb +3 -0
- data/lib/lti2_commons/lib/lti2_commons.rb +9 -0
- data/lib/lti2_commons/lib/lti2_commons/cache.rb +29 -0
- data/lib/lti2_commons/lib/lti2_commons/json_wrapper.rb +118 -0
- data/lib/lti2_commons/lib/lti2_commons/lti2_launch.rb +158 -0
- data/lib/lti2_commons/lib/lti2_commons/message_support.rb +218 -0
- data/lib/lti2_commons/lib/lti2_commons/oauth_request.rb +179 -0
- data/lib/lti2_commons/lib/lti2_commons/signer.rb +75 -0
- data/lib/lti2_commons/lib/lti2_commons/substitution_support.rb +87 -0
- data/lib/lti2_commons/lib/lti2_commons/utils.rb +28 -0
- data/lib/lti2_commons/lib/lti2_commons/version.rb +3 -0
- data/lib/lti2_commons/lib/lti2_commons/wire_log.rb +163 -0
- data/lib/lti2_commons/test/test_jsonpath.rb +255 -0
- data/lib/lti2_commons/test/test_jsonwrapper.rb +230 -0
- data/lib/lti2_commons/test/test_oauth_request.rb +143 -0
- data/lib/lti2_commons/test/test_substitution_support.rb +71 -0
- data/lib/lti2_commons/test/test_wire_log.rb +15 -0
- data/lib/tasks/lti2_tasks.rake +4 -0
- metadata +199 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'oauth'
|
3
|
+
require_relative 'oauth_request'
|
4
|
+
|
5
|
+
module Lti2Commons
|
6
|
+
module Signer
|
7
|
+
# Creates an OAuth signed request using the OAuth Gem - https://github.com/oauth/oauth-ruby
|
8
|
+
#
|
9
|
+
# @param launch_url [String] Endpoint of service to be launched
|
10
|
+
# @param http_method [String] Http method ('get', 'post', 'put', 'delete')
|
11
|
+
# @param consumer_key [String] OAuth consumer key
|
12
|
+
# @param consumer_secret [String] OAuth consumer secret
|
13
|
+
# @param params [Hash] Non-auth parameters or oauth parameter default values
|
14
|
+
# oauth_timestamp => defaults to current time
|
15
|
+
# oauth_nonce => defaults to random number
|
16
|
+
# oauth_signature_method => defaults to HMAC-SHA1 (also RSA-SHA1 supported)
|
17
|
+
# @param body [String] Body content. Usually would include this for body-signing of non form-encoded data.
|
18
|
+
# @param content_type [String] HTTP CONTENT-TYPE header; defaults: 'application/x-www-form-urlencoded'
|
19
|
+
# @return [Request] Signed request
|
20
|
+
def create_signed_request(launch_url, http_method, consumer_key, consumer_secret, params = {},
|
21
|
+
body = nil, content_type = nil, accept = nil)
|
22
|
+
params['oauth_consumer_key'] = consumer_key
|
23
|
+
params['oauth_nonce'] = (rand * 10E12).to_i.to_s unless params.key? 'oauth_nonce'
|
24
|
+
params['oauth_signature_method'] = 'HMAC-SHA1' unless params.key? 'oauth_signature_method'
|
25
|
+
params['oauth_timestamp'] = Time.now.to_i.to_s unless params.key? 'oauth_timestamp'
|
26
|
+
params['oauth_version'] = '1.0' unless params.key? 'oauth_version'
|
27
|
+
params['oauth_callback'] = 'about:blank'
|
28
|
+
|
29
|
+
content_type = 'application/x-www-form-urlencoded' unless content_type
|
30
|
+
|
31
|
+
launch_url = URI.unescape(launch_url)
|
32
|
+
uri = URI.parse(launch_url)
|
33
|
+
|
34
|
+
# flatten in query string arrays
|
35
|
+
if uri.query && uri.query != ''
|
36
|
+
CGI.parse(uri.query).each do |query_key, query_values|
|
37
|
+
params[query_key] = query_values.first unless params[query_key]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
final_uri = uri
|
42
|
+
uri = URI.parse(launch_url.split('?').first)
|
43
|
+
|
44
|
+
unless content_type == 'application/x-www-form-urlencoded'
|
45
|
+
params['oauth_body_hash'] = compute_oauth_body_hash body if body
|
46
|
+
end
|
47
|
+
|
48
|
+
request = OAuth::OAuthProxy::OAuthRequest.new(
|
49
|
+
'method' => http_method.to_s.upcase,
|
50
|
+
'uri' => uri,
|
51
|
+
'parameters' => params,
|
52
|
+
'final_uri' => final_uri
|
53
|
+
)
|
54
|
+
|
55
|
+
request.body = body
|
56
|
+
request.content_type = content_type
|
57
|
+
request.accept = accept
|
58
|
+
|
59
|
+
request.sign!(consumer_secret: consumer_secret)
|
60
|
+
|
61
|
+
# puts "Sender secret: #{consumer_secret}"
|
62
|
+
request
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Creates the value of an OAuth body hash
|
68
|
+
#
|
69
|
+
# @param launch_url [String] Content to be body signed
|
70
|
+
# @return [String] Signature base string (useful for debugging signature problems)
|
71
|
+
def compute_oauth_body_hash(content)
|
72
|
+
Base64.encode64(Digest::SHA1.digest(content.chomp)).gsub(/\n/, '')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Lti2Commons
|
2
|
+
module SubstitutionSupport
|
3
|
+
# Resolver resolves values by name from a variety of object sources.
|
4
|
+
# It's useful for variable substitution.
|
5
|
+
#
|
6
|
+
# The supported object sources are:
|
7
|
+
# Hash ::= value by key
|
8
|
+
# Proc ::= single-argument block evaluation
|
9
|
+
# Method ::= single-argument method obect evaluation
|
10
|
+
# Resolver ::= a nested resolver. useful for scoping resolvers;
|
11
|
+
# i.e. a constant, global inner resolver, but a one-time outer-resolver
|
12
|
+
# Object ::= value by dynamic instrospection of any object's accessor
|
13
|
+
#
|
14
|
+
# See the accompanying tester for numerous examples
|
15
|
+
class Resolver
|
16
|
+
attr_accessor :resolvers
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@resolver_hash = Hash.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add some type of resolver object_source to the Resolver
|
23
|
+
#
|
24
|
+
# @param key [String] A dotted name with the leading zone indicating the object category
|
25
|
+
# @param resolver [ObjectSource] a raw object_source for resolving
|
26
|
+
# @returns [String] value. If no resolution, return the incoming name
|
27
|
+
#
|
28
|
+
def add_resolver(key, resolver)
|
29
|
+
if resolver.is_a? Resolver
|
30
|
+
# Resolvers themselves should always be generic
|
31
|
+
key_sym = :*
|
32
|
+
else
|
33
|
+
key_sym = key.to_sym
|
34
|
+
end
|
35
|
+
@resolver_hash[key_sym] = [] unless @resolver_hash.key? key_sym
|
36
|
+
@resolver_hash[key_sym] << resolver
|
37
|
+
end
|
38
|
+
|
39
|
+
def resolve(full_name)
|
40
|
+
full_name ||= ''
|
41
|
+
zones = full_name.split('.')
|
42
|
+
return full_name if zones[0].blank?
|
43
|
+
category = zones[0].to_sym
|
44
|
+
name = zones[1..-1].join('.')
|
45
|
+
|
46
|
+
# Find any hits within category
|
47
|
+
@resolver_hash.each_pair do |k, v|
|
48
|
+
if k == category
|
49
|
+
result = resolve_by_category(full_name, name, v)
|
50
|
+
return result if result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Find any hits in global category
|
55
|
+
resolvers = @resolver_hash[:*]
|
56
|
+
result = resolve_by_category full_name, name, resolvers if resolvers
|
57
|
+
return result if result
|
58
|
+
|
59
|
+
"#{full_name}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
"Resolver for [#{@resolver_hash.keys}]"
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def resolve_by_category(full_name, name, resolvers)
|
69
|
+
resolvers.each do |resolver|
|
70
|
+
if resolver.is_a? Hash
|
71
|
+
value = resolver[name]
|
72
|
+
elsif resolver.is_a? Proc
|
73
|
+
value = resolver.call(name)
|
74
|
+
elsif resolver.is_a? Method
|
75
|
+
value = resolver.call(name)
|
76
|
+
elsif resolver.is_a? Resolver
|
77
|
+
value = resolver.resolve(full_name)
|
78
|
+
elsif resolver.is_a? Object
|
79
|
+
value = resolver.send(name)
|
80
|
+
end
|
81
|
+
return value if value
|
82
|
+
end
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'uuid'
|
3
|
+
|
4
|
+
module Lti2Commons
|
5
|
+
module Utils
|
6
|
+
def is_hash_intersect(node, constraint_hash)
|
7
|
+
# failed search if constraint_hash as invalid keys
|
8
|
+
return nil if (constraint_hash.keys - node.keys).length > 0
|
9
|
+
node.each_pair do |k, v|
|
10
|
+
if constraint_hash.key?(k)
|
11
|
+
return false unless v == constraint_hash[k]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def hash_to_query_string(hash)
|
18
|
+
hash.keys.inject('') do |query_string, key|
|
19
|
+
query_string << '&' unless key == hash.keys.first
|
20
|
+
query_string << "#{URI.encode(key.to_s)}=#{CGI.escape(hash[key])}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def substitute_template_values_from_hash(source_string, prefix, suffix, hash)
|
25
|
+
hash.each { |k, v| source_string.sub!(prefix + k + suffix, v) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Lti2Commons
|
4
|
+
module WireLogSupport
|
5
|
+
class WireLog
|
6
|
+
STATUS_CODES = {
|
7
|
+
100 => 'Continue',
|
8
|
+
101 => 'Switching Protocols',
|
9
|
+
102 => 'Processing',
|
10
|
+
|
11
|
+
200 => 'OK',
|
12
|
+
201 => 'Created',
|
13
|
+
202 => 'Accepted',
|
14
|
+
203 => 'Non-Authoritative Information',
|
15
|
+
204 => 'No Content',
|
16
|
+
205 => 'Reset Content',
|
17
|
+
206 => 'Partial Content',
|
18
|
+
207 => 'Multi-Status',
|
19
|
+
226 => 'IM Used',
|
20
|
+
|
21
|
+
300 => 'Multiple Choices',
|
22
|
+
301 => 'Moved Permanently',
|
23
|
+
302 => 'Found',
|
24
|
+
303 => 'See Other',
|
25
|
+
304 => 'Not Modified',
|
26
|
+
305 => 'Use Proxy',
|
27
|
+
307 => 'Temporary Redirect',
|
28
|
+
|
29
|
+
400 => 'Bad Request',
|
30
|
+
401 => 'Unauthorized',
|
31
|
+
402 => 'Payment Required',
|
32
|
+
403 => 'Forbidden',
|
33
|
+
404 => 'Not Found',
|
34
|
+
405 => 'Method Not Allowed',
|
35
|
+
406 => 'Not Acceptable',
|
36
|
+
407 => 'Proxy Authentication Required',
|
37
|
+
408 => 'Request Timeout',
|
38
|
+
409 => 'Conflict',
|
39
|
+
410 => 'Gone',
|
40
|
+
411 => 'Length Required',
|
41
|
+
412 => 'Precondition Failed',
|
42
|
+
413 => 'Request Entity Too Large',
|
43
|
+
414 => 'Request-URI Too Long',
|
44
|
+
415 => 'Unsupported Media Type',
|
45
|
+
416 => 'Requested Range Not Satisfiable',
|
46
|
+
417 => 'Expectation Failed',
|
47
|
+
422 => 'Unprocessable Entity',
|
48
|
+
423 => 'Locked',
|
49
|
+
424 => 'Failed Dependency',
|
50
|
+
426 => 'Upgrade Required',
|
51
|
+
|
52
|
+
500 => 'Internal Server Error',
|
53
|
+
501 => 'Not Implemented',
|
54
|
+
502 => 'Bad Gateway',
|
55
|
+
503 => 'Service Unavailable',
|
56
|
+
504 => 'Gateway Timeout',
|
57
|
+
505 => 'HTTP Version Not Supported',
|
58
|
+
507 => 'Insufficient Storage',
|
59
|
+
510 => 'Not Extended'
|
60
|
+
}
|
61
|
+
|
62
|
+
attr_accessor :is_logging, :output_file_name
|
63
|
+
|
64
|
+
def initialize(wire_log_name, output_file, is_html_output = true)
|
65
|
+
@output_file_name = output_file
|
66
|
+
@is_logging = true
|
67
|
+
@wire_log_name = wire_log_name
|
68
|
+
@log_buffer = nil
|
69
|
+
@is_html_output = is_html_output
|
70
|
+
end
|
71
|
+
|
72
|
+
def clear_log
|
73
|
+
output_file = File.open(@output_file_name, 'a+')
|
74
|
+
output_file.truncate(0)
|
75
|
+
output_file.close
|
76
|
+
end
|
77
|
+
|
78
|
+
def flush(options = {})
|
79
|
+
output_file = File.open(@output_file_name, 'a+')
|
80
|
+
@log_buffer.rewind
|
81
|
+
buffer = @log_buffer.read
|
82
|
+
if @is_html_output
|
83
|
+
oldbuffer = buffer.dup
|
84
|
+
buffer = ''
|
85
|
+
oldbuffer.each_char do |c|
|
86
|
+
if c == '<'
|
87
|
+
buffer << '<'
|
88
|
+
elsif c == '>'
|
89
|
+
buffer << '>'
|
90
|
+
else
|
91
|
+
buffer << c
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
if options.key? :css_class
|
96
|
+
css_class = options[:css_class]
|
97
|
+
else
|
98
|
+
css_class = "#{@wire_log_name}"
|
99
|
+
end
|
100
|
+
output_file.puts("<div class=\"#{css_class}\"><pre>") if @is_html_output
|
101
|
+
output_file.write(buffer)
|
102
|
+
output_file.puts("\n</div></pre>") if @is_html_output
|
103
|
+
output_file.close
|
104
|
+
@log_buffer = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
def log(s)
|
108
|
+
timestamp
|
109
|
+
raw_log("#{s}")
|
110
|
+
flush
|
111
|
+
end
|
112
|
+
|
113
|
+
def log_response(response, title = nil)
|
114
|
+
timestamp
|
115
|
+
raw_log(title.nil? ? 'Response' : "Response: #{title}")
|
116
|
+
raw_log("Status: #{response.code} #{STATUS_CODES[response.code]}")
|
117
|
+
headers = response.headers
|
118
|
+
unless headers.blank?
|
119
|
+
raw_log('Headers:')
|
120
|
+
headers.each { |k, v| raw_log("#{k}: #{v}") if k.downcase =~ /^content/ }
|
121
|
+
end
|
122
|
+
|
123
|
+
if response.body
|
124
|
+
# the following is expensive so do only when needed
|
125
|
+
raw_log('Body:') if @is_logging
|
126
|
+
begin
|
127
|
+
json_obj = JSON.load(response.body)
|
128
|
+
raw_log(JSON.pretty_generate(json_obj))
|
129
|
+
rescue
|
130
|
+
raw_log("#{response.body}")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
newline
|
134
|
+
flush(css_class: "#{@wire_log_name}Response")
|
135
|
+
end
|
136
|
+
|
137
|
+
def newline
|
138
|
+
raw_log("\n")
|
139
|
+
end
|
140
|
+
|
141
|
+
def log_buffer
|
142
|
+
# put in the css header if file doesn't exist
|
143
|
+
unless File.size? @output_file_name
|
144
|
+
@output_file = File.open(@output_file_name, 'a')
|
145
|
+
@output_file.puts '<link rel="stylesheet" type="text/css" href="wirelog.css" />'
|
146
|
+
@output_file.puts ''
|
147
|
+
@output_file.close
|
148
|
+
end
|
149
|
+
@log_buffer = StringIO.new unless @log_buffer
|
150
|
+
@log_buffer
|
151
|
+
end
|
152
|
+
|
153
|
+
def raw_log(s)
|
154
|
+
@log_buffer = log_buffer
|
155
|
+
@log_buffer.puts(s)
|
156
|
+
end
|
157
|
+
|
158
|
+
def timestamp
|
159
|
+
raw_log(Time.new)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'json'
|
4
|
+
require 'jsonpath'
|
5
|
+
|
6
|
+
class TestTree < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
@json_str =
|
9
|
+
<<PROXY
|
10
|
+
{
|
11
|
+
"@context" : [
|
12
|
+
"http://www.imsglobal.org/imspurl/lti/v2/ctx/ToolProxy",
|
13
|
+
"http://purl.org/blackboard/ctx/v1/iconStyle"
|
14
|
+
],
|
15
|
+
"@type" : "ToolProxy",
|
16
|
+
"@id" : "http://fabericious..com/ToolProxy/869e5ce5-214c-4e85-86c6-b99e8458a592",
|
17
|
+
"lti_version" : "LTI-2p0",
|
18
|
+
"tool_proxy_guid" : "869e5ce5-214c-4e85-86c6-b99e8458a592",
|
19
|
+
"tool_consumer_profile" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4",
|
20
|
+
"tool_profile" : {
|
21
|
+
"lti_version" : "LTI-2p0",
|
22
|
+
"product_instance" : {
|
23
|
+
"guid" : "fd75124a-140e-470f-944c-114d2d92bb40",
|
24
|
+
"product_info" : {
|
25
|
+
"product_name" : {
|
26
|
+
"default_value" : "Acme Assessments",
|
27
|
+
"key" : "tool.name"
|
28
|
+
},
|
29
|
+
"description" : {
|
30
|
+
"default_value" : "Acme Assessments provide an interactive test format.",
|
31
|
+
"key" : "tool.description"
|
32
|
+
},
|
33
|
+
"product_version" : "10.3",
|
34
|
+
"technical_description" : {
|
35
|
+
"default_value" : "Support provided for all LTI 1 extensions as well as LTI 2",
|
36
|
+
"key" : "tool.technical"
|
37
|
+
},
|
38
|
+
"product_family" : {
|
39
|
+
"code" : "assessment-tool",
|
40
|
+
"vendor" : {
|
41
|
+
"code" : "acme.com",
|
42
|
+
"name" : {
|
43
|
+
"default_value" : "Acme",
|
44
|
+
"key" : "tool.vendor.name"
|
45
|
+
},
|
46
|
+
"description" : {
|
47
|
+
"default_value" : "Acme is a leading provider of interactive tools for education",
|
48
|
+
"key" : "tool.vendor.description"
|
49
|
+
},
|
50
|
+
"website" : "http://acme.example.com",
|
51
|
+
"timestamp" : "2012-04-05T09:08:16-04:00",
|
52
|
+
"contact" : {
|
53
|
+
"email" : "info@example.com"
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
},
|
58
|
+
"support" : {
|
59
|
+
"email" : "helpdesk@example.com"
|
60
|
+
},
|
61
|
+
"service_provider" : {
|
62
|
+
"guid" : "18e7ea50-3d6d-4f6b-aff2-ed3ab577716c",
|
63
|
+
"provider_name" : {
|
64
|
+
"default_value" : "Acme Hosting",
|
65
|
+
"key" : "service_provider.name"
|
66
|
+
},
|
67
|
+
"description" : {
|
68
|
+
"default_value" : "Provider of high performance managed hosting environments",
|
69
|
+
"key" : "service_provider.description"
|
70
|
+
},
|
71
|
+
"support" : {
|
72
|
+
"email" : "support@acme-hosting.example.com"
|
73
|
+
},
|
74
|
+
"timestamp" : "2012-04-05T09:08:16-04:00"
|
75
|
+
}
|
76
|
+
},
|
77
|
+
"base_url_choice" : [
|
78
|
+
{ "default_base_url" : "http://acme.example.com",
|
79
|
+
"secure_base_url" : "https://acme.example.com",
|
80
|
+
"selector" : {
|
81
|
+
"applies_to" : [
|
82
|
+
"IconEndpoint",
|
83
|
+
"MessageHandler"
|
84
|
+
]
|
85
|
+
}
|
86
|
+
}
|
87
|
+
],
|
88
|
+
"resource_handler" : [
|
89
|
+
{
|
90
|
+
"name" : {
|
91
|
+
"default_value" : "Acme Assessment",
|
92
|
+
"key" : "assessment.resource.name"
|
93
|
+
},
|
94
|
+
"description" : {
|
95
|
+
"default_value" : "An interactive assessment using the Acme scale.",
|
96
|
+
"key" : "assessment.resource.description"
|
97
|
+
},
|
98
|
+
"message" : {
|
99
|
+
"message_type" : "basic-lti-launch-request",
|
100
|
+
"path" : "/handler/launchRequest",
|
101
|
+
"capability" : [
|
102
|
+
"Result.autocreate",
|
103
|
+
"Result.sourcedGUID"
|
104
|
+
],
|
105
|
+
"parameter" : [
|
106
|
+
{ "name" : "result_id",
|
107
|
+
"variable" : "$Result.sourcedGUID"
|
108
|
+
},
|
109
|
+
{ "name" : "discipline",
|
110
|
+
"fixed" : "chemistry"
|
111
|
+
}
|
112
|
+
]
|
113
|
+
},
|
114
|
+
"icon_info" : [
|
115
|
+
{
|
116
|
+
"default_location" : {
|
117
|
+
"path" : "/images/bb/en/icon.png"
|
118
|
+
},
|
119
|
+
"key" : "iconStyle.default.path"
|
120
|
+
},
|
121
|
+
{ "icon_style" : "BbListElementIcon",
|
122
|
+
"default_location" : {
|
123
|
+
"path" : "/images/bb/en/listElement.png"
|
124
|
+
},
|
125
|
+
"key" : "iconStyle.bb.listElement.path"
|
126
|
+
},
|
127
|
+
{ "icon_style" : "BbPushButtonIcon",
|
128
|
+
"default_location" : {
|
129
|
+
"path" : "images/bb/en/pushButton.png"
|
130
|
+
},
|
131
|
+
"key" : "iconStyle.bb.pushButton.path"
|
132
|
+
}
|
133
|
+
]
|
134
|
+
}
|
135
|
+
]
|
136
|
+
},
|
137
|
+
"security_contract" : {
|
138
|
+
"shared_secret" : "ThisIsASecret!",
|
139
|
+
"tool_service" : [
|
140
|
+
{ "@type" : "RestServiceProfile",
|
141
|
+
"service" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#ToolProxy.collection",
|
142
|
+
"action" : "POST"
|
143
|
+
},
|
144
|
+
{ "@type" : "RestServiceProfile",
|
145
|
+
"service" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#ToolProxy.item",
|
146
|
+
"action" : [
|
147
|
+
"GET",
|
148
|
+
"PUT"
|
149
|
+
]
|
150
|
+
},
|
151
|
+
{ "@type" : "RestService",
|
152
|
+
"service" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#Result.item",
|
153
|
+
"action" : [
|
154
|
+
"GET",
|
155
|
+
"PUT"
|
156
|
+
]
|
157
|
+
}
|
158
|
+
]
|
159
|
+
}
|
160
|
+
}
|
161
|
+
PROXY
|
162
|
+
|
163
|
+
@json_obj = JSON.parse(@json_str)
|
164
|
+
end
|
165
|
+
|
166
|
+
# asserts if expected_value is provided, else prints result
|
167
|
+
# useful for preparing new tests
|
168
|
+
def assert_node(jsonpath, expected_value=nil)
|
169
|
+
try_result = JsonPath.new(jsonpath).on(@json_obj)
|
170
|
+
if expected_value
|
171
|
+
assert_equal expected_value, try_result
|
172
|
+
else
|
173
|
+
puts "#{jsonpath}: #{try_result.inspect}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# asserts if singleton expected_value is provided, else prints result
|
178
|
+
# useful for preparing new tests
|
179
|
+
def assert_first(jsonpath, expected_value=nil)
|
180
|
+
try_result = JsonPath.new(jsonpath).on(@json_obj).first
|
181
|
+
if expected_value
|
182
|
+
assert_equal expected_value, try_result
|
183
|
+
else
|
184
|
+
puts "#{jsonpath}: #{try_result.inspect}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_path_on_json
|
189
|
+
# Note that same result for JSON string or loaded JSON object
|
190
|
+
assert_equal ["869e5ce5-214c-4e85-86c6-b99e8458a592"], JsonPath.new('tool_proxy_guid').on(@json_str)
|
191
|
+
assert_equal ["869e5ce5-214c-4e85-86c6-b99e8458a592"], JsonPath.new('tool_proxy_guid').on(@json_obj)
|
192
|
+
# assert_node 'tool_proxy_guid'
|
193
|
+
assert_node 'tool_proxy_guid', ["869e5ce5-214c-4e85-86c6-b99e8458a592"]
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_first
|
197
|
+
assert_equal "869e5ce5-214c-4e85-86c6-b99e8458a592", JsonPath.new('tool_proxy_guid').on(@json_str).first
|
198
|
+
end
|
199
|
+
|
200
|
+
def test_basics
|
201
|
+
assert_node 'tool_proxy_guid', ["869e5ce5-214c-4e85-86c6-b99e8458a592"]
|
202
|
+
assert_node 'security_contract.shared_secret', ["ThisIsASecret!"]
|
203
|
+
# 2 dots goes through array(s)
|
204
|
+
assert_node 'tool_profile.base_url_choice..default_base_url', ["http://acme.example.com"]
|
205
|
+
assert_node 'lti_version', ["LTI-2p0"]
|
206
|
+
end
|
207
|
+
|
208
|
+
def test_arrays
|
209
|
+
assert_node "tool_profile.resource_handler[0].message.path", ["/handler/launchRequest"]
|
210
|
+
assert_node 'tool_profile.resource_handler[0].message.message_type', ["basic-lti-launch-request"]
|
211
|
+
end
|
212
|
+
|
213
|
+
def test_filter0
|
214
|
+
# resource_handler = JsonPath.new('tool_profile.resource_handler').on(@json_obj)
|
215
|
+
# assert_node 'tool_profile.resource_handler[?(true)]'
|
216
|
+
# assert_node 'tool_profile.resource_handler.[?(@["message"]["message_type"]=="basic-lti-launch-request")]'
|
217
|
+
#find matching message_type and dig down to get message.path
|
218
|
+
assert_equal "/handler/launchRequest",
|
219
|
+
JsonPath.new('tool_profile.resource_handler.[?(@["message"]["message_type"]=="basic-lti-launch-request")]').on(@json_obj).first['message']['path']
|
220
|
+
|
221
|
+
# do the same replying JsonPath
|
222
|
+
assert_equal "/handler/launchRequest", JsonPath.new('@..message..path').on(
|
223
|
+
JsonPath.new('tool_profile.resource_handler.[?(@["message"]["message_type"]=="basic-lti-launch-request")]').on(@json_obj)).first
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
def test_filter
|
228
|
+
assert_first 'security_contract.tool_service[0].action', "POST"
|
229
|
+
assert_first 'security_contract.tool_service[?(@["action"]=="POST")]',
|
230
|
+
{"service"=>"http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#ToolProxy.collection", "action"=>"POST", "@type"=>"RestServiceProfile"}
|
231
|
+
# assert_first 'security_contract.tool_service[?(@["action"]=="POST")]'
|
232
|
+
assert_equal "POST", JsonPath.new('security_contract.tool_service[?(@["action"]=="POST")]').on(@json_obj).first['action']
|
233
|
+
end
|
234
|
+
|
235
|
+
def test_enumerate
|
236
|
+
enum = JsonPath.new("$..*")[@json_obj]
|
237
|
+
counter = 0
|
238
|
+
enum.each {|node| counter += 1}
|
239
|
+
assert_equal 114, counter
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_enumerate
|
243
|
+
root = JsonPath.new("$").on(@json_str).first
|
244
|
+
assert_equal ["http://acme.example.com"], JsonPath.new('tool_profile.base_url_choice..default_base_url').on(root)
|
245
|
+
end
|
246
|
+
|
247
|
+
def test_print
|
248
|
+
root = JsonPath.new("$").on(@json_str).first
|
249
|
+
# puts root.to_json
|
250
|
+
puts JSON.pretty_generate root
|
251
|
+
end
|
252
|
+
|
253
|
+
ARGV = ['', "--name", "test_print"]
|
254
|
+
Test::Unit::AutoRunner.run(false, nil, ARGV)
|
255
|
+
end
|