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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -0
  3. data/lib/lingara/canvas.rb +101 -0
  4. data/lib/lingara/client.rb +8 -5
  5. data/lib/lingara/dialogue.rb +143 -0
  6. data/lib/lingara/models/activate_canvas_request.rb +313 -0
  7. data/lib/lingara/models/activate_dialogue_request.rb +237 -0
  8. data/lib/lingara/models/allowance_row.rb +1 -1
  9. data/lib/lingara/models/app_context_slice.rb +1 -1
  10. data/lib/lingara/models/app_disabled_data.rb +1 -1
  11. data/lib/lingara/models/app_enabled_data.rb +1 -1
  12. data/lib/lingara/models/app_installed_data.rb +1 -1
  13. data/lib/lingara/models/app_uninstalled_data.rb +1 -1
  14. data/lib/lingara/models/canvas.rb +339 -0
  15. data/lib/lingara/models/canvas_activation_outcome.rb +41 -0
  16. data/lib/lingara/models/canvas_active.rb +317 -0
  17. data/lib/lingara/models/canvas_definition.rb +323 -0
  18. data/lib/lingara/models/canvas_draft.rb +317 -0
  19. data/lib/lingara/models/canvas_dry_run.rb +265 -0
  20. data/lib/lingara/models/canvas_edge.rb +263 -0
  21. data/lib/lingara/models/canvas_made_object.rb +311 -0
  22. data/lib/lingara/models/canvas_node.rb +397 -0
  23. data/lib/lingara/models/canvas_node_kind.rb +43 -0
  24. data/lib/lingara/models/canvas_object_ref.rb +237 -0
  25. data/lib/lingara/models/canvas_page.rb +289 -0
  26. data/lib/lingara/models/canvas_param.rb +105 -0
  27. data/lib/lingara/models/canvas_plan.rb +248 -0
  28. data/lib/lingara/models/canvas_recipe.rb +43 -0
  29. data/lib/lingara/models/canvas_summary.rb +511 -0
  30. data/lib/lingara/models/canvas_verdict.rb +389 -0
  31. data/lib/lingara/models/canvas_verdict_kind.rb +42 -0
  32. data/lib/lingara/models/create_canvas_request.rb +279 -0
  33. data/lib/lingara/models/create_dialogue_request.rb +248 -0
  34. data/lib/lingara/models/create_lesson_plan_event_error.rb +1 -1
  35. data/lib/lingara/models/create_lesson_plan_event_phase.rb +1 -1
  36. data/lib/lingara/models/create_lesson_plan_event_result.rb +1 -1
  37. data/lib/lingara/models/create_lesson_plan_event_started.rb +1 -1
  38. data/lib/lingara/models/dialogue.rb +367 -0
  39. data/lib/lingara/models/dialogue_active.rb +290 -0
  40. data/lib/lingara/models/dialogue_draft.rb +290 -0
  41. data/lib/lingara/models/dialogue_entry.rb +1 -1
  42. data/lib/lingara/models/dialogue_npc.rb +326 -0
  43. data/lib/lingara/models/dialogue_page.rb +277 -0
  44. data/lib/lingara/models/dialogue_rule.rb +55 -0
  45. data/lib/lingara/models/dialogue_summary.rb +431 -0
  46. data/lib/lingara/models/dialogue_transition.rb +264 -0
  47. data/lib/lingara/models/dialogue_turn_request.rb +110 -6
  48. data/lib/lingara/models/dialogue_warning.rb +285 -0
  49. data/lib/lingara/models/dialogue_warning_code.rb +42 -0
  50. data/lib/lingara/models/embed_player_link.rb +1 -1
  51. data/lib/lingara/models/embed_token.rb +1 -1
  52. data/lib/lingara/models/embed_token_request.rb +1 -1
  53. data/lib/lingara/models/error_envelope.rb +2 -2
  54. data/lib/lingara/models/event_envelope.rb +1 -1
  55. data/lib/lingara/models/event_page.rb +1 -1
  56. data/lib/lingara/models/fact_value.rb +105 -0
  57. data/lib/lingara/models/generate_vocabulary_event_done.rb +1 -1
  58. data/lib/lingara/models/generate_vocabulary_event_error.rb +1 -1
  59. data/lib/lingara/models/generate_vocabulary_event_item.rb +1 -1
  60. data/lib/lingara/models/generate_vocabulary_event_started.rb +1 -1
  61. data/lib/lingara/models/graph_checkpoint.rb +322 -0
  62. data/lib/lingara/models/graph_edge.rb +329 -0
  63. data/lib/lingara/models/inbound_event_accepted.rb +1 -1
  64. data/lib/lingara/models/lesson_plan.rb +1 -1
  65. data/lib/lingara/models/lesson_plan_content.rb +1 -1
  66. data/lib/lingara/models/lesson_plan_create_request.rb +1 -1
  67. data/lib/lingara/models/lesson_plan_failed_data.rb +1 -1
  68. data/lib/lingara/models/lesson_plan_ready_data.rb +1 -1
  69. data/lib/lingara/models/link_status.rb +1 -1
  70. data/lib/lingara/models/notice.rb +1 -1
  71. data/lib/lingara/models/npc.rb +1 -1
  72. data/lib/lingara/models/npc_language.rb +345 -0
  73. data/lib/lingara/models/pedagogy.rb +41 -0
  74. data/lib/lingara/models/plan_fail_reason.rb +1 -1
  75. data/lib/lingara/models/plan_pending.rb +1 -1
  76. data/lib/lingara/models/plan_phase.rb +1 -1
  77. data/lib/lingara/models/plan_question.rb +1 -1
  78. data/lib/lingara/models/plan_ready_status.rb +1 -1
  79. data/lib/lingara/models/plan_result.rb +1 -1
  80. data/lib/lingara/models/plan_set.rb +1 -1
  81. data/lib/lingara/models/plan_started.rb +1 -1
  82. data/lib/lingara/models/plan_status.rb +1 -1
  83. data/lib/lingara/models/plan_word.rb +1 -1
  84. data/lib/lingara/models/practice_completed_data.rb +1 -1
  85. data/lib/lingara/models/practice_item.rb +1 -1
  86. data/lib/lingara/models/practice_outcome.rb +1 -1
  87. data/lib/lingara/models/reaction_report.rb +1 -1
  88. data/lib/lingara/models/reaction_status.rb +1 -1
  89. data/lib/lingara/models/rename_canvas_request.rb +237 -0
  90. data/lib/lingara/models/rename_dialogue_request.rb +237 -0
  91. data/lib/lingara/models/save_canvas_draft_request.rb +273 -0
  92. data/lib/lingara/models/save_dialogue_draft_request.rb +274 -0
  93. data/lib/lingara/models/send_dialogue_turn_event_delta.rb +1 -1
  94. data/lib/lingara/models/send_dialogue_turn_event_done.rb +1 -1
  95. data/lib/lingara/models/send_dialogue_turn_event_error.rb +1 -1
  96. data/lib/lingara/models/send_dialogue_turn_event_notice.rb +1 -1
  97. data/lib/lingara/models/send_dialogue_turn_event_transition.rb +287 -0
  98. data/lib/lingara/models/send_tutor_message_event_delta.rb +1 -1
  99. data/lib/lingara/models/send_tutor_message_event_done.rb +1 -1
  100. data/lib/lingara/models/send_tutor_message_event_error.rb +1 -1
  101. data/lib/lingara/models/send_tutor_message_event_notice.rb +1 -1
  102. data/lib/lingara/models/speaker.rb +1 -1
  103. data/lib/lingara/models/stream_error.rb +1 -1
  104. data/lib/lingara/models/stream_events_event_done.rb +1 -1
  105. data/lib/lingara/models/stream_events_event_error.rb +1 -1
  106. data/lib/lingara/models/stream_events_event_event.rb +1 -1
  107. data/lib/lingara/models/stream_lesson_plan_event_error.rb +1 -1
  108. data/lib/lingara/models/stream_lesson_plan_event_pending.rb +1 -1
  109. data/lib/lingara/models/stream_lesson_plan_event_phase.rb +1 -1
  110. data/lib/lingara/models/stream_lesson_plan_event_result.rb +1 -1
  111. data/lib/lingara/models/stream_lesson_plan_event_started.rb +1 -1
  112. data/lib/lingara/models/threshold_scope.rb +1 -1
  113. data/lib/lingara/models/turn_delta.rb +1 -1
  114. data/lib/lingara/models/turn_entry.rb +1 -1
  115. data/lib/lingara/models/turn_role.rb +1 -1
  116. data/lib/lingara/models/tutor_turn_request.rb +1 -1
  117. data/lib/lingara/models/usage.rb +1 -1
  118. data/lib/lingara/models/usage_ledger.rb +1 -1
  119. data/lib/lingara/models/usage_threshold_reached_data.rb +1 -1
  120. data/lib/lingara/models/version_detail.rb +1 -1
  121. data/lib/lingara/models/version_history_entry.rb +1 -1
  122. data/lib/lingara/models/version_list.rb +1 -1
  123. data/lib/lingara/models/version_spec.rb +1 -1
  124. data/lib/lingara/models/version_state.rb +1 -1
  125. data/lib/lingara/models/version_summary.rb +1 -1
  126. data/lib/lingara/models/vocab_example.rb +1 -1
  127. data/lib/lingara/models/vocab_item.rb +1 -1
  128. data/lib/lingara/models/vocab_meta.rb +1 -1
  129. data/lib/lingara/models/vocab_request.rb +1 -1
  130. data/lib/lingara/models/vocab_started.rb +1 -1
  131. data/lib/lingara/models/world_context_changed.rb +1 -1
  132. data/lib/lingara/models/world_practice_completed.rb +1 -1
  133. data/lib/lingara/models/world_practice_requested.rb +1 -1
  134. data/lib/lingara/operations.rb +127 -1
  135. data/lib/lingara/streams.rb +1 -0
  136. data/lib/lingara/version.rb +3 -3
  137. data/lib/lingara.rb +2 -0
  138. data/sig/lingara.rbs +50 -0
  139. metadata +46 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e1d7800b570c108fac871dee55f078c590ebe11c708c5a9e991013b6a0a91a8
4
- data.tar.gz: 4e6fb8dbee4b626ec0027122306a1d69c878080624fdb02cfbfd77fa18b002c9
3
+ metadata.gz: 5e943622393135dc22889e58f20576f09b77e025d2cc932d5bc7b29e474106e7
4
+ data.tar.gz: 716d6f0423e4b090b1501a1a8a822dc8a9e288a9e584649be1801d781d275dc9
5
5
  SHA512:
6
- metadata.gz: 88939b0e78a2b3e05aeeec1c961ffe39c24f28c7edcc99e0815051d2188784904143790fb183f986e0d9c0eebcbd059082b34f8c0e12877203b16ca7a9980c0b
7
- data.tar.gz: 317b65dc02a825c0f15f08f3aa43b0fb53f2afa642e725be2e8f38f494a6d0fb220818adf5a5838d83c6f3364c74b9d0c0af4f541e9dd19c136f2174abbec4bd
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
@@ -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
- def pipeline(operation, url, body, on_start: nil, headers: {}, policy: @policy, &on_success)
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
- next Attempt.new(response: response, body: response.body.to_s) unless (200..299).cover?(response.code.to_i)
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