learnosity-sdk 0.2.2 → 0.4.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 +4 -4
- data/.travis.yml +1 -2
- data/CONTRIBUTING.md +13 -1
- data/ChangeLog.md +32 -0
- data/README.md +8 -1
- data/REFERENCE.md +65 -5
- data/docs/quickstart/lrn-sdk-rails/Gemfile +15 -14
- data/docs/quickstart/lrn-sdk-rails/app/controllers/author_controller.rb +2 -2
- data/docs/quickstart/lrn-sdk-rails/app/controllers/authoraide_controller.rb +32 -0
- data/docs/quickstart/lrn-sdk-rails/app/controllers/data_api_controller.rb +156 -0
- data/docs/quickstart/lrn-sdk-rails/app/controllers/index_controller.rb +1 -1
- data/docs/quickstart/lrn-sdk-rails/app/controllers/items_controller.rb +3 -3
- data/docs/quickstart/lrn-sdk-rails/app/controllers/questions_controller.rb +2 -2
- data/docs/quickstart/lrn-sdk-rails/app/controllers/reports_controller.rb +4 -4
- data/docs/quickstart/lrn-sdk-rails/app/views/authoraide/index.html.erb +6 -0
- data/docs/quickstart/lrn-sdk-rails/app/views/data_api/index.html.erb +277 -0
- data/docs/quickstart/lrn-sdk-rails/app/views/index/index.html.erb +8 -0
- data/docs/quickstart/lrn-sdk-rails/config/boot.rb +4 -0
- data/docs/quickstart/lrn-sdk-rails/config/initializers/new_framework_defaults.rb +2 -1
- data/docs/quickstart/lrn-sdk-rails/config/routes.rb +2 -0
- data/examples/simple/data_api_example.rb +82 -0
- data/examples/simple/uuid_example.rb +29 -0
- data/lib/learnosity/sdk/request/data_api.rb +196 -0
- data/lib/learnosity/sdk/request/init.rb +3 -3
- data/lib/learnosity/sdk/utils/uuid.rb +21 -0
- data/lib/learnosity/sdk/version.rb +1 -1
- data/lib/learnosity/sdk.rb +7 -1
- metadata +15 -7
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<style>
|
|
5
|
+
body {
|
|
6
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
7
|
+
max-width: 1200px;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
padding: 20px;
|
|
10
|
+
background: #f5f5f5;
|
|
11
|
+
}
|
|
12
|
+
h1 {
|
|
13
|
+
color: #333;
|
|
14
|
+
border-bottom: 3px solid #667eea;
|
|
15
|
+
padding-bottom: 10px;
|
|
16
|
+
}
|
|
17
|
+
.demo-section {
|
|
18
|
+
background: #f8f9fa;
|
|
19
|
+
border-left: 4px solid #667eea;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
margin: 20px 0;
|
|
22
|
+
border-radius: 4px;
|
|
23
|
+
}
|
|
24
|
+
.demo-section h2 {
|
|
25
|
+
margin-top: 0;
|
|
26
|
+
color: #333;
|
|
27
|
+
}
|
|
28
|
+
.item {
|
|
29
|
+
background: white;
|
|
30
|
+
padding: 15px;
|
|
31
|
+
margin: 10px 0;
|
|
32
|
+
border-radius: 4px;
|
|
33
|
+
border: 1px solid #dee2e6;
|
|
34
|
+
}
|
|
35
|
+
.item-reference {
|
|
36
|
+
font-family: monospace;
|
|
37
|
+
color: #667eea;
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
margin-bottom: 5px;
|
|
40
|
+
}
|
|
41
|
+
.item-status {
|
|
42
|
+
color: #666;
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
}
|
|
45
|
+
.meta-info {
|
|
46
|
+
background: #e7f3ff;
|
|
47
|
+
padding: 10px;
|
|
48
|
+
margin: 10px 0;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
border-left: 3px solid #0066cc;
|
|
51
|
+
}
|
|
52
|
+
.error {
|
|
53
|
+
background: #f8d7da;
|
|
54
|
+
color: #721c24;
|
|
55
|
+
padding: 15px;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
border-left: 4px solid #f5c6cb;
|
|
58
|
+
}
|
|
59
|
+
pre {
|
|
60
|
+
background: #2d2d2d;
|
|
61
|
+
color: #f8f8f2;
|
|
62
|
+
padding: 15px;
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
overflow-x: auto;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
}
|
|
67
|
+
code {
|
|
68
|
+
background: #f4f4f4;
|
|
69
|
+
padding: 2px 6px;
|
|
70
|
+
border-radius: 3px;
|
|
71
|
+
font-family: monospace;
|
|
72
|
+
}
|
|
73
|
+
.note-box {
|
|
74
|
+
background: #d1ecf1;
|
|
75
|
+
border: 1px solid #bee5eb;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
padding: 15px;
|
|
78
|
+
margin: 20px 0;
|
|
79
|
+
color: #0c5460;
|
|
80
|
+
}
|
|
81
|
+
.note-box strong {
|
|
82
|
+
display: block;
|
|
83
|
+
margin-bottom: 5px;
|
|
84
|
+
}
|
|
85
|
+
.api-responses {
|
|
86
|
+
margin: 20px 0;
|
|
87
|
+
}
|
|
88
|
+
.api-responses h2 {
|
|
89
|
+
color: #333;
|
|
90
|
+
margin-bottom: 15px;
|
|
91
|
+
}
|
|
92
|
+
.request-info {
|
|
93
|
+
background: #4a90e2;
|
|
94
|
+
color: white;
|
|
95
|
+
padding: 12px 20px;
|
|
96
|
+
border-radius: 4px 4px 0 0;
|
|
97
|
+
font-weight: bold;
|
|
98
|
+
margin-top: 20px;
|
|
99
|
+
}
|
|
100
|
+
.request-details {
|
|
101
|
+
background: white;
|
|
102
|
+
border: 1px solid #dee2e6;
|
|
103
|
+
border-top: none;
|
|
104
|
+
}
|
|
105
|
+
.request-row {
|
|
106
|
+
display: flex;
|
|
107
|
+
border-bottom: 1px solid #dee2e6;
|
|
108
|
+
}
|
|
109
|
+
.request-row:last-child {
|
|
110
|
+
border-bottom: none;
|
|
111
|
+
}
|
|
112
|
+
.request-label {
|
|
113
|
+
background: #f8f9fa;
|
|
114
|
+
padding: 12px 20px;
|
|
115
|
+
font-weight: bold;
|
|
116
|
+
width: 150px;
|
|
117
|
+
border-right: 1px solid #dee2e6;
|
|
118
|
+
}
|
|
119
|
+
.request-value {
|
|
120
|
+
padding: 12px 20px;
|
|
121
|
+
flex: 1;
|
|
122
|
+
font-family: monospace;
|
|
123
|
+
word-break: break-all;
|
|
124
|
+
}
|
|
125
|
+
.request-value.success {
|
|
126
|
+
color: #28a745;
|
|
127
|
+
font-weight: bold;
|
|
128
|
+
}
|
|
129
|
+
.request-value.url {
|
|
130
|
+
color: #e83e8c;
|
|
131
|
+
}
|
|
132
|
+
.request-value.action {
|
|
133
|
+
color: #e83e8c;
|
|
134
|
+
}
|
|
135
|
+
.metadata-headers {
|
|
136
|
+
background: #5cb85c;
|
|
137
|
+
color: white;
|
|
138
|
+
padding: 12px 20px;
|
|
139
|
+
border-radius: 4px 4px 0 0;
|
|
140
|
+
font-weight: bold;
|
|
141
|
+
margin-top: 20px;
|
|
142
|
+
}
|
|
143
|
+
.metadata-note {
|
|
144
|
+
background: white;
|
|
145
|
+
border: 1px solid #dee2e6;
|
|
146
|
+
border-top: none;
|
|
147
|
+
padding: 12px 20px;
|
|
148
|
+
color: #666;
|
|
149
|
+
font-size: 14px;
|
|
150
|
+
}
|
|
151
|
+
.header-row {
|
|
152
|
+
display: flex;
|
|
153
|
+
border-bottom: 1px solid #dee2e6;
|
|
154
|
+
background: white;
|
|
155
|
+
}
|
|
156
|
+
.header-row:last-child {
|
|
157
|
+
border-bottom: none;
|
|
158
|
+
}
|
|
159
|
+
.header-name {
|
|
160
|
+
background: #f8f9fa;
|
|
161
|
+
padding: 12px 20px;
|
|
162
|
+
font-weight: bold;
|
|
163
|
+
width: 250px;
|
|
164
|
+
border-right: 1px solid #dee2e6;
|
|
165
|
+
color: #e83e8c;
|
|
166
|
+
}
|
|
167
|
+
.header-value {
|
|
168
|
+
padding: 12px 20px;
|
|
169
|
+
flex: 1;
|
|
170
|
+
font-family: monospace;
|
|
171
|
+
word-break: break-all;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<h1>Data API Example - With Metadata Headers</h1>
|
|
177
|
+
|
|
178
|
+
<div class="note-box">
|
|
179
|
+
<strong>Note:</strong> This example demonstrates the Data API with automatic metadata headers. Every request includes consumer, action, and SDK language-version information.
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<% if @request_metadata %>
|
|
183
|
+
<div class="api-responses">
|
|
184
|
+
<h2>API Responses</h2>
|
|
185
|
+
|
|
186
|
+
<div class="request-info">Request Information</div>
|
|
187
|
+
<div class="request-details">
|
|
188
|
+
<div class="request-row">
|
|
189
|
+
<div class="request-label">Endpoint</div>
|
|
190
|
+
<div class="request-value url"><%= @request_metadata[:endpoint] %></div>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="request-row">
|
|
193
|
+
<div class="request-label">Action</div>
|
|
194
|
+
<div class="request-value action"><%= @request_metadata[:action] %></div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="request-row">
|
|
197
|
+
<div class="request-label">Status Code</div>
|
|
198
|
+
<div class="request-value success"><%= @request_metadata[:status_code] || 'N/A' %></div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="metadata-headers">Metadata Headers (Sent Automatically)</div>
|
|
203
|
+
<div class="metadata-note">
|
|
204
|
+
These headers are added automatically by the SDK and are invisible to customers:
|
|
205
|
+
</div>
|
|
206
|
+
<div class="request-details">
|
|
207
|
+
<div class="header-row">
|
|
208
|
+
<div class="header-name">X-Learnosity-Consumer</div>
|
|
209
|
+
<div class="header-value"><%= @request_metadata[:headers]['X-Learnosity-Consumer'] %></div>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="header-row">
|
|
212
|
+
<div class="header-name">X-Learnosity-Action</div>
|
|
213
|
+
<div class="header-value"><%= @request_metadata[:headers]['X-Learnosity-Action'] %></div>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="header-row">
|
|
216
|
+
<div class="header-name">X-Learnosity-SDK</div>
|
|
217
|
+
<div class="header-value"><%= @request_metadata[:headers]['X-Learnosity-SDK'] %></div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<% end %>
|
|
222
|
+
|
|
223
|
+
<div class="demo-section">
|
|
224
|
+
<h2>Demo 1: Manual Iteration (5 items)</h2>
|
|
225
|
+
<p>Using <code>request()</code> method with manual pagination via the 'next' pointer.</p>
|
|
226
|
+
<% if @demo1_error %>
|
|
227
|
+
<div class="error">Error: <%= @demo1_error %></div>
|
|
228
|
+
<% else %>
|
|
229
|
+
<% @demo1_output.each do |item| %>
|
|
230
|
+
<div class="item">
|
|
231
|
+
<div class="item-reference">Item <%= item[:number] %>: <%= item[:reference] %></div>
|
|
232
|
+
<div class="item-status">Status: <%= item[:status] %></div>
|
|
233
|
+
</div>
|
|
234
|
+
<% end %>
|
|
235
|
+
<% end %>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div class="demo-section">
|
|
239
|
+
<h2>Demo 2: Page Iteration (5 pages)</h2>
|
|
240
|
+
<p>Using <code>request_iter()</code> method to automatically iterate over pages.</p>
|
|
241
|
+
<% if @demo2_error %>
|
|
242
|
+
<div class="error">Error: <%= @demo2_error %></div>
|
|
243
|
+
<% else %>
|
|
244
|
+
<% @demo2_output.each do |page| %>
|
|
245
|
+
<div class="meta-info">
|
|
246
|
+
Page <%= page[:page_number] %>: <%= page[:item_count] %> items
|
|
247
|
+
</div>
|
|
248
|
+
<% page[:items].each do |item| %>
|
|
249
|
+
<div class="item">
|
|
250
|
+
<div class="item-reference"><%= item[:reference] %></div>
|
|
251
|
+
<div class="item-status">Status: <%= item[:status] %></div>
|
|
252
|
+
</div>
|
|
253
|
+
<% end %>
|
|
254
|
+
<% end %>
|
|
255
|
+
<% end %>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="demo-section">
|
|
259
|
+
<h2>Demo 3: Results Iteration (5 items)</h2>
|
|
260
|
+
<p>Using <code>results_iter()</code> method to automatically iterate over individual items.</p>
|
|
261
|
+
<% if @demo3_error %>
|
|
262
|
+
<div class="error">Error: <%= @demo3_error %></div>
|
|
263
|
+
<% else %>
|
|
264
|
+
<% @demo3_output.each do |item| %>
|
|
265
|
+
<div class="item">
|
|
266
|
+
<div class="item-reference">Item <%= item[:number] %>: <%= h item[:reference] %></div>
|
|
267
|
+
<div class="item-status">Status: <%= h item[:status] %></div>
|
|
268
|
+
<pre><%= h item[:json] %>...</pre>
|
|
269
|
+
</div>
|
|
270
|
+
<% end %>
|
|
271
|
+
<% end %>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<p><a href="/">Back to API Examples</a></p>
|
|
275
|
+
</body>
|
|
276
|
+
</html>
|
|
277
|
+
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
<td>Reports API</td>
|
|
30
30
|
<td><%=link_to("Here", reports_index_path, target: '_blank') %></td>
|
|
31
31
|
</tr>
|
|
32
|
+
<tr>
|
|
33
|
+
<td>Authoraide API</td>
|
|
34
|
+
<td><%=link_to("Here", authoraide_index_path, target: '_blank') %> </td>
|
|
35
|
+
</tr>
|
|
36
|
+
<tr>
|
|
37
|
+
<td>Data API</td>
|
|
38
|
+
<td><%=link_to("Here", data_api_index_path, target: '_blank') %> </td>
|
|
39
|
+
</tr>
|
|
32
40
|
</table>
|
|
33
41
|
</body>
|
|
34
42
|
</html>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
|
2
2
|
|
|
3
3
|
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
|
4
|
+
|
|
5
|
+
# Fix for Ruby 2.6 compatibility with Rails 6.1
|
|
6
|
+
# Rails 6.1 expects Logger to be available before ActiveSupport loads
|
|
7
|
+
require 'logger'
|
|
@@ -18,7 +18,8 @@ ActiveSupport.to_time_preserves_timezone = true
|
|
|
18
18
|
Rails.application.config.active_record.belongs_to_required_by_default = true
|
|
19
19
|
|
|
20
20
|
# Do not halt callback chains when a callback returns false. Previous versions had true.
|
|
21
|
-
|
|
21
|
+
# This option is not applicable for Rails 6.1 and has been removed.
|
|
22
|
+
# ActiveSupport.halt_callback_chains_on_return_false = false
|
|
22
23
|
|
|
23
24
|
# Configure SSL options to enable HSTS with subdomains. Previous versions had false.
|
|
24
25
|
Rails.application.config.ssl_options = { hsts: { subdomains: true } }
|
|
@@ -3,8 +3,10 @@ Rails.application.routes.draw do
|
|
|
3
3
|
get 'index/index'
|
|
4
4
|
get 'questions/index', as: 'questions_index'
|
|
5
5
|
get 'author/index' , as: 'author_index'
|
|
6
|
+
get 'authoraide/index' , as: 'authoraide_index'
|
|
6
7
|
get 'reports/index', as: 'reports_index'
|
|
7
8
|
get 'items/index', as: 'items_index'
|
|
9
|
+
get 'data_api/index', as: 'data_api_index'
|
|
8
10
|
# get 'abc' , to: "index#index"
|
|
9
11
|
|
|
10
12
|
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'learnosity/sdk/request/data_api'
|
|
3
|
+
|
|
4
|
+
# Configuration
|
|
5
|
+
# XXX: This is a Learnosity Demos consumer; replace it with your own consumer key
|
|
6
|
+
consumer_key = 'yis0TYCu7U9V4o7M'
|
|
7
|
+
# XXX: The consumer secret should be in a properly secured credential store, and *NEVER* checked into version control
|
|
8
|
+
consumer_secret = '74c5fd430cf1242a527f6223aebd42d30464be22'
|
|
9
|
+
domain = 'localhost'
|
|
10
|
+
|
|
11
|
+
# Initialize DataApi
|
|
12
|
+
data_api = Learnosity::Sdk::DataApi.new(
|
|
13
|
+
consumer_key: consumer_key,
|
|
14
|
+
consumer_secret: consumer_secret,
|
|
15
|
+
domain: domain
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Security packet
|
|
19
|
+
security_packet = {
|
|
20
|
+
'consumer_key' => consumer_key,
|
|
21
|
+
'domain' => domain
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Endpoint
|
|
25
|
+
endpoint = 'https://data.learnosity.com/v1/itembank/items'
|
|
26
|
+
|
|
27
|
+
puts "=== Example 1: Single Request ==="
|
|
28
|
+
puts
|
|
29
|
+
|
|
30
|
+
# Make a single request
|
|
31
|
+
response = data_api.request(
|
|
32
|
+
endpoint,
|
|
33
|
+
security_packet,
|
|
34
|
+
consumer_secret,
|
|
35
|
+
{ 'limit' => 5 },
|
|
36
|
+
'get'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
puts "Status: #{response.code}"
|
|
40
|
+
data = JSON.parse(response.body)
|
|
41
|
+
puts "Records: #{data['meta']['records']}"
|
|
42
|
+
puts "Items returned: #{data['data'].length}"
|
|
43
|
+
puts
|
|
44
|
+
|
|
45
|
+
puts "=== Example 2: Iterate Through Pages ==="
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
# Iterate through pages (up to 3 pages)
|
|
49
|
+
page_count = 0
|
|
50
|
+
data_api.request_iter(
|
|
51
|
+
endpoint,
|
|
52
|
+
security_packet,
|
|
53
|
+
consumer_secret,
|
|
54
|
+
{ 'limit' => 5 },
|
|
55
|
+
'get'
|
|
56
|
+
).each do |page|
|
|
57
|
+
page_count += 1
|
|
58
|
+
puts "Page #{page_count}: #{page['data'].length} items"
|
|
59
|
+
break if page_count >= 3
|
|
60
|
+
end
|
|
61
|
+
puts
|
|
62
|
+
|
|
63
|
+
puts "=== Example 3: Iterate Through Individual Results ==="
|
|
64
|
+
puts
|
|
65
|
+
|
|
66
|
+
# Iterate through individual results (up to 10 items)
|
|
67
|
+
item_count = 0
|
|
68
|
+
data_api.results_iter(
|
|
69
|
+
endpoint,
|
|
70
|
+
security_packet,
|
|
71
|
+
consumer_secret,
|
|
72
|
+
{ 'limit' => 5 },
|
|
73
|
+
'get'
|
|
74
|
+
).each do |item|
|
|
75
|
+
item_count += 1
|
|
76
|
+
puts "Item #{item_count}: #{item['reference'] || item['id'] || 'N/A'}"
|
|
77
|
+
break if item_count >= 10
|
|
78
|
+
end
|
|
79
|
+
puts
|
|
80
|
+
|
|
81
|
+
puts "Done!"
|
|
82
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
require 'learnosity/sdk'
|
|
4
|
+
|
|
5
|
+
# Example: Generate UUIDs using the Learnosity SDK utility
|
|
6
|
+
# This is commonly used for user_id and session_id in API requests
|
|
7
|
+
|
|
8
|
+
puts "Generating UUIDs using Learnosity::Sdk::Uuid.generate:"
|
|
9
|
+
puts
|
|
10
|
+
|
|
11
|
+
# Generate a few UUIDs
|
|
12
|
+
5.times do |i|
|
|
13
|
+
uuid = Learnosity::Sdk::Uuid.generate
|
|
14
|
+
puts "UUID #{i + 1}: #{uuid}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
puts
|
|
18
|
+
puts "Example usage in API requests:"
|
|
19
|
+
puts
|
|
20
|
+
|
|
21
|
+
# Example: Using UUIDs in an Items API request
|
|
22
|
+
user_id = Learnosity::Sdk::Uuid.generate
|
|
23
|
+
session_id = Learnosity::Sdk::Uuid.generate
|
|
24
|
+
|
|
25
|
+
puts "user_id: #{user_id}"
|
|
26
|
+
puts "session_id: #{session_id}"
|
|
27
|
+
|
|
28
|
+
# vim: sw=2
|
|
29
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'learnosity/sdk/request/init'
|
|
5
|
+
require 'learnosity/sdk/version'
|
|
6
|
+
|
|
7
|
+
module Learnosity
|
|
8
|
+
module Sdk
|
|
9
|
+
module Request
|
|
10
|
+
# DataApi - Routing layer for Learnosity Data API
|
|
11
|
+
#
|
|
12
|
+
# Provides methods to make HTTP requests to the Data API with automatic
|
|
13
|
+
# signing and pagination support.
|
|
14
|
+
class DataApi
|
|
15
|
+
attr_reader :consumer_key, :consumer_secret, :domain
|
|
16
|
+
|
|
17
|
+
# Initialize a new DataApi instance
|
|
18
|
+
#
|
|
19
|
+
# @param options [Hash] Configuration options
|
|
20
|
+
# @option options [String] :consumer_key Learnosity consumer key
|
|
21
|
+
# @option options [String] :consumer_secret Learnosity consumer secret
|
|
22
|
+
# @option options [String] :domain Domain for security packet
|
|
23
|
+
# @option options [Proc] :http_adapter Optional custom HTTP adapter
|
|
24
|
+
def initialize(options = {})
|
|
25
|
+
@consumer_key = options[:consumer_key]
|
|
26
|
+
@consumer_secret = options[:consumer_secret]
|
|
27
|
+
@domain = options[:domain]
|
|
28
|
+
@http_adapter = options[:http_adapter] || method(:default_http_adapter)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Make a single request to Data API
|
|
32
|
+
#
|
|
33
|
+
# @param endpoint [String] Full URL to the Data API endpoint
|
|
34
|
+
# @param security_packet [Hash] Security object with consumer_key and domain
|
|
35
|
+
# @param secret [String] Consumer secret
|
|
36
|
+
# @param request_packet [Hash] Request parameters (default: {})
|
|
37
|
+
# @param action [String] Action type: 'get', 'set', 'update', 'delete' (default: 'get')
|
|
38
|
+
# @return [Net::HTTPResponse] HTTP response object
|
|
39
|
+
def request(endpoint, security_packet, secret, request_packet = {}, action = 'get')
|
|
40
|
+
# Generate signed request using SDK
|
|
41
|
+
init = Init.new('data', security_packet, secret, request_packet, action)
|
|
42
|
+
signed_request = init.generate
|
|
43
|
+
|
|
44
|
+
# Extract metadata for routing
|
|
45
|
+
consumer = extract_consumer(security_packet)
|
|
46
|
+
derived_action = derive_action(endpoint, action)
|
|
47
|
+
|
|
48
|
+
# Prepare headers with routing metadata
|
|
49
|
+
headers = {
|
|
50
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
51
|
+
'X-Learnosity-Consumer' => consumer,
|
|
52
|
+
'X-Learnosity-Action' => derived_action,
|
|
53
|
+
'X-Learnosity-SDK' => "Ruby:#{Learnosity::Sdk::VERSION}"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Make HTTP request using adapter
|
|
57
|
+
@http_adapter.call(endpoint, signed_request, headers)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Iterate over pages of results from Data API
|
|
61
|
+
#
|
|
62
|
+
# @param endpoint [String] Full URL to the Data API endpoint
|
|
63
|
+
# @param security_packet [Hash] Security object
|
|
64
|
+
# @param secret [String] Consumer secret
|
|
65
|
+
# @param request_packet [Hash] Request parameters (default: {})
|
|
66
|
+
# @param action [String] Action type (default: 'get')
|
|
67
|
+
# @return [Enumerator] Enumerator yielding pages of results
|
|
68
|
+
def request_iter(endpoint, security_packet, secret, request_packet = {}, action = 'get')
|
|
69
|
+
Enumerator.new do |yielder|
|
|
70
|
+
# Deep copy to avoid mutation
|
|
71
|
+
security = deep_copy(security_packet)
|
|
72
|
+
request_params = deep_copy(request_packet)
|
|
73
|
+
data_end = false
|
|
74
|
+
|
|
75
|
+
until data_end
|
|
76
|
+
response = self.request(endpoint, security, secret, request_params, action)
|
|
77
|
+
validate_response(response)
|
|
78
|
+
|
|
79
|
+
data = parse_response_body(response)
|
|
80
|
+
validate_response_status(data)
|
|
81
|
+
|
|
82
|
+
data_end = !has_more_pages?(data)
|
|
83
|
+
request_params['next'] = data['meta']['next'] if data['meta'] && data['meta']['next']
|
|
84
|
+
|
|
85
|
+
yielder << data
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Iterate over individual results from Data API
|
|
91
|
+
#
|
|
92
|
+
# Automatically handles pagination and yields each individual result
|
|
93
|
+
# from the data array.
|
|
94
|
+
#
|
|
95
|
+
# @param endpoint [String] Full URL to the Data API endpoint
|
|
96
|
+
# @param security_packet [Hash] Security object
|
|
97
|
+
# @param secret [String] Consumer secret
|
|
98
|
+
# @param request_packet [Hash] Request parameters (default: {})
|
|
99
|
+
# @param action [String] Action type (default: 'get')
|
|
100
|
+
# @return [Enumerator] Enumerator yielding individual results
|
|
101
|
+
def results_iter(endpoint, security_packet, secret, request_packet = {}, action = 'get')
|
|
102
|
+
Enumerator.new do |yielder|
|
|
103
|
+
request_iter(endpoint, security_packet, secret, request_packet, action).each do |page|
|
|
104
|
+
if page['data'].is_a?(Hash)
|
|
105
|
+
# If data is a hash (not array), yield key-value pairs
|
|
106
|
+
page['data'].each do |key, value|
|
|
107
|
+
yielder << { key => value }
|
|
108
|
+
end
|
|
109
|
+
elsif page['data'].is_a?(Array)
|
|
110
|
+
# If data is an array, yield each item
|
|
111
|
+
page['data'].each do |result|
|
|
112
|
+
yielder << result
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Extract consumer key from security packet
|
|
120
|
+
def extract_consumer(security_packet)
|
|
121
|
+
security_packet['consumer_key'] || security_packet[:consumer_key] || ''
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Derive action metadata from endpoint and action
|
|
125
|
+
def derive_action(endpoint, action)
|
|
126
|
+
uri = URI.parse(endpoint)
|
|
127
|
+
path = uri.path.sub(/\/$/, '')
|
|
128
|
+
|
|
129
|
+
# Remove version prefix (e.g., /v1, /v2023.1.LTS, /latest)
|
|
130
|
+
path_parts = path.split('/')
|
|
131
|
+
|
|
132
|
+
if path_parts.length > 1
|
|
133
|
+
first_segment = path_parts[1].downcase
|
|
134
|
+
version_pattern = /^v[\d.]+(?:\.(lts|preview\d+))?$/
|
|
135
|
+
special_versions = ['latest', 'latest-lts', 'developer']
|
|
136
|
+
|
|
137
|
+
if version_pattern.match?(first_segment) || special_versions.include?(first_segment)
|
|
138
|
+
path = '/' + path_parts[2..-1].join('/')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
"#{action}_#{path}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Default HTTP adapter using Net::HTTP
|
|
148
|
+
def default_http_adapter(endpoint, signed_request, headers)
|
|
149
|
+
uri = URI.parse(endpoint)
|
|
150
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
151
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
152
|
+
http.open_timeout = 15 # seconds to establish connection
|
|
153
|
+
http.read_timeout = 60 # seconds to read response
|
|
154
|
+
|
|
155
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
156
|
+
request.set_form_data(signed_request)
|
|
157
|
+
|
|
158
|
+
http.request(request)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Deep copy a hash to avoid mutation
|
|
162
|
+
# Using JSON serialization instead of Marshal for security
|
|
163
|
+
def deep_copy(obj)
|
|
164
|
+
JSON.parse(JSON.generate(obj))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validate HTTP response
|
|
168
|
+
def validate_response(response)
|
|
169
|
+
return if response.is_a?(Net::HTTPSuccess)
|
|
170
|
+
raise "Server returned HTTP status #{response.code}: #{response.body}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Parse response body as JSON
|
|
174
|
+
def parse_response_body(response)
|
|
175
|
+
JSON.parse(response.body)
|
|
176
|
+
rescue JSON::ParserError
|
|
177
|
+
raise "Server returned invalid JSON: #{response.body}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Validate response has successful status
|
|
181
|
+
def validate_response_status(data)
|
|
182
|
+
return if data.dig('meta', 'status') == true
|
|
183
|
+
raise "Server returned unsuccessful status: #{data.to_json}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check if there are more pages to fetch
|
|
187
|
+
def has_more_pages?(data)
|
|
188
|
+
data['meta'] && data['meta']['next'] && data['data'] && !data['data'].empty?
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# vim: sw=2
|
|
196
|
+
|
|
@@ -19,7 +19,7 @@ module Learnosity
|
|
|
19
19
|
@@valid_security_keys = ['consumer_key', 'domain', 'timestamp', 'expires', 'user_id'];
|
|
20
20
|
|
|
21
21
|
# Service names that are valid for `$service`
|
|
22
|
-
@@valid_services = ['assess', 'author', 'data', 'events', 'items', 'questions', 'reports'];
|
|
22
|
+
@@valid_services = ['assess', 'author', 'data', 'events', 'items', 'questions', 'reports', 'authoraide'];
|
|
23
23
|
|
|
24
24
|
# Determines if telemetry is enabled
|
|
25
25
|
@@telemetry_enabled = true
|
|
@@ -77,7 +77,7 @@ module Learnosity
|
|
|
77
77
|
output = {}
|
|
78
78
|
|
|
79
79
|
case @service
|
|
80
|
-
when 'assess', 'author', 'data', 'items', 'reports'
|
|
80
|
+
when 'assess', 'author', 'data', 'items', 'reports', 'authoraide'
|
|
81
81
|
output['security'] = @security_packet
|
|
82
82
|
|
|
83
83
|
unless @request_packet.nil?
|
|
@@ -258,7 +258,7 @@ module Learnosity
|
|
|
258
258
|
hashed_users[k] = hash_value(k , @secret)
|
|
259
259
|
end
|
|
260
260
|
@request_packet['users'] = hashed_users
|
|
261
|
-
when 'author', 'data'
|
|
261
|
+
when 'author', 'data', 'authoraide'
|
|
262
262
|
@sign_request_data = true
|
|
263
263
|
else
|
|
264
264
|
raise Exception, "set_service_options() for #{@service} not implemented"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
module Learnosity
|
|
4
|
+
module Sdk
|
|
5
|
+
module Utils
|
|
6
|
+
# UUID utility for generating UUIDv4 identifiers
|
|
7
|
+
# Commonly used for user_id and session_id in Learnosity API requests
|
|
8
|
+
class Uuid
|
|
9
|
+
# Generate a UUIDv4 string
|
|
10
|
+
#
|
|
11
|
+
# @return [String] A UUIDv4 string
|
|
12
|
+
def self.generate
|
|
13
|
+
SecureRandom.uuid
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# vim: sw=2
|
|
21
|
+
|