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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/CONTRIBUTING.md +13 -1
  4. data/ChangeLog.md +32 -0
  5. data/README.md +8 -1
  6. data/REFERENCE.md +65 -5
  7. data/docs/quickstart/lrn-sdk-rails/Gemfile +15 -14
  8. data/docs/quickstart/lrn-sdk-rails/app/controllers/author_controller.rb +2 -2
  9. data/docs/quickstart/lrn-sdk-rails/app/controllers/authoraide_controller.rb +32 -0
  10. data/docs/quickstart/lrn-sdk-rails/app/controllers/data_api_controller.rb +156 -0
  11. data/docs/quickstart/lrn-sdk-rails/app/controllers/index_controller.rb +1 -1
  12. data/docs/quickstart/lrn-sdk-rails/app/controllers/items_controller.rb +3 -3
  13. data/docs/quickstart/lrn-sdk-rails/app/controllers/questions_controller.rb +2 -2
  14. data/docs/quickstart/lrn-sdk-rails/app/controllers/reports_controller.rb +4 -4
  15. data/docs/quickstart/lrn-sdk-rails/app/views/authoraide/index.html.erb +6 -0
  16. data/docs/quickstart/lrn-sdk-rails/app/views/data_api/index.html.erb +277 -0
  17. data/docs/quickstart/lrn-sdk-rails/app/views/index/index.html.erb +8 -0
  18. data/docs/quickstart/lrn-sdk-rails/config/boot.rb +4 -0
  19. data/docs/quickstart/lrn-sdk-rails/config/initializers/new_framework_defaults.rb +2 -1
  20. data/docs/quickstart/lrn-sdk-rails/config/routes.rb +2 -0
  21. data/examples/simple/data_api_example.rb +82 -0
  22. data/examples/simple/uuid_example.rb +29 -0
  23. data/lib/learnosity/sdk/request/data_api.rb +196 -0
  24. data/lib/learnosity/sdk/request/init.rb +3 -3
  25. data/lib/learnosity/sdk/utils/uuid.rb +21 -0
  26. data/lib/learnosity/sdk/version.rb +1 -1
  27. data/lib/learnosity/sdk.rb +7 -1
  28. 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
- ActiveSupport.halt_callback_chains_on_return_false = false
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
+
@@ -1,5 +1,5 @@
1
1
  module Learnosity
2
2
  module Sdk
3
- VERSION = "0.2.2"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end