lingara 0.1.0.pre.alpha.13 → 0.2.0.pre.alpha.3

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +74 -2
  3. data/lib/lingara/client.rb +78 -10
  4. data/lib/lingara/events/catalogue.rb +20 -2
  5. data/lib/lingara/minted_token.rb +73 -0
  6. data/lib/lingara/models/allowance_row.rb +1 -1
  7. data/lib/lingara/models/app_context_slice.rb +1 -1
  8. data/lib/lingara/models/app_disabled_data.rb +263 -0
  9. data/lib/lingara/models/app_enabled_data.rb +263 -0
  10. data/lib/lingara/models/app_installed_data.rb +1 -1
  11. data/lib/lingara/models/app_uninstalled_data.rb +1 -1
  12. data/lib/lingara/models/create_lesson_plan_event_error.rb +1 -1
  13. data/lib/lingara/models/create_lesson_plan_event_phase.rb +1 -1
  14. data/lib/lingara/models/create_lesson_plan_event_result.rb +1 -1
  15. data/lib/lingara/models/create_lesson_plan_event_started.rb +1 -1
  16. data/lib/lingara/models/dialogue_entry.rb +303 -0
  17. data/lib/lingara/models/dialogue_turn_request.rb +442 -0
  18. data/lib/lingara/models/embed_player_link.rb +427 -0
  19. data/lib/lingara/models/embed_token.rb +379 -0
  20. data/lib/lingara/models/embed_token_request.rb +331 -0
  21. data/lib/lingara/models/error_envelope.rb +1 -1
  22. data/lib/lingara/models/event_envelope.rb +1 -1
  23. data/lib/lingara/models/event_page.rb +1 -1
  24. data/lib/lingara/models/generate_vocabulary_event_done.rb +1 -1
  25. data/lib/lingara/models/generate_vocabulary_event_error.rb +1 -1
  26. data/lib/lingara/models/generate_vocabulary_event_item.rb +1 -1
  27. data/lib/lingara/models/generate_vocabulary_event_started.rb +1 -1
  28. data/lib/lingara/models/inbound_event_accepted.rb +1 -1
  29. data/lib/lingara/models/lesson_plan.rb +1 -1
  30. data/lib/lingara/models/lesson_plan_content.rb +1 -1
  31. data/lib/lingara/models/lesson_plan_create_request.rb +1 -1
  32. data/lib/lingara/models/lesson_plan_failed_data.rb +1 -1
  33. data/lib/lingara/models/lesson_plan_ready_data.rb +1 -1
  34. data/lib/lingara/models/link_status.rb +41 -0
  35. data/lib/lingara/models/notice.rb +1 -1
  36. data/lib/lingara/models/npc.rb +1 -1
  37. data/lib/lingara/models/plan_fail_reason.rb +1 -1
  38. data/lib/lingara/models/plan_pending.rb +1 -1
  39. data/lib/lingara/models/plan_phase.rb +1 -1
  40. data/lib/lingara/models/plan_question.rb +1 -1
  41. data/lib/lingara/models/plan_ready_status.rb +1 -1
  42. data/lib/lingara/models/plan_result.rb +1 -1
  43. data/lib/lingara/models/plan_set.rb +1 -1
  44. data/lib/lingara/models/plan_started.rb +1 -1
  45. data/lib/lingara/models/plan_status.rb +1 -1
  46. data/lib/lingara/models/plan_word.rb +1 -1
  47. data/lib/lingara/models/practice_completed_data.rb +291 -0
  48. data/lib/lingara/models/practice_item.rb +347 -0
  49. data/lib/lingara/models/practice_outcome.rb +45 -0
  50. data/lib/lingara/models/reaction_report.rb +1 -1
  51. data/lib/lingara/models/reaction_status.rb +1 -1
  52. data/lib/lingara/models/send_dialogue_turn_event_delta.rb +287 -0
  53. data/lib/lingara/models/send_dialogue_turn_event_done.rb +287 -0
  54. data/lib/lingara/models/send_dialogue_turn_event_error.rb +287 -0
  55. data/lib/lingara/models/send_dialogue_turn_event_notice.rb +287 -0
  56. data/lib/lingara/models/send_tutor_message_event_delta.rb +1 -1
  57. data/lib/lingara/models/send_tutor_message_event_done.rb +1 -1
  58. data/lib/lingara/models/send_tutor_message_event_error.rb +1 -1
  59. data/lib/lingara/models/send_tutor_message_event_notice.rb +1 -1
  60. data/lib/lingara/models/speaker.rb +41 -0
  61. data/lib/lingara/models/stream_error.rb +1 -1
  62. data/lib/lingara/models/stream_events_event_done.rb +1 -1
  63. data/lib/lingara/models/stream_events_event_error.rb +1 -1
  64. data/lib/lingara/models/stream_events_event_event.rb +1 -1
  65. data/lib/lingara/models/stream_lesson_plan_event_error.rb +1 -1
  66. data/lib/lingara/models/stream_lesson_plan_event_pending.rb +1 -1
  67. data/lib/lingara/models/stream_lesson_plan_event_phase.rb +1 -1
  68. data/lib/lingara/models/stream_lesson_plan_event_result.rb +1 -1
  69. data/lib/lingara/models/stream_lesson_plan_event_started.rb +1 -1
  70. data/lib/lingara/models/threshold_scope.rb +1 -1
  71. data/lib/lingara/models/turn_delta.rb +1 -1
  72. data/lib/lingara/models/turn_entry.rb +1 -1
  73. data/lib/lingara/models/turn_role.rb +1 -1
  74. data/lib/lingara/models/tutor_turn_request.rb +1 -1
  75. data/lib/lingara/models/usage.rb +1 -1
  76. data/lib/lingara/models/usage_ledger.rb +1 -1
  77. data/lib/lingara/models/usage_threshold_reached_data.rb +1 -1
  78. data/lib/lingara/models/version_detail.rb +1 -1
  79. data/lib/lingara/models/version_history_entry.rb +1 -1
  80. data/lib/lingara/models/version_list.rb +1 -1
  81. data/lib/lingara/models/version_spec.rb +1 -1
  82. data/lib/lingara/models/version_state.rb +1 -1
  83. data/lib/lingara/models/version_summary.rb +1 -1
  84. data/lib/lingara/models/vocab_example.rb +1 -1
  85. data/lib/lingara/models/vocab_item.rb +1 -1
  86. data/lib/lingara/models/vocab_meta.rb +1 -1
  87. data/lib/lingara/models/vocab_request.rb +1 -1
  88. data/lib/lingara/models/vocab_started.rb +1 -1
  89. data/lib/lingara/models/world_context_changed.rb +1 -1
  90. data/lib/lingara/models/world_practice_completed.rb +294 -0
  91. data/lib/lingara/models/world_practice_requested.rb +1 -1
  92. data/lib/lingara/operations.rb +45 -0
  93. data/lib/lingara/streams.rb +25 -0
  94. data/lib/lingara/version.rb +3 -3
  95. data/lib/lingara.rb +1 -0
  96. data/sig/lingara.rbs +36 -0
  97. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c106647ff775f7a271c4ad22e324eda04a23f239088cd6e88b9ef039c467de3
