lingara 0.2.0.pre.alpha.4 → 0.2.0.pre.alpha.6
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/README.md +90 -0
- data/lib/lingara/canvas.rb +101 -0
- data/lib/lingara/client.rb +8 -5
- data/lib/lingara/dialogue.rb +143 -0
- data/lib/lingara/models/activate_canvas_request.rb +313 -0
- data/lib/lingara/models/activate_dialogue_request.rb +237 -0
- data/lib/lingara/models/allowance_row.rb +1 -1
- data/lib/lingara/models/app_context_slice.rb +1 -1
- data/lib/lingara/models/app_disabled_data.rb +1 -1
- data/lib/lingara/models/app_enabled_data.rb +1 -1
- data/lib/lingara/models/app_installed_data.rb +1 -1
- data/lib/lingara/models/app_uninstalled_data.rb +1 -1
- data/lib/lingara/models/canvas.rb +339 -0
- data/lib/lingara/models/canvas_activation_outcome.rb +41 -0
- data/lib/lingara/models/canvas_active.rb +317 -0
- data/lib/lingara/models/canvas_definition.rb +323 -0
- data/lib/lingara/models/canvas_draft.rb +317 -0
- data/lib/lingara/models/canvas_dry_run.rb +265 -0
- data/lib/lingara/models/canvas_edge.rb +263 -0
- data/lib/lingara/models/canvas_made_object.rb +311 -0
- data/lib/lingara/models/canvas_node.rb +397 -0
- data/lib/lingara/models/canvas_node_kind.rb +43 -0
- data/lib/lingara/models/canvas_object_ref.rb +237 -0
- data/lib/lingara/models/canvas_page.rb +289 -0
- data/lib/lingara/models/canvas_param.rb +105 -0
- data/lib/lingara/models/canvas_plan.rb +248 -0
- data/lib/lingara/models/canvas_recipe.rb +43 -0
- data/lib/lingara/models/canvas_summary.rb +511 -0
- data/lib/lingara/models/canvas_verdict.rb +389 -0
- data/lib/lingara/models/canvas_verdict_kind.rb +42 -0
- data/lib/lingara/models/create_canvas_request.rb +279 -0
- data/lib/lingara/models/create_dialogue_request.rb +248 -0
- data/lib/lingara/models/create_lesson_plan_event_error.rb +1 -1
- data/lib/lingara/models/create_lesson_plan_event_phase.rb +1 -1
- data/lib/lingara/models/create_lesson_plan_event_result.rb +1 -1
- data/lib/lingara/models/create_lesson_plan_event_started.rb +1 -1
- data/lib/lingara/models/dialogue.rb +367 -0
- data/lib/lingara/models/dialogue_active.rb +290 -0
- data/lib/lingara/models/dialogue_draft.rb +290 -0
- data/lib/lingara/models/dialogue_entry.rb +1 -1
- data/lib/lingara/models/dialogue_npc.rb +326 -0
- data/lib/lingara/models/dialogue_page.rb +277 -0
- data/lib/lingara/models/dialogue_rule.rb +55 -0
- data/lib/lingara/models/dialogue_summary.rb +431 -0
- data/lib/lingara/models/dialogue_transition.rb +264 -0
- data/lib/lingara/models/dialogue_turn_request.rb +110 -6
- data/lib/lingara/models/dialogue_warning.rb +285 -0
- data/lib/lingara/models/dialogue_warning_code.rb +42 -0
- data/lib/lingara/models/embed_player_link.rb +1 -1
- data/lib/lingara/models/embed_token.rb +1 -1
- data/lib/lingara/models/embed_token_request.rb +1 -1
- data/lib/lingara/models/error_envelope.rb +2 -2
- data/lib/lingara/models/event_envelope.rb +1 -1
- data/lib/lingara/models/event_page.rb +1 -1
- data/lib/lingara/models/fact_value.rb +105 -0
- data/lib/lingara/models/generate_vocabulary_event_done.rb +1 -1
- data/lib/lingara/models/generate_vocabulary_event_error.rb +1 -1
- data/lib/lingara/models/generate_vocabulary_event_item.rb +1 -1
- data/lib/lingara/models/generate_vocabulary_event_started.rb +1 -1
- data/lib/lingara/models/graph_checkpoint.rb +322 -0
- data/lib/lingara/models/graph_edge.rb +329 -0
- data/lib/lingara/models/inbound_event_accepted.rb +1 -1
- data/lib/lingara/models/lesson_plan.rb +1 -1
- data/lib/lingara/models/lesson_plan_content.rb +1 -1
- data/lib/lingara/models/lesson_plan_create_request.rb +1 -1
- data/lib/lingara/models/lesson_plan_failed_data.rb +1 -1
- data/lib/lingara/models/lesson_plan_ready_data.rb +1 -1
- data/lib/lingara/models/link_status.rb +1 -1
- data/lib/lingara/models/notice.rb +1 -1
- data/lib/lingara/models/npc.rb +1 -1
- data/lib/lingara/models/npc_language.rb +345 -0
- data/lib/lingara/models/pedagogy.rb +41 -0
- data/lib/lingara/models/plan_fail_reason.rb +1 -1
- data/lib/lingara/models/plan_pending.rb +1 -1
- data/lib/lingara/models/plan_phase.rb +1 -1
- data/lib/lingara/models/plan_question.rb +1 -1
- data/lib/lingara/models/plan_ready_status.rb +1 -1
- data/lib/lingara/models/plan_result.rb +1 -1
- data/lib/lingara/models/plan_set.rb +1 -1
- data/lib/lingara/models/plan_started.rb +1 -1
- data/lib/lingara/models/plan_status.rb +1 -1
- data/lib/lingara/models/plan_word.rb +1 -1
- data/lib/lingara/models/practice_completed_data.rb +1 -1
- data/lib/lingara/models/practice_item.rb +1 -1
- data/lib/lingara/models/practice_outcome.rb +1 -1
- data/lib/lingara/models/reaction_report.rb +1 -1
- data/lib/lingara/models/reaction_status.rb +1 -1
- data/lib/lingara/models/rename_canvas_request.rb +237 -0
- data/lib/lingara/models/rename_dialogue_request.rb +237 -0
- data/lib/lingara/models/save_canvas_draft_request.rb +273 -0
- data/lib/lingara/models/save_dialogue_draft_request.rb +274 -0
- data/lib/lingara/models/send_dialogue_turn_event_delta.rb +1 -1
- data/lib/lingara/models/send_dialogue_turn_event_done.rb +1 -1
- data/lib/lingara/models/send_dialogue_turn_event_error.rb +1 -1
- data/lib/lingara/models/send_dialogue_turn_event_notice.rb +1 -1
- data/lib/lingara/models/send_dialogue_turn_event_transition.rb +287 -0
- data/lib/lingara/models/send_tutor_message_event_delta.rb +1 -1
- data/lib/lingara/models/send_tutor_message_event_done.rb +1 -1
- data/lib/lingara/models/send_tutor_message_event_error.rb +1 -1
- data/lib/lingara/models/send_tutor_message_event_notice.rb +1 -1
- data/lib/lingara/models/speaker.rb +1 -1
- data/lib/lingara/models/stream_error.rb +1 -1
- data/lib/lingara/models/stream_events_event_done.rb +1 -1
- data/lib/lingara/models/stream_events_event_error.rb +1 -1
- data/lib/lingara/models/stream_events_event_event.rb +1 -1
- data/lib/lingara/models/stream_lesson_plan_event_error.rb +1 -1
- data/lib/lingara/models/stream_lesson_plan_event_pending.rb +1 -1
- data/lib/lingara/models/stream_lesson_plan_event_phase.rb +1 -1
- data/lib/lingara/models/stream_lesson_plan_event_result.rb +1 -1
- data/lib/lingara/models/stream_lesson_plan_event_started.rb +1 -1
- data/lib/lingara/models/threshold_scope.rb +1 -1
- data/lib/lingara/models/turn_delta.rb +1 -1
- data/lib/lingara/models/turn_entry.rb +1 -1
- data/lib/lingara/models/turn_role.rb +1 -1
- data/lib/lingara/models/tutor_turn_request.rb +1 -1
- data/lib/lingara/models/usage.rb +1 -1
- data/lib/lingara/models/usage_ledger.rb +1 -1
- data/lib/lingara/models/usage_threshold_reached_data.rb +1 -1
- data/lib/lingara/models/version_detail.rb +1 -1
- data/lib/lingara/models/version_history_entry.rb +1 -1
- data/lib/lingara/models/version_list.rb +1 -1
- data/lib/lingara/models/version_spec.rb +1 -1
- data/lib/lingara/models/version_state.rb +1 -1
- data/lib/lingara/models/version_summary.rb +1 -1
- data/lib/lingara/models/vocab_example.rb +1 -1
- data/lib/lingara/models/vocab_item.rb +1 -1
- data/lib/lingara/models/vocab_meta.rb +1 -1
- data/lib/lingara/models/vocab_request.rb +1 -1
- data/lib/lingara/models/vocab_started.rb +1 -1
- data/lib/lingara/models/world_context_changed.rb +1 -1
- data/lib/lingara/models/world_practice_completed.rb +1 -1
- data/lib/lingara/models/world_practice_requested.rb +1 -1
- data/lib/lingara/operations.rb +127 -1
- data/lib/lingara/streams.rb +1 -0
- data/lib/lingara/version.rb +3 -3
- data/lib/lingara.rb +2 -0
- data/sig/lingara.rbs +50 -0
- metadata +46 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e943622393135dc22889e58f20576f09b77e025d2cc932d5bc7b29e474106e7
|
|
4
|
+
data.tar.gz: 716d6f0423e4b090b1501a1a8a822dc8a9e288a9e584649be1801d781d275dc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f29623a41324b9824a178f4aa563d7e38688a28a4754d492dc270ab6417c5d908c36c3e8c3f7a57f4c4e80ec176c1100a86466152438bb48aa53120973557c0
|
|
7
|
+
data.tar.gz: abca4a29e74e84236a5e933868df3dd47df14a8952572db0df11cd263cdb7e12e7a6acfc9f2f2570d8736a1347da2061de672b2b8284224364b2ac80abf4da9e
|
data/README.md
CHANGED
|
@@ -228,6 +228,96 @@ For higher-level helpers on your server, see the
|
|
|
228
228
|
[Lingara embed server kits](https://github.com/Spinning-Cat-Studios/lingara_embeddable_sdk)
|
|
229
229
|
(the `lingara-embed` gem).
|
|
230
230
|
|
|
231
|
+
## Canvases
|
|
232
|
+
|
|
233
|
+
The canvas operations (`canvas:read`, `canvas:write`) live on one group,
|
|
234
|
+
`client.canvases`, which uses the client's own token, retries, version pin
|
|
235
|
+
and errors. Each returns a `Lingara::Response`:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
canvases = client.canvases
|
|
239
|
+
page = canvases.list_canvases(status: "live", kinds: ["webhook"], limit: 20).value # a CanvasPage
|
|
240
|
+
canvas = canvases.create_canvas({name: "Hooks", recipe: "lesson_plan_ready", definition: definition}).value
|
|
241
|
+
canvas = canvases.get_canvas(canvas.id).value # draft and active inline
|
|
242
|
+
canvases.rename_canvas(canvas.id, {name: "Lesson plan webhook"})
|
|
243
|
+
canvases.save_canvas_draft(canvas.id, {definition: definition, draft_version: canvas.draft.draft_version})
|
|
244
|
+
canvases.activate_canvas(canvas.id, {draft_version: 4, outcome: "complete", objects: {"n1" => {id: object_id}}})
|
|
245
|
+
dry_run = canvases.dry_run_canvas_plan({ops: ops}).value # 200 whether or not dry_run.ok
|
|
246
|
+
canvases.page_canvases(kinds: ["webhook"]).each { |item| puts item.name }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`page_canvases(**filters)` follows `next_cursor` until a page says
|
|
250
|
+
`has_more: false`, resending the filters on every page; it never sleeps and
|
|
251
|
+
stops on the first error, and a page with `has_more` but no `next_cursor` is
|
|
252
|
+
`TransportError` `:malformed_response`. A node's `fields` and `params` are
|
|
253
|
+
open hashes, so a definition read and saved back keeps keys this library does
|
|
254
|
+
not know.
|
|
255
|
+
|
|
256
|
+
A stale `draft_version` is `409 canvas_conflict`, raised as
|
|
257
|
+
`Lingara::ApiError` and **never retried**. Re-read with `get_canvas`, compare
|
|
258
|
+
its draft with the one you sent (after a retried save it may be your own),
|
|
259
|
+
take its `draft_version`, and decide; the library never merges or re-sends.
|
|
260
|
+
A replayed `activate_canvas` at a `draft_version` already promoted answers
|
|
261
|
+
`409 canvas_conflict` too, and writes nothing. Branch on `code`, show the
|
|
262
|
+
message only as a fallback, and localise by code: the ten `canvas_*` codes are
|
|
263
|
+
the contract, the text is not. A dry-run's `refused` verdict carries its
|
|
264
|
+
`code` and an English `error` as data, never raised.
|
|
265
|
+
|
|
266
|
+
A canvas is deleted through the platform's lifecycle routes, not this library.
|
|
267
|
+
|
|
268
|
+
## Dialogues
|
|
269
|
+
|
|
270
|
+
The stored-dialogue operations (`dialogues:read`, `dialogues:write`) live on
|
|
271
|
+
one group, `client.dialogues`, which uses the client's own token, retries,
|
|
272
|
+
version pin and errors. Each returns a `Lingara::Response`:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
dialogues = client.dialogues
|
|
276
|
+
page = dialogues.list_dialogues(name: "market", limit: 20).value # a DialoguePage
|
|
277
|
+
dialogue = dialogues.create_dialogue({name: "Night market", definition: definition}).value
|
|
278
|
+
dialogue = dialogues.get_dialogue(dialogue.id).value # draft and active inline
|
|
279
|
+
dialogues.rename_dialogue(dialogue.id, {name: "Night market keepers"})
|
|
280
|
+
dialogues.save_dialogue_draft(dialogue.id, {definition: definition, draft_version: dialogue.draft.draft_version})
|
|
281
|
+
dialogues.activate_dialogue(dialogue.id, {draft_version: 4})
|
|
282
|
+
exported = dialogues.export_dialogue(dialogue.id, if_none_match: cached_etag).value
|
|
283
|
+
dialogues.page_dialogues(name: "market").each { |item| puts item.name }
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`page_dialogues(name:, limit:)` follows `next_cursor` until a page says
|
|
287
|
+
`has_more: false`, resending the filters on every page; it never sleeps and
|
|
288
|
+
stops on the first error, and a page with `has_more` but no `next_cursor` is
|
|
289
|
+
`TransportError` `:malformed_response`. A definition is an open hash, so one
|
|
290
|
+
read and saved back keeps keys this library does not know, `x-layout`
|
|
291
|
+
included. A `warnings` entry is advice, never a refusal; a warning `code`
|
|
292
|
+
this library's spec does not list fails to decode the `Dialogue`
|
|
293
|
+
(`TransportError` `:malformed_response`) until the library is upgraded.
|
|
294
|
+
|
|
295
|
+
A stale `draft_version` is `409 dialogue_conflict`, raised as
|
|
296
|
+
`Lingara::ApiError` and **never retried**. Re-read with `get_dialogue`, take
|
|
297
|
+
its `draft.draft_version` (or send `0` when `draft` is `nil`, which opens
|
|
298
|
+
one), and decide; after a retried save the change may be your own, and the
|
|
299
|
+
library never merges or re-sends. A replayed `activate_dialogue` answers
|
|
300
|
+
`409 dialogue_conflict` too, and writes nothing. Branch on `code`, show the
|
|
301
|
+
message only as a fallback, and localise by code: the `dialogue_*` codes are
|
|
302
|
+
the contract, the text is not. `400 dialogue_invalid` and
|
|
303
|
+
`400 dialogue_graph_invalid` carry `path` and `rule` on the wire, but this
|
|
304
|
+
release does not surface them on `ApiError`: a tool that needs them reads the
|
|
305
|
+
`400`'s body itself.
|
|
306
|
+
|
|
307
|
+
`export_dialogue(id, revision:, layout:, if_none_match:)` answers a
|
|
308
|
+
`Response` whose `value` is a `Lingara::ExportedDialogue::Body` (`revision`,
|
|
309
|
+
`etag`, `ir_json`) or, when `if_none_match:` still matches, a
|
|
310
|
+
`Lingara::ExportedDialogue::NotModified` (`revision`, `etag`): a `304` is an
|
|
311
|
+
answer, never an error. Cache by `etag`: `ir_json` is the `dialogue.v1`
|
|
312
|
+
document as canonical bytes, never parsed here, so store it exactly as
|
|
313
|
+
received and send the `etag` back unchanged. A `304` still spends one unit of
|
|
314
|
+
the rate budget. `revision:` (`"active"` or `"draft"`) and `layout:`
|
|
315
|
+
(`"keep"` or `"omit"`) are sent only when given; a missing `ETag` or
|
|
316
|
+
`Lingara-Dialogue-Revision` is `TransportError` `:malformed_response`.
|
|
317
|
+
|
|
318
|
+
A dialogue is deleted through the platform's lifecycle routes, not this
|
|
319
|
+
library.
|
|
320
|
+
|
|
231
321
|
## Errors
|
|
232
322
|
|
|
233
323
|
Every failure is a `Lingara::Error`, one of four:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lingara
|
|
4
|
+
# The canvas operations (ADR 16.10.26ak; scopes canvas:read and
|
|
5
|
+
# canvas:write), reached as client.canvases. The group holds no state of
|
|
6
|
+
# its own: every call goes through the client's request path, so the
|
|
7
|
+
# token, retries, version pin and errors are the client's. Each call
|
|
8
|
+
# returns a Response.
|
|
9
|
+
#
|
|
10
|
+
# A stale draft_version is 409 canvas_conflict, raised as an ApiError and
|
|
11
|
+
# never retried: re-read with get_canvas, compare its draft with yours
|
|
12
|
+
# (after a retried save it may be your own), and decide. A replayed
|
|
13
|
+
# activate_canvas answers 409 canvas_conflict too, and writes nothing.
|
|
14
|
+
# Branch on an error's code, never its localised message. A dry-run
|
|
15
|
+
# answers 200 whether or not ok is true; a refused verdict's code and
|
|
16
|
+
# English error are data. There is no delete: a canvas is deleted through
|
|
17
|
+
# the platform's lifecycle routes, not this library.
|
|
18
|
+
class Canvases
|
|
19
|
+
# Built by Client#canvases over the client's own request path.
|
|
20
|
+
def initialize(request)
|
|
21
|
+
@request = request
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# One page of canvases, a CanvasPage; page_canvases walks them. The
|
|
25
|
+
# lists are sent comma-separated, one value each.
|
|
26
|
+
def list_canvases(cursor: nil, limit: nil, name: nil, status: nil, recipes: nil, kinds: nil, scopes: nil)
|
|
27
|
+
lists = {recipes: recipes, kinds: kinds, scopes: scopes}.transform_values { |list| list unless list.nil? || list.empty? }
|
|
28
|
+
@request.call("listCanvases", [], {cursor: cursor, limit: limit, name: name, status: status, **lists}, nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Creates a canvas (201). The answer does not echo recipe.
|
|
32
|
+
def create_canvas(body)
|
|
33
|
+
@request.call("createCanvas", [], nil, body)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# One canvas, its draft and active revision inline.
|
|
37
|
+
def get_canvas(canvas_id)
|
|
38
|
+
@request.call("getCanvas", [canvas_id], nil, nil)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Renames a canvas (PATCH): body is {name:}.
|
|
42
|
+
def rename_canvas(canvas_id, body)
|
|
43
|
+
@request.call("renameCanvas", [canvas_id], nil, body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Saves the draft (PUT): body is {definition:, draft_version:, name:}.
|
|
47
|
+
# A stale draft_version is 409 canvas_conflict, which may follow your
|
|
48
|
+
# own retried save: re-read with get_canvas and compare first.
|
|
49
|
+
def save_canvas_draft(canvas_id, body)
|
|
50
|
+
@request.call("saveCanvasDraft", [canvas_id], nil, body)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Promotes a draft with the objects an apply made: body is
|
|
54
|
+
# {draft_version:, outcome:, objects:}.
|
|
55
|
+
def activate_canvas(canvas_id, body)
|
|
56
|
+
@request.call("activateCanvas", [canvas_id], nil, body)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Judges a plan ({ops:}) without applying it. A plan with a refused op
|
|
60
|
+
# still answers 200, ok false.
|
|
61
|
+
def dry_run_canvas_plan(body)
|
|
62
|
+
@request.call("dryRunCanvasPlan", [], nil, body)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Every CanvasSummary the filters match, following next_cursor until a
|
|
66
|
+
# page says has_more false, the filters resent on every page: an
|
|
67
|
+
# Enumerator that fetches lazily, or with a block, each item yielded. It
|
|
68
|
+
# never sleeps or polls and stops on the first error. A page with
|
|
69
|
+
# has_more and no next_cursor is TransportError :malformed_response,
|
|
70
|
+
# its items unyielded.
|
|
71
|
+
def page_canvases(**filters, &block)
|
|
72
|
+
items = Enumerator.new { |out| each_item(filters, out) }
|
|
73
|
+
block ? items.each(&block) : items
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def each_item(filters, out)
|
|
79
|
+
cursor = nil
|
|
80
|
+
loop do
|
|
81
|
+
page = list_canvases(**filters, cursor: cursor).value
|
|
82
|
+
if page.has_more && page.next_cursor.nil?
|
|
83
|
+
raise TransportError.new(:malformed_response, "a canvas page has more but no next_cursor")
|
|
84
|
+
end
|
|
85
|
+
page.items.each { |item| out << item }
|
|
86
|
+
break unless page.has_more
|
|
87
|
+
cursor = page.next_cursor
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Client
|
|
93
|
+
# The canvas operations (ADR 16.10.26ak), over this client's request
|
|
94
|
+
# path.
|
|
95
|
+
def canvases
|
|
96
|
+
@canvases ||= Canvases.new(lambda do |id, args, query, body|
|
|
97
|
+
json(id, args, query: query, body: payload(OPERATIONS.fetch(id), body))
|
|
98
|
+
end)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/lingara/client.rb
CHANGED
|
@@ -282,10 +282,12 @@ module Lingara
|
|
|
282
282
|
# the version observer, and returns what the block returns. A client with
|
|
283
283
|
# no token source sends an operation that needs one without
|
|
284
284
|
# Authorization; the server's 401 is the answer. +policy+ is the tail's
|
|
285
|
-
# single-attempt one for a tail open (K5a).
|
|
286
|
-
|
|
285
|
+
# single-attempt one for a tail open (K5a). +allow+ is one more status
|
|
286
|
+
# yielded as a success: the export's 304 (ADR 29.10.26c D6), and nil for
|
|
287
|
+
# every other call.
|
|
288
|
+
def pipeline(operation, url, body, on_start: nil, headers: {}, policy: @policy, allow: nil, &on_success)
|
|
287
289
|
send_all = lambda do |token|
|
|
288
|
-
policy.run { attempt(operation, url, body, [token, headers], on_start, &on_success) }
|
|
290
|
+
policy.run { attempt(operation, url, body, [token, headers], on_start, allow: allow, &on_success) }
|
|
289
291
|
end
|
|
290
292
|
result = if @token_source && operation[:needs_token]
|
|
291
293
|
RetryPolicy.with_token_retry(@token_source, &send_all)
|
|
@@ -297,12 +299,13 @@ module Lingara
|
|
|
297
299
|
end
|
|
298
300
|
|
|
299
301
|
# +auth+ is the token and the call's own headers.
|
|
300
|
-
def attempt(operation, url, body, auth, on_start)
|
|
302
|
+
def attempt(operation, url, body, auth, on_start, allow: nil)
|
|
301
303
|
token, extra = auth
|
|
302
304
|
observe = ->(response) { @versions.observe(response, url) }
|
|
303
305
|
result = @transport.request(operation[:method], url, headers(operation, body, token).merge(extra), body: body,
|
|
304
306
|
read_timeout: operation[:stream] && @idle, secrets: token ? [token.expose_secret] : [], on_start: on_start) do |response, phase|
|
|
305
|
-
|
|
307
|
+
status = response.code.to_i
|
|
308
|
+
next Attempt.new(response: response, body: response.body.to_s) unless (200..299).cover?(status) || status == allow
|
|
306
309
|
Attempt.new(value: yield(response, phase, observe), done: true)
|
|
307
310
|
end
|
|
308
311
|
# Phase#leave hands back the stream's value directly.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lingara
|
|
4
|
+
# What export_dialogue answers, as a Response's value (ADR 29.10.26c D6):
|
|
5
|
+
# a Body, the dialogue.v1 document as text never parsed here (its bytes
|
|
6
|
+
# are canonical and etag is their SHA-256, so store them as received), or
|
|
7
|
+
# on a 304 a NotModified: keep the copy you hold. Both carry the revision
|
|
8
|
+
# (Lingara-Dialogue-Revision) and the etag (ETag) exactly as sent.
|
|
9
|
+
module ExportedDialogue
|
|
10
|
+
Body = Data.define(:revision, :etag, :ir_json)
|
|
11
|
+
NotModified = Data.define(:revision, :etag)
|
|
12
|
+
|
|
13
|
+
# The arm a 200 or 304 answers. A missing ETag or
|
|
14
|
+
# Lingara-Dialogue-Revision, or a revision that is not a non-negative
|
|
15
|
+
# integer, is TransportError :malformed_response.
|
|
16
|
+
def self.from(response)
|
|
17
|
+
revision = response["Lingara-Dialogue-Revision"].to_s
|
|
18
|
+
etag = response["ETag"]
|
|
19
|
+
unless revision.match?(/\A\d+\z/) && etag
|
|
20
|
+
raise TransportError.new(:malformed_response, "an export needs ETag and an integer Lingara-Dialogue-Revision")
|
|
21
|
+
end
|
|
22
|
+
return NotModified.new(revision: revision.to_i, etag: etag) if response.code.to_i == 304
|
|
23
|
+
Body.new(revision: revision.to_i, etag: etag, ir_json: response.body.to_s.dup.force_encoding(Encoding::UTF_8))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The stored-dialogue operations (ADR 29.10.26c; scopes dialogues:read and
|
|
28
|
+
# dialogues:write), reached as client.dialogues. The group holds no state
|
|
29
|
+
# of its own: every call goes through the client's request path, so the
|
|
30
|
+
# token, retries, version pin and errors are the client's. Each call
|
|
31
|
+
# returns a Response.
|
|
32
|
+
#
|
|
33
|
+
# A stale draft_version is 409 dialogue_conflict, raised as an ApiError and
|
|
34
|
+
# never retried: re-read with get_dialogue, take draft.draft_version (or
|
|
35
|
+
# send 0 when draft is nil), and decide. A replayed activate_dialogue
|
|
36
|
+
# answers 409 dialogue_conflict too, and writes nothing. Branch on an
|
|
37
|
+
# error's code, never its localised message. There is no delete: a
|
|
38
|
+
# dialogue is deleted through the platform's lifecycle routes, not this
|
|
39
|
+
# library.
|
|
40
|
+
class Dialogues
|
|
41
|
+
# Built by Client#dialogues over the client's own request path, and its
|
|
42
|
+
# export path, which reads a 304 as an answer.
|
|
43
|
+
def initialize(request, export)
|
|
44
|
+
@request = request
|
|
45
|
+
@export = export
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# One page of dialogues, a DialoguePage; page_dialogues walks them.
|
|
49
|
+
def list_dialogues(cursor: nil, limit: nil, name: nil)
|
|
50
|
+
@request.call("listDialogues", [], {cursor: cursor, limit: limit, name: name}, nil)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Creates a dialogue (201): body is {name:, definition:}. Its warnings
|
|
54
|
+
# are advice, never a refusal.
|
|
55
|
+
def create_dialogue(body)
|
|
56
|
+
@request.call("createDialogue", [], nil, body)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# One dialogue, its draft and active revision inline.
|
|
60
|
+
def get_dialogue(dialogue_id)
|
|
61
|
+
@request.call("getDialogue", [dialogue_id], nil, nil)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Renames a dialogue (PATCH): body is {name:}.
|
|
65
|
+
def rename_dialogue(dialogue_id, body)
|
|
66
|
+
@request.call("renameDialogue", [dialogue_id], nil, body)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Saves the draft (PUT): body is {definition:, draft_version:, name:};
|
|
70
|
+
# draft_version 0 with no draft opens one. A stale draft_version is 409
|
|
71
|
+
# dialogue_conflict, which may follow your own retried save: re-read
|
|
72
|
+
# with get_dialogue and compare first.
|
|
73
|
+
def save_dialogue_draft(dialogue_id, body)
|
|
74
|
+
@request.call("saveDialogueDraft", [dialogue_id], nil, body)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Promotes the draft: body is {draft_version:}. A stale or missing draft
|
|
78
|
+
# is 409 dialogue_conflict and writes nothing.
|
|
79
|
+
def activate_dialogue(dialogue_id, body)
|
|
80
|
+
@request.call("activateDialogue", [dialogue_id], nil, body)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Exports a dialogue (dialogues:read): a Response whose value is an
|
|
84
|
+
# ExportedDialogue::Body or, when +if_none_match+ (the etag you hold,
|
|
85
|
+
# sent back unchanged) still matches, an ExportedDialogue::NotModified.
|
|
86
|
+
# +revision+ is "active" or "draft", +layout+ "keep" or "omit"; each is
|
|
87
|
+
# sent only when given. A 304 still spends one unit of the rate budget.
|
|
88
|
+
# K4's retries apply.
|
|
89
|
+
def export_dialogue(dialogue_id, revision: nil, layout: nil, if_none_match: nil)
|
|
90
|
+
headers = if_none_match.nil? ? {} : {"If-None-Match" => if_none_match}
|
|
91
|
+
@export.call([dialogue_id], {revision: revision, layout: layout}, headers)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Every DialogueSummary the filters (name:, limit:) match, following
|
|
95
|
+
# next_cursor until a page says has_more false, the filters resent on
|
|
96
|
+
# every page: an Enumerator that fetches lazily, or with a block, each
|
|
97
|
+
# item yielded. It never sleeps or polls and stops on the first error. A
|
|
98
|
+
# page with has_more and no next_cursor is TransportError
|
|
99
|
+
# :malformed_response, its items unyielded.
|
|
100
|
+
def page_dialogues(**filters, &block)
|
|
101
|
+
items = Enumerator.new { |out| each_item(filters, out) }
|
|
102
|
+
block ? items.each(&block) : items
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def each_item(filters, out)
|
|
108
|
+
cursor = nil
|
|
109
|
+
loop do
|
|
110
|
+
page = list_dialogues(**filters, cursor: cursor).value
|
|
111
|
+
if page.has_more && page.next_cursor.nil?
|
|
112
|
+
raise TransportError.new(:malformed_response, "a dialogue page has more but no next_cursor")
|
|
113
|
+
end
|
|
114
|
+
page.items.each { |item| out << item }
|
|
115
|
+
break unless page.has_more
|
|
116
|
+
cursor = page.next_cursor
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Client
|
|
122
|
+
# The stored-dialogue operations (ADR 29.10.26c), over this client's
|
|
123
|
+
# request path.
|
|
124
|
+
def dialogues
|
|
125
|
+
@dialogues ||= Dialogues.new(
|
|
126
|
+
lambda { |id, args, query, body| json(id, args, query: query, body: payload(OPERATIONS.fetch(id), body)) },
|
|
127
|
+
lambda { |args, query, headers| export_dialogue_response(args, query, headers) }
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# exportDialogue read by hand (D6): a 304 is an answer, the body is
|
|
134
|
+
# never decoded, and both headers are part of the result.
|
|
135
|
+
def export_dialogue_response(args, query, headers)
|
|
136
|
+
operation = OPERATIONS.fetch("exportDialogue")
|
|
137
|
+
pipeline(operation, url_for(operation, args, query), nil, headers: headers, allow: 304) do |response, _phase, observe|
|
|
138
|
+
served = observe.call(response)
|
|
139
|
+
Response.new(value: ExportedDialogue.from(response), served_version: served)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|