4
- data.tar.gz: 9e8a5830acde513c26696500d902e182d14790b37ce7107853caa15e72c1cbe5
3
+ metadata.gz: 1cfd533625ec4e78a0e68f5327ce5f3af893e26f4e04ab4c25b1e8f2bbdf2e41
4
+ data.tar.gz: 435203b71ac2e9856463fead5cf5c91e852e2ba92b4d9bf1f9b3322609d05791
5
5
  SHA512:
6
- metadata.gz: c7e19be3b61b6cd9df78caf67b3591af23d404b22bd7f960679c4a4800e18d40303ef0b2860fc09d6824530e81f64287cf31c970b8381dd73a137631a0e7e4c8
7
- data.tar.gz: cf52f86493fbd3d8065b07057b60be6426f59118cb253cf0c37e3e1e8494d1ac992d4cb1f46615598958f55d5a6e4f643b0718d71a1aa8c667735f4f75c33737
6
+ metadata.gz: 3265f0f31df57f75f58f2c0b2465d40ff3770b54c979adaf2d64268a929db753d30125294a78df5f678d616c619e2b854052cb10f66dad0e7ef91b379c3dbd43
7
+ data.tar.gz: c9db5cd1433951e938fdcf2b6aa4bc6564e4abb2d6a386890def4acb96fc8a0396bc9144cccf363bf724f2914fcfc3b445003a86602ef21a7749eeb81b3efa6d
data/README.md CHANGED
@@ -61,7 +61,7 @@ calls them. A client without credentials sends the other nine without
61
61
 
62
62
  ## Streams
63
63
 
64
- The four streaming methods take a block, or return a stream:
64
+ The streaming methods take a block, or return a stream:
65
65
 
66
66
  - **With a block**, the call sends the request, yields each event and returns
67
67
  a `Lingara::StreamResult` (`#served_version`). `break`, `return` or `throw`
@@ -157,6 +157,77 @@ for another event returns the **first** answer. Only
157
157
  Event `data` is rendered at your client's pinned version, and this library's
158
158
  models are those of `Lingara::GENERATED_FOR_VERSION`: pin your client to it.
159
159
 
160
+ ## Embedding Lingara
161
+
162
+ A game or website that puts Lingara in front of its players vouches for each
163
+ player from its own server.
164
+
165
+ **Mint on your server, never on a player's device**, from a **metered**
166
+ client holding `embed:mint` (with an explicit `scopes:`, include it):
167
+
168
+ ```ruby
169
+ minted = client.create_embed_token(player_ref: "guild-42/player-1001", scopes: ["embed:play"]).value
170
+ minted.subject # "lgr_sub_…": store it beside the player
171
+ minted.token.expose_secret # "lgr_et_…": hand it, with minted.expires_at, to the player kit
172
+ ```
173
+
174
+ `player_ref` is your own reference for the player (at most 128 bytes). It is
175
+ sent as one path segment wherever it appears, so `/`, spaces and non-ASCII
176
+ text are safe. The result is a `Lingara::MintedToken`, and its `token` renders
177
+ `[REDACTED]` in `inspect`, `to_s` and `pp` like every other token here. Store
178
+ `subject`: it is how every event and webhook names that player. The token
179
+ lives 900 s and Lingara never refreshes it, so mint again when the player kit
180
+ asks. `expires_in` (whole seconds) is there for a device whose clock cannot be
181
+ trusted. An allowance client is refused with `403 embed_needs_metered`.
182
+
183
+ `client.delete_embed_player(player_ref)` deletes a player and revokes its
184
+ tokens. It returns a `Lingara::Response` whose `value` is `nil`. It is
185
+ idempotent, since an unknown player is a success too, and it keeps working
186
+ while embedding is switched off for your client.
187
+
188
+ `client.link_embed_player(player_ref)` links a player to the Lingara account
189
+ that granted your app access (`embed:play`); its `value` is an
190
+ `EmbedPlayerLink`. Call it on a client whose `token_source:` returns the
191
+ **player's delegated token** — the one your server received from that
192
+ account's own consent — never on your client-credentials client, which gets
193
+ `403 embed_link_needs_grant`. The library does not obtain that token yet; the
194
+ `link_embed_player` snippet's `delegated_client` builds the client in a few
195
+ lines. A fixed token is retried once on a `401`, so a revoked grant surfaces
196
+ as a `Lingara::ApiError` with status 401. `"existing"` is a success too, and a
197
+ repeat moves what could not move before: call again while
198
+ `kept["lesson_plans"]` is non-zero.
199
+
200
+ `client.unlink_embed_player(player_ref)` ends that link (`embed:mint`, your
201
+ ordinary client); its `value` is `nil`. A `404 embed_link_not_found` after a
202
+ retried call means it was already unlinked.
203
+
204
+ `client.send_dialogue_turn(npc:, source_lang:, target_lang:, level:, line:, history:)`
205
+ streams one NPC reply (`embed:play`, from an embed token or a metered
206
+ client's own token) as `delta` and `notice` events, ending on `done`. The
207
+ window is yours to keep, and the schema bounds it: at most 12 `history`
208
+ entries, with `line` and each entry at most 500 characters. Send an NPC reply
209
+ back into `history` cut to its first 500 characters. A turn is **never
210
+ retried**, because each attempt spends the player's NPC cells and the payer's:
211
+ a `429` or `503` raises `Lingara::ApiError` at once with its `retry_after`,
212
+ and you decide whether to send the turn again. No retry helps
213
+ `403 embed_needs_metered` or `422 safety_input_flagged` (say something else).
214
+
215
+ `practice.completed` arrives typed as `Lingara::Events::PracticeCompleted` on
216
+ the feed, the tail and webhooks, and your client needs `events:read` and
217
+ `embed:play` to hear it. A game reports practice with
218
+ `Lingara::Events::InboundEvent.world_practice_completed(…)`, which needs
219
+ `events:write` and `embed:play`.
220
+
221
+ A player-side caller uses an embed token through a custom `token_source:`
222
+ whose `#token` returns `Lingara::AccessToken.new(lgr_et_token)`: no client
223
+ secret is involved. The library never sends `X-Lingara-Embed-Origin`, so a
224
+ token minted with `origin:` belongs to the browser widget, and a game mints
225
+ without one.
226
+
227
+ For higher-level helpers on your server, see the
228
+ [Lingara embed server kits](https://github.com/Spinning-Cat-Studios/lingara_embeddable_sdk)
229
+ (the `lingara-embed` gem).
230
+
160
231
  ## Errors
161
232
 
162
233
  Every failure is a `Lingara::Error`, one of four:
@@ -169,7 +240,8 @@ Every failure is a `Lingara::Error`, one of four:
169
240
  | `Lingara::TransportError` | no usable answer | `kind`: `:connect`, `:tls`, `:reset`, `:timeout`, `:stream_ended_early`, `:malformed_response`, `:malformed_event` |
170
241
 
171
242
  A `429` or `503` with a `Retry-After` of at most `retry_after_cap:` (60 s) is
172
- retried, up to `max_attempts:` (3) tries; a `401` fetches a fresh token and
243
+ retried, up to `max_attempts:` (3) tries, except by `send_dialogue_turn`,
244
+ which sends once; a `401` fetches a fresh token and
173
245
  retries once. A stream that has yielded an event is never replayed. Your own
174
246
  interrupt (`Thread#raise`, `Thread#kill`, `Timeout.timeout`) is never wrapped
175
247
  and never retried.
@@ -49,7 +49,7 @@ module Lingara
49
49
  ))
50
50
  end
51
51
 
52
- # ── The thirteen operations, over operations.rb ───────────────────────
52
+ # ── The operations, over operations.rb ────────────────────────────────
53
53
 
54
54
  # Streams a vocabulary list (scope vocab:generate).
55
55
  def generate_vocabulary(**body, &block)
@@ -158,6 +158,57 @@ module Lingara
158
158
  json("sendEvent", [], body: JSON.generate(event.to_hash), headers: {"Idempotency-Key" => key})
159
159
  end
160
160
 
161
+ # ── Embedding (ADR 1.10.26w) ──────────────────────────────────────────
162
+
163
+ # Mints a player's embed token (scope embed:mint, a metered client
164
+ # only): player_ref:, and optionally scopes: (a list) and origin:. Call
165
+ # it on your server, never on a player's device. Returns a MintedToken,
166
+ # whose token renders [REDACTED]. The token lives 900 s and is never
167
+ # refreshed: mint again when the player kit asks.
168
+ def create_embed_token(**body)
169
+ operation = OPERATIONS.fetch("createEmbedToken")
170
+ json("createEmbedToken", [], body: payload(operation, body), decode: MintedToken.method(:decode))
171
+ end
172
+
173
+ # Deletes a player and revokes its tokens (scope embed:mint). An unknown
174
+ # player_ref is a success too, so a retry is safe, and it works while
175
+ # embedding is switched off. Returns a Response whose value is nil.
176
+ def delete_embed_player(player_ref)
177
+ no_content("deleteEmbedPlayer", [player_ref])
178
+ end
179
+
180
+ # Links a player to the Lingara account that granted your app access
181
+ # (scope embed:play; ADR 22.10.26t). Call it on a client whose
182
+ # token_source: returns the player's delegated token, from that account's
183
+ # own consent: a client-credentials client is refused 403
184
+ # embed_link_needs_grant. The library does not obtain that token yet. A
185
+ # repeat answers "existing" and moves what could not move before, so a
186
+ # retry is safe: call again while kept["lesson_plans"] is non-zero.
187
+ # Returns a Response whose value is an EmbedPlayerLink.
188
+ def link_embed_player(player_ref)
189
+ json("linkEmbedPlayer", [player_ref])
190
+ end
191
+
192
+ # Ends a player's link (scope embed:mint, on your ordinary client). No
193
+ # live link is 404 embed_link_not_found, which after a retried success
194
+ # means it was already unlinked. Returns a Response whose value is nil.
195
+ def unlink_embed_player(player_ref)
196
+ no_content("unlinkEmbedPlayer", [player_ref])
197
+ end
198
+
199
+ # Streams an NPC's reply to one line (scope embed:play, from an embed
200
+ # token or a metered client's own token): delta and notice events, ending
201
+ # on done. Each turn spends the player's NPC cells and the payer's, so it
202
+ # is sent once, never retried: a 429 or 503 raises ApiError at once,
203
+ # retry_after included, and the caller decides whether to send again.
204
+ # The window is the schema's: at most 12 history entries, line and each
205
+ # entry at most 500 characters; send an NPC reply back cut to its first
206
+ # 500. 403 embed_needs_metered and 422 safety_input_flagged (say
207
+ # something else) are refusals no retry helps.
208
+ def send_dialogue_turn(**body, &block)
209
+ stream("sendDialogueTurn", [], body, policy: @tail_policy, &block)
210
+ end
211
+
161
212
  def to_s
162
213
  inspect
163
214
  end
@@ -179,16 +230,34 @@ module Lingara
179
230
  raise ArgumentError, "stream_idle_timeout: must be a positive number" unless idle.is_a?(Numeric) && idle.positive?
180
231
  end
181
232
 
182
- # +body+ is JSON text already; +headers+ join every attempt's.
183
- def json(id, args, query: nil, body: nil, headers: {})
233
+ # +body+ is JSON text already; +headers+ join every attempt's. +decode+
234
+ # replaces the generated model's decoding (create_embed_token's).
235
+ def json(id, args, query: nil, body: nil, headers: {}, decode: nil)
184
236
  operation = OPERATIONS.fetch(id)
185
237
  url = url_for(operation, args, query)
238
+ decode ||= ->(text) { Decoding.response(operation[:response], text) }
186
239
  pipeline(operation, url, body, headers: headers) do |response, _phase, observe|
187
240
  served = observe.call(response)
188
- Response.new(value: Decoding.response(operation[:response], response.body.to_s), served_version: served)
241
+ Response.new(value: decode.call(response.body.to_s), served_version: served)
242
+ end
243
+ end
244
+
245
+ # An operation whose success has no body (D4): no request body and no
246
+ # Content-Type, and any 2xx body is discarded unread. Called by name: a
247
+ # route's `response: nil` already means "a plain Hash".
248
+ def no_content(id, args)
249
+ operation = OPERATIONS.fetch(id)
250
+ pipeline(operation, url_for(operation, args), nil) do |response, _phase, observe|
251
+ Response.new(value: nil, served_version: observe.call(response))
189
252
  end
190
253
  end
191
254
 
255
+ # The request model's constructor refuses a bad keyword before any
256
+ # request is sent: a programming error, like Client.new's.
257
+ def payload(operation, body)
258
+ body && JSON.generate(Lingara.const_get(operation[:request_body]).new(body).to_hash)
259
+ end
260
+
192
261
  # One page of the feed as its raw Hash, so each item reaches
193
262
  # Events.decode as the bytes the server sent (D6).
194
263
  def page(operation, url)
@@ -198,14 +267,13 @@ module Lingara
198
267
  end
199
268
  end
200
269
 
201
- def stream(id, args, body, query: nil, headers: {}, &block)
270
+ # +policy+ is the single-attempt one for a stream that must not be
271
+ # retried (send_dialogue_turn).
272
+ def stream(id, args, body, query: nil, headers: {}, policy: @policy, &block)
202
273
  operation = OPERATIONS.fetch(id)
203
274
  url = url_for(operation, args, query)
204
- # The request model's constructor refuses a bad keyword before any
205
- # request is sent: a programming error, like Client.new's.
206
- payload = body && JSON.generate(Lingara.const_get(operation[:request_body]).new(body).to_hash)
207
- send = ->(*call, **options, &consume) { pipeline(*call, headers: headers, **options, &consume) }
208
- events = EventStream.new(pipeline: send, operation: operation, url: url, body: payload)
275
+ send = ->(*call, **options, &consume) { pipeline(*call, headers: headers, policy: policy, **options, &consume) }
276
+ events = EventStream.new(pipeline: send, operation: operation, url: url, body: payload(operation, body))
209
277
  block ? events.run(&block) : events
210
278
  end
211
279
 
@@ -30,6 +30,15 @@ module Lingara
30
30
  # app.uninstalled: data is a Lingara::AppUninstalledData.
31
31
  AppUninstalled = Data.define(*ENVELOPE) { include Event }
32
32
 
33
+ # app.disabled: data is a Lingara::AppDisabledData.
34
+ AppDisabled = Data.define(*ENVELOPE) { include Event }
35
+
36
+ # app.enabled: data is a Lingara::AppEnabledData.
37
+ AppEnabled = Data.define(*ENVELOPE) { include Event }
38
+
39
+ # practice.completed: data is a Lingara::PracticeCompletedData.
40
+ PracticeCompleted = Data.define(*ENVELOPE) { include Event }
41
+
33
42
  # An event of a type this library does not know: data is the raw JSON
34
43
  # object, as a Hash. The catalogue only grows, so acknowledge it.
35
44
  UnknownEvent = Data.define(*ENVELOPE) { include Event }
@@ -42,7 +51,10 @@ module Lingara
42
51
  "usage.threshold_reached" => [UsageThresholdReached, "UsageThresholdReachedData"].freeze,
43
52
  "webhook.test" => [WebhookTest, nil].freeze,
44
53
  "app.installed" => [AppInstalled, "AppInstalledData"].freeze,
45
- "app.uninstalled" => [AppUninstalled, "AppUninstalledData"].freeze
54
+ "app.uninstalled" => [AppUninstalled, "AppUninstalledData"].freeze,
55
+ "app.disabled" => [AppDisabled, "AppDisabledData"].freeze,
56
+ "app.enabled" => [AppEnabled, "AppEnabledData"].freeze,
57
+ "practice.completed" => [PracticeCompleted, "PracticeCompletedData"].freeze
46
58
  }.freeze
47
59
  end
48
60
 
@@ -53,7 +65,8 @@ module Lingara
53
65
  # Each inbound type's constructor.
54
66
  CONSTRUCTORS = {
55
67
  "world.context_changed" => :world_context_changed,
56
- "world.practice_requested" => :world_practice_requested
68
+ "world.practice_requested" => :world_practice_requested,
69
+ "world.practice_completed" => :world_practice_completed
57
70
  }.freeze
58
71
 
59
72
  attr_reader :type, :data
@@ -70,6 +83,11 @@ module Lingara
70
83
  new("world.practice_requested", data.is_a?(Lingara::WorldPracticeRequested) ? data : Lingara::WorldPracticeRequested.new(data))
71
84
  end
72
85
 
86
+ # world.practice_completed, from a Lingara::WorldPracticeCompleted or the Hash one is built from.
87
+ def self.world_practice_completed(data)
88
+ new("world.practice_completed", data.is_a?(Lingara::WorldPracticeCompleted) ? data : Lingara::WorldPracticeCompleted.new(data))
89
+ end
90
+
73
91
  def initialize(type, data)
74
92
  @type = type
75
93
  @data = data
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Lingara
6
+ # What create_embed_token returns (ADR 1.10.26w D3): a player's embed
7
+ # token and the five fields minted beside it. #token is an AccessToken, so
8
+ # the lgr_et_ value renders [REDACTED] in #inspect, #to_s and pp like
9
+ # every other token (K1); token.expose_secret reads it.
10
+ #
11
+ # Lives at lingara/minted_token, never lingara/embed: that require path is
12
+ # the lingara-embed gem's, and two gems shipping one path shadow each other
13
+ # on $LOAD_PATH.
14
+ class MintedToken
15
+ include Redacted
16
+
17
+ PREFIX = "lgr_et_"
18
+ # Each answer field and the JSON type it must have.
19
+ FIELDS = {
20
+ "token" => String, "expires_at" => String, "expires_in" => Integer, "subject" => String, "scopes" => Array,
21
+ "account_linked" => [true, false]
22
+ }.freeze
23
+
24
+ # +token+ an AccessToken; +expires_at+ a Time, as the generated
25
+ # EmbedToken reads it; +expires_in+ whole seconds, for a device whose
26
+ # clock cannot be trusted; +subject+ the player's lgr_sub_, stable across
27
+ # mints; +scopes+ as granted; +account_linked+ always false until a
28
+ # player links an account.
29
+ attr_reader :token, :expires_at, :expires_in, :subject, :scopes, :account_linked
30
+
31
+ # A mint's answer body to a MintedToken. Every field must be present and
32
+ # of its JSON type, and the token must start lgr_et_; anything else is
33
+ # TransportError(:malformed_response), raised with no cause so nothing
34
+ # of the body, the token included, travels with it.
35
+ def self.decode(body)
36
+ fields = JSON.parse(body)
37
+ raise ArgumentError unless well_typed?(fields)
38
+ model = EmbedToken.build_from_hash(fields)
39
+ new(token: AccessToken.new(model.token), expires_at: model.expires_at, expires_in: model.expires_in,
40
+ subject: model.subject, scopes: model.scopes, account_linked: model.account_linked)
41
+ rescue
42
+ raise TransportError.new(:malformed_response, "the answer is not a minted token"), cause: nil
43
+ end
44
+
45
+ def self.well_typed?(fields)
46
+ return false unless fields.is_a?(Hash)
47
+ typed = FIELDS.all? do |name, type|
48
+ type.is_a?(Array) ? type.include?(fields[name]) : fields[name].is_a?(type)
49
+ end
50
+ typed && fields["token"].start_with?(PREFIX) && fields["scopes"].all?(String)
51
+ end
52
+ private_class_method :well_typed?
53
+
54
+ def initialize(token:, expires_at:, expires_in:, subject:, scopes:, account_linked:)
55
+ @token = token
56
+ @expires_at = expires_at
57
+ @expires_in = expires_in
58
+ @subject = subject.dup.freeze
59
+ @scopes = scopes.map { |scope| scope.dup.freeze }.freeze
60
+ @account_linked = account_linked
61
+ freeze
62
+ end
63
+
64
+ def to_s
65
+ inspect
66
+ end
67
+
68
+ def inspect
69
+ "#<Lingara::MintedToken token=#{REDACTED} expires_at=#{@expires_at.inspect} expires_in=#{@expires_in} " \
70
+ "subject=#{@subject.inspect} scopes=#{@scopes.inspect} account_linked=#{@account_linked}>"
71
+ end
72
+ end
73
+ end
@@ -3,7 +3,7 @@
3
3
 
4
4
  #Generate vocabulary lists and lesson plans, and hold tutor conversations, in the languages Lingara teaches. Each call is paid for by the client that makes it: an `allowance` client spends its owner's allowance, and a `metered` client is billed for its usage.
5
5
 
6
- The version of the OpenAPI document: 2026-10-affable-towhee
6
+ The version of the OpenAPI document: 2026-10-virile-dragon
7
7
 
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.14.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #Generate vocabulary lists and lesson plans, and hold tutor conversations, in the languages Lingara teaches. Each call is paid for by the client that makes it: an `allowance` client spends its owner's allowance, and a `metered` client is billed for its usage.
5
5
 
6
- The version of the OpenAPI document: 2026-10-affable-towhee
6
+ The version of the OpenAPI document: 2026-10-virile-dragon
7
7
 
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.14.0
@@ -0,0 +1,263 @@
1
+ =begin
2
+ #Lingara API
3
+
4
+ #Generate vocabulary lists and lesson plans, and hold tutor conversations, in the languages Lingara teaches. Each call is paid for by the client that makes it: an `allowance` client spends its owner's allowance, and a `metered` client is billed for its usage.
5
+
6
+ The version of the OpenAPI document: 2026-10-virile-dragon
7
+
8
+ Generated by: https://openapi-generator.tech
9
+ Generator version: 7.14.0
10
+
11
+ =end
12
+
13
+ require 'date'
14
+ require 'time'
15
+
16
+ module Lingara
17
+ class AppDisabledData
18
+ attr_accessor :client_id
19
+
20
+ attr_accessor :install_id
21
+
22
+ # Attribute mapping from ruby-style variable name to JSON key.
23
+ def self.attribute_map
24
+ {
25
+ :'client_id' => :'client_id',
26
+ :'install_id' => :'install_id'
27
+ }
28
+ end
29
+
30
+ # Returns attribute mapping this model knows about
31
+ def self.acceptable_attribute_map
32
+ attribute_map
33
+ end
34
+
35
+ # Returns all the JSON keys this model knows about
36
+ def self.acceptable_attributes
37
+ acceptable_attribute_map.values
38
+ end
39
+
40
+ # Attribute type mapping.
41
+ def self.openapi_types
42
+ {
43
+ :'client_id' => :'String',
44
+ :'install_id' => :'String'
45
+ }
46
+ end
47
+
48
+ # List of attributes with nullable: true
49
+ def self.openapi_nullable
50
+ Set.new([
51
+ ])
52
+ end
53
+
54
+ # Initializes the object
55
+ # @param [Hash] attributes Model attributes in the form of hash
56
+ def initialize(attributes = {})
57
+ if (!attributes.is_a?(Hash))
58
+ fail ArgumentError, "The input argument (attributes) must be a hash in `Lingara::AppDisabledData` initialize method"
59
+ end
60
+
61
+ # check to see if the attribute exists and convert string to symbol for hash key
62
+ acceptable_attribute_map = self.class.acceptable_attribute_map
63
+ attributes = attributes.each_with_object({}) { |(k, v), h|
64
+ if (!acceptable_attribute_map.key?(k.to_sym))
65
+ fail ArgumentError, "`#{k}` is not a valid attribute in `Lingara::AppDisabledData`. Please check the name to make sure it's valid. List of attributes: " + acceptable_attribute_map.keys.inspect
66
+ end
67
+ h[k.to_sym] = v
68
+ }
69
+
70
+ if attributes.key?(:'client_id')
71
+ self.client_id = attributes[:'client_id']
72
+ else
73
+ self.client_id = nil
74
+ end
75
+
76
+ if attributes.key?(:'install_id')
77
+ self.install_id = attributes[:'install_id']
78
+ else
79
+ self.install_id = nil
80
+ end
81
+ end
82
+
83
+ # Show invalid properties with the reasons. Usually used together with valid?
84
+ # @return Array for valid properties with the reasons
85
+ def list_invalid_properties
86
+ warn '[DEPRECATED] the `list_invalid_properties` method is obsolete'
87
+ invalid_properties = Array.new
88
+ if @client_id.nil?
89
+ invalid_properties.push('invalid value for "client_id", client_id cannot be nil.')
90
+ end
91
+
92
+ if @install_id.nil?
93
+ invalid_properties.push('invalid value for "install_id", install_id cannot be nil.')
94
+ end
95
+
96
+ invalid_properties
97
+ end
98
+
99
+ # Check to see if the all the properties in the model are valid
100
+ # @return true if the model is valid
101
+ def valid?
102
+ warn '[DEPRECATED] the `valid?` method is obsolete'
103
+ return false if @client_id.nil?
104
+ return false if @install_id.nil?
105
+ true
106
+ end
107
+
108
+ # Custom attribute writer method with validation
109
+ # @param [Object] client_id Value to be assigned
110
+ def client_id=(client_id)
111
+ if client_id.nil?
112
+ fail ArgumentError, 'client_id cannot be nil'
113
+ end
114
+
115
+ @client_id = client_id
116
+ end
117
+
118
+ # Custom attribute writer method with validation
119
+ # @param [Object] install_id Value to be assigned
120
+ def install_id=(install_id)
121
+ if install_id.nil?
122
+ fail ArgumentError, 'install_id cannot be nil'
123
+ end
124
+
125
+ @install_id = install_id
126
+ end
127
+
128
+ # Checks equality by comparing each attribute.
129
+ # @param [Object] Object to be compared
130
+ def ==(o)
131
+ return true if self.equal?(o)
132
+ self.class == o.class &&
133
+ client_id == o.client_id &&
134
+ install_id == o.install_id
135
+ end
136
+
137
+ # @see the `==` method
138
+ # @param [Object] Object to be compared
139
+ def eql?(o)
140
+ self == o
141
+ end
142
+
143
+ # Calculates hash code according to all attributes.
144
+ # @return [Integer] Hash code
145
+ def hash
146
+ [client_id, install_id].hash
147
+ end
148
+
149
+ # Builds the object from hash
150
+ # @param [Hash] attributes Model attributes in the form of hash
151
+ # @return [Object] Returns the model itself
152
+ def self.build_from_hash(attributes)
153
+ return nil unless attributes.is_a?(Hash)
154
+ attributes = attributes.transform_keys(&:to_sym)
155
+ transformed_hash = {}
156
+ openapi_types.each_pair do |key, type|
157
+ if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil?
158
+ transformed_hash["#{key}"] = nil
159
+ elsif type =~ /\AArray<(.*)>/i
160
+ # check to ensure the input is an array given that the attribute
161
+ # is documented as an array but the input is not
162
+ if attributes[attribute_map[key]].is_a?(Array)
163
+ transformed_hash["#{key}"] = attributes[attribute_map[key]].map { |v| _deserialize($1, v) }
164
+ end
165
+ elsif !attributes[attribute_map[key]].nil?
166
+ transformed_hash["#{key}"] = _deserialize(type, attributes[attribute_map[key]])
167
+ end
168
+ end
169
+ new(transformed_hash)
170
+ end
171
+
172
+ # Deserializes the data based on type
173
+ # @param string type Data type
174
+ # @param string value Value to be deserialized
175
+ # @return [Object] Deserialized data
176
+ def self._deserialize(type, value)
177
+ case type.to_sym
178
+ when :Time
179
+ Time.parse(value)
180
+ when :Date
181
+ Date.parse(value)
182
+ when :String
183
+ value.to_s
184
+ when :Integer
185
+ value.to_i
186
+ when :Float
187
+ value.to_f
188
+ when :Boolean
189
+ if value.to_s =~ /\A(true|t|yes|y|1)\z/i
190
+ true
191
+ else
192
+ false
193
+ end
194
+ when :Object
195
+ # generic object (usually a Hash), return directly
196
+ value
197
+ when /\AArray<(?<inner_type>.+)>\z/
198
+ inner_type = Regexp.last_match[:inner_type]
199
+ value.map { |v| _deserialize(inner_type, v) }
200
+ when /\AHash<(?<k_type>.+?), (?<v_type>.+)>\z/
201
+ k_type = Regexp.last_match[:k_type]
202
+ v_type = Regexp.last_match[:v_type]
203
+ {}.tap do |hash|
204
+ value.each do |k, v|
205
+ hash[_deserialize(k_type, k)] = _deserialize(v_type, v)
206
+ end
207
+ end
208
+ else # model
209
+ # models (e.g. Pet) or oneOf
210
+ klass = Lingara.const_get(type)
211
+ klass.respond_to?(:openapi_any_of) || klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
212
+ end
213
+ end
214
+
215
+ # Returns the string representation of the object
216
+ # @return [String] String presentation of the object
217
+ def to_s
218
+ to_hash.to_s
219
+ end
220
+
221
+ # to_body is an alias to to_hash (backward compatibility)
222
+ # @return [Hash] Returns the object in the form of hash
223
+ def to_body
224
+ to_hash
225
+ end
226
+
227
+ # Returns the object in the form of hash
228
+ # @return [Hash] Returns the object in the form of hash
229
+ def to_hash
230
+ hash = {}
231
+ self.class.attribute_map.each_pair do |attr, param|
232
+ value = self.send(attr)
233
+ if value.nil?
234
+ is_nullable = self.class.openapi_nullable.include?(attr)
235
+ next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}"))
236
+ end
237
+
238
+ hash[param] = _to_hash(value)
239
+ end
240
+ hash
241
+ end
242
+
243
+ # Outputs non-array value in the form of hash
244
+ # For object, use to_hash. Otherwise, just return the value
245
+ # @param [Object] value Any valid value
246
+ # @return [Hash] Returns the value in the form of hash
247
+ def _to_hash(value)
248
+ if value.is_a?(Array)
249
+ value.compact.map { |v| _to_hash(v) }
250
+ elsif value.is_a?(Hash)
251
+ {}.tap do |hash|
252
+ value.each { |k, v| hash[k] = _to_hash(v) }
253
+ end
254
+ elsif value.respond_to? :to_hash
255
+ value.to_hash
256
+ else
257
+ value
258
+ end
259
+ end
260
+
261
+ end
262
+
263
+ end