Skip to main content

turnframe_runtime/reduce/
copy.rs

1//! The codes of the runtime's own refusals and notices, and the copy it writes for them.
2
3use super::*;
4
5/// Rejection codes the default reducer produces.
6///
7/// They are stable strings, so an application can key its copy catalogue and its
8/// dashboards on them.
9pub mod rejection {
10    /// The proposed target shape is not one the operation accepts.
11    pub const TARGET_POLICY_MISMATCH: &str = "turnframe.target.policy_mismatch";
12    /// The token is unknown, or belongs to another tenant.
13    pub const TARGET_UNAUTHORIZED: &str = "turnframe.target.unauthorized";
14    /// The case the target named no longer exists, or nothing matched.
15    pub const TARGET_MISSING: &str = "turnframe.target.missing";
16    /// The case moved past the revision the target was issued at.
17    pub const TARGET_STALE: &str = "turnframe.target.stale";
18    /// The target resolved to something this reducer does not know how to use.
19    pub const TARGET_UNRESOLVED: &str = "turnframe.target.unresolved";
20    /// Too many candidates matched to offer an honest choice.
21    pub const TARGET_TOO_MANY_CANDIDATES: &str = "turnframe.target.too_many_candidates";
22    /// The act addressed the active card and there is none.
23    pub const NO_ACTIVE_INTERACTION: &str = "turnframe.interaction.none_active";
24    /// The card may not be resolved from typed text (§13.2 rule 8, §15.7).
25    pub const TEXT_RESOLUTION_NOT_ALLOWED: &str =
26        "turnframe.interaction.text_resolution_not_allowed";
27    /// The interpreted option is not one of the card's stored options.
28    pub const UNKNOWN_OPTION: &str = "turnframe.interaction.unknown_option";
29    /// The act named a workflow the registry does not know.
30    pub const UNKNOWN_WORKFLOW: &str = "turnframe.workflow.unknown";
31    /// The act named an operation this turn does not offer — or one the
32    /// workflow's own `compile_act` does not recognise, which is the same
33    /// sentence to a user and a defect in the deployment either way.
34    ///
35    /// Shared with [`turnframe_core::error::UNKNOWN_OPERATION`], which is the
36    /// code a workflow returns for the second case.
37    pub use turnframe_core::error::UNKNOWN_OPERATION;
38    /// The act's arguments are not the shape the operation's schema demands.
39    pub const INVALID_ARGUMENTS: &str = "turnframe.operation.invalid_arguments";
40    /// The act named an operation only a card may run, and cites no card.
41    pub const NOT_PROPOSABLE: &str = "turnframe.operation.not_proposable";
42    /// The workflow the act would start declares a prerequisite this turn does
43    /// not meet.
44    pub const PRECONDITION_UNMET: &str = "turnframe.workflow.precondition_unmet";
45    /// The act would start a workflow that resumes an open case, and the turn
46    /// can see more than one.
47    pub const SEVERAL_OPEN_CASES: &str = "turnframe.workflow.several_open_cases";
48    /// A constraint the user placed on the turn blocks the act.
49    pub const BLOCKED_BY_CONSTRAINT: &str = "turnframe.constraint.blocked";
50    /// The orchestration mode is not eligible for this risk class (§11.4).
51    pub const BLOCKED_BY_MODE: &str = "turnframe.policy.blocked_by_mode";
52    /// The policy configuration forbids this risk class outright.
53    pub const FORBIDDEN_RISK_CLASS: &str = "turnframe.policy.forbidden_risk_class";
54    /// Only a qualified human other than the user may authorize this.
55    pub const HUMAN_REVIEW_REQUIRED: &str = "turnframe.policy.human_review_required";
56    /// The act needs a record an earlier act of the turn opens, and that act did not run.
57    pub const PREREQUISITE_NOT_DONE: &str = "turnframe.operation.prerequisite_not_done";
58}
59
60/// Stable codes of the notices the default reducer emits.
61pub mod notice {
62    /// Nothing was submitted, because the user said not to submit.
63    pub const NOTHING_SUBMITTED: &str = "turnframe.notice.nothing_submitted";
64    /// Nothing was deleted, because the user said not to delete.
65    pub const NOTHING_DELETED: &str = "turnframe.notice.nothing_deleted";
66    /// Everything stayed a draft, because the user asked for drafts only.
67    pub const DRAFT_ONLY: &str = "turnframe.notice.draft_only";
68    /// No external effect was attempted, because the user forbade them.
69    pub const NO_EXTERNAL_EFFECTS: &str = "turnframe.notice.no_external_effects";
70    /// Part of the turn went through and part did not (§12.3 point 4).
71    pub const PARTIAL_RESULT: &str = "turnframe.notice.partial_result";
72    /// The domain refused an act the plan proposed.
73    pub const ACT_REFUSED: &str = "turnframe.notice.act_refused";
74    /// A value is asked for again, for a reason the server found.
75    pub const VALUE_ASKED_AGAIN: &str = "turnframe.notice.value_asked_again";
76    /// A file the turn carried and the model was not shown.
77    pub const ATTACHMENT_NOT_SHOWN: &str = "turnframe.notice.attachment_not_shown";
78    /// The card this turn clicked had already been answered.
79    pub const ALREADY_ANSWERED: &str = "turnframe.notice.already_answered";
80    /// Part of the message produced nothing to act on.
81    pub const NOT_UNDERSTOOD: &str = "turnframe.notice.not_understood";
82    /// The message could not be read at all, so nothing in it ran.
83    pub const MESSAGE_UNREADABLE: &str = "turnframe.notice.message_unreadable";
84    /// An act was left alone because the user asked to keep what it would change.
85    pub const KEPT_UNCHANGED: &str = "turnframe.notice.kept_unchanged";
86}
87
88/// Answer key of the "yes, the condition holds" option on the clarification
89/// card an [`ConstraintKind::ApplyOnlyIf`] produces.
90pub const CONDITION_HOLDS_ANSWER: &str = "turnframe.condition_holds";
91
92/// Server-authored copy for everything the reducer writes itself.
93#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct NoticeCopy {
96    /// Body of the [`notice::NOTHING_SUBMITTED`] notice.
97    pub nothing_submitted: LocalizedText,
98    /// Body of the [`notice::NOTHING_DELETED`] notice.
99    pub nothing_deleted: LocalizedText,
100    /// Body of the [`notice::DRAFT_ONLY`] notice.
101    pub draft_only: LocalizedText,
102    /// Body of the [`notice::NO_EXTERNAL_EFFECTS`] notice.
103    pub no_external_effects: LocalizedText,
104    /// Body of the [`notice::PARTIAL_RESULT`] notice.
105    pub partial_result: LocalizedText,
106    /// Title of a selection card.
107    pub select_target_title: LocalizedText,
108    /// Label of the "none of these" option on a selection card.
109    pub select_target_cancel: LocalizedText,
110    /// Title of the card an `ApplyOnlyIf` constraint raises.
111    pub condition_title: LocalizedText,
112    /// Label of the "yes, apply it" option on that card.
113    pub condition_yes: LocalizedText,
114    /// Label of the "no, leave it" option on that card.
115    pub condition_no: LocalizedText,
116    /// Body of the [`notice::ACT_REFUSED`] notice, for a domain rejection that carried
117    /// no explanation. Deliberately vague: a workflow that wants the user told why uses
118    /// [`DomainRejection::with_explanation`](turnframe_core::error::DomainRejection::with_explanation).
119    /// The runtime's own refusals have their own fields below.
120    pub act_refused: LocalizedText,
121    /// The act aimed at a shape the operation does not accept.
122    pub target_policy_mismatch: LocalizedText,
123    /// The handle named a record this actor may not reach.
124    pub target_unauthorized: LocalizedText,
125    /// Nothing matched the record the act named.
126    pub target_missing: LocalizedText,
127    /// The record moved on since the handle was issued.
128    pub target_stale: LocalizedText,
129    /// The target resolved to something the reducer cannot use.
130    pub target_unresolved: LocalizedText,
131    /// Too many records matched to offer an honest choice.
132    pub target_too_many_candidates: LocalizedText,
133    /// The workflow the act would start already has more than one case open.
134    pub several_open_cases: LocalizedText,
135    /// The act addressed the card on screen and there is none.
136    pub no_active_interaction: LocalizedText,
137    /// The card may not be answered by typing.
138    pub text_resolution_not_allowed: LocalizedText,
139    /// The option named is not one the card offers.
140    pub unknown_option: LocalizedText,
141    /// The act named a workflow this deployment does not run.
142    pub unknown_workflow: LocalizedText,
143    /// The act named an operation this turn does not offer.
144    pub unknown_operation: LocalizedText,
145    /// The card was clicked again while its first answer is still running.
146    pub already_answered_running: LocalizedText,
147    /// The card was clicked again after its first answer had committed.
148    pub already_answered_done: LocalizedText,
149    /// The card was clicked again after its first answer had failed.
150    pub already_answered_failed: LocalizedText,
151    /// The card was clicked again after it had been closed without doing the
152    /// work it offered.
153    pub already_answered_closed: LocalizedText,
154    /// The act's arguments are not what the operation takes.
155    pub invalid_arguments: LocalizedText,
156    /// The act named an operation only a card may run.
157    pub not_proposable: LocalizedText,
158    /// The orchestration mode may not carry this risk class.
159    pub blocked_by_mode: LocalizedText,
160    /// The configuration forbids this risk class outright.
161    pub forbidden_risk_class: LocalizedText,
162    /// Someone other than the user has to authorize this.
163    pub human_review_required: LocalizedText,
164    /// An act needed a record another act opens, and that act did not run.
165    pub prerequisite_not_done: LocalizedText,
166    /// Why a record the user named is asked for again: none is called that. `{workflow}`
167    /// and `{named}` are replaced.
168    pub record_not_found: LocalizedText,
169    /// The same, when none is called that and one can be registered now.
170    pub record_not_found_yet: LocalizedText,
171    /// The same, when several are called that.
172    pub record_not_unique: LocalizedText,
173    /// Words that produced nothing to act on; `{words}` is replaced by them.
174    pub not_understood: LocalizedText,
175    /// The whole message could not be read.
176    pub message_unreadable: LocalizedText,
177    /// Acts left alone as the user asked; `{words}` is replaced by the words asking it.
178    pub kept_unchanged: LocalizedText,
179}
180
181impl NoticeCopy {
182    /// The built-in copy: English, with Italian.
183    #[must_use]
184    pub fn standard() -> Self {
185        crate::copy::ServerCopy::translated(Self::english(), "it", ITALIAN)
186    }
187
188    /// English alone.
189    #[must_use]
190    pub fn english() -> Self {
191        Self {
192            nothing_submitted: LocalizedText::new("Nothing has been submitted."),
193            nothing_deleted: LocalizedText::new("Nothing has been deleted."),
194            draft_only: LocalizedText::new("Everything stayed a draft."),
195            no_external_effects: LocalizedText::new("No external effect was attempted."),
196            partial_result: LocalizedText::new("Part of this turn still needs your input."),
197            select_target_title: LocalizedText::new("Which one did you mean?"),
198            select_target_cancel: LocalizedText::new("None of these"),
199            condition_title: LocalizedText::new("Does that condition hold?"),
200            condition_yes: LocalizedText::new("Yes, apply it"),
201            condition_no: LocalizedText::new("No, leave it"),
202            act_refused: LocalizedText::new("One of the things you asked for could not be done."),
203            target_policy_mismatch: LocalizedText::new(
204                "I could not tell which record that was meant to change.",
205            ),
206            target_unauthorized: LocalizedText::new(
207                "That record is not one you can reach from here.",
208            ),
209            target_missing: LocalizedText::new("I could not find the record you meant."),
210            target_stale: LocalizedText::new(
211                "That record changed while we were talking, so I did not write over it.",
212            ),
213            target_unresolved: LocalizedText::new(
214                "I could not tell which record you meant, so I wrote nothing.",
215            ),
216            target_too_many_candidates: LocalizedText::new(
217                "Too many records match that description for me to pick one.",
218            ),
219            several_open_cases: LocalizedText::new(
220                "You have more than one of those open already, so I did not start another. \
221                 Tell me which one you mean.",
222            ),
223            no_active_interaction: LocalizedText::new(
224                "There is no question on screen for that answer.",
225            ),
226            text_resolution_not_allowed: LocalizedText::new(
227                "That one has to be answered with the buttons rather than in writing.",
228            ),
229            unknown_option: LocalizedText::new("That is not one of the choices on offer."),
230            unknown_workflow: LocalizedText::new("That is not something this assistant handles."),
231            unknown_operation: LocalizedText::new("That is not something I can do here."),
232            already_answered_running: LocalizedText::new(
233                "I am still working on that one: no need to press it again.",
234            ),
235            already_answered_done: LocalizedText::new("That one is already done."),
236            already_answered_failed: LocalizedText::new(
237                "That one did not go through, so nothing was changed by it.",
238            ),
239            already_answered_closed: LocalizedText::new("That one is no longer open."),
240            invalid_arguments: LocalizedText::new(
241                "I did not have what that needed, so I did not do it.",
242            ),
243            not_proposable: LocalizedText::new(
244                "That one only happens when you press the button for it.",
245            ),
246            blocked_by_mode: LocalizedText::new("I am not set up to do that here."),
247            forbidden_risk_class: LocalizedText::new("I am not allowed to do that at all."),
248            human_review_required: LocalizedText::new("Someone else has to approve that one."),
249            prerequisite_not_done: LocalizedText::new(
250                "That depended on something else you asked for, which did not happen.",
251            ),
252            record_not_found: LocalizedText::new("There is no {workflow} called «{named}»."),
253            record_not_found_yet: LocalizedText::new(
254                "There is no {workflow} called «{named}» yet: I can register it, or you can name another.",
255            ),
256            record_not_unique: LocalizedText::new("More than one {workflow} is called «{named}»."),
257            not_understood: LocalizedText::new("I did not understand this part: «{words}»."),
258            message_unreadable: LocalizedText::new(
259                "I could not make sense of that message, so I did nothing with it. \
260                 Could you say it another way?",
261            ),
262            kept_unchanged: LocalizedText::new("Left as it is, as you asked: «{words}»."),
263        }
264    }
265
266    /// What a user is told when they click a card that was already answered.
267    #[must_use]
268    pub fn already_answered(&self, progress: AnswerProgress) -> &LocalizedText {
269        match progress {
270            AnswerProgress::Running => &self.already_answered_running,
271            AnswerProgress::Done => &self.already_answered_done,
272            AnswerProgress::Failed => &self.already_answered_failed,
273            // A variant added later reads as "no longer open", which claims the
274            // least of the four.
275            AnswerProgress::Closed | _ => &self.already_answered_closed,
276        }
277    }
278
279    /// The notice code and body for a refusal the **runtime** decided, or
280    /// `None` when the code is not one of its own.
281    ///
282    /// The returned code is the rejection's own, not
283    /// [`notice::ACT_REFUSED`], and that is the point of the pair. Notices are
284    /// deduplicated by code, so a turn in which one act named a record nobody
285    /// could find and another answered a card that is not on screen used to
286    /// reach the user as a single sentence covering neither. Now it reaches
287    /// them as two, each keyed on a code an application can style, translate or
288    /// route on.
289    #[must_use]
290    pub fn runtime_refusal(&self, code: &str) -> Option<(&'static str, &LocalizedText)> {
291        let found = match code {
292            rejection::TARGET_POLICY_MISMATCH => (
293                rejection::TARGET_POLICY_MISMATCH,
294                &self.target_policy_mismatch,
295            ),
296            rejection::TARGET_UNAUTHORIZED => {
297                (rejection::TARGET_UNAUTHORIZED, &self.target_unauthorized)
298            }
299            rejection::TARGET_MISSING => (rejection::TARGET_MISSING, &self.target_missing),
300            rejection::TARGET_STALE => (rejection::TARGET_STALE, &self.target_stale),
301            rejection::TARGET_UNRESOLVED => (rejection::TARGET_UNRESOLVED, &self.target_unresolved),
302            rejection::TARGET_TOO_MANY_CANDIDATES => (
303                rejection::TARGET_TOO_MANY_CANDIDATES,
304                &self.target_too_many_candidates,
305            ),
306            rejection::SEVERAL_OPEN_CASES => {
307                (rejection::SEVERAL_OPEN_CASES, &self.several_open_cases)
308            }
309            rejection::NO_ACTIVE_INTERACTION => (
310                rejection::NO_ACTIVE_INTERACTION,
311                &self.no_active_interaction,
312            ),
313            rejection::TEXT_RESOLUTION_NOT_ALLOWED => (
314                rejection::TEXT_RESOLUTION_NOT_ALLOWED,
315                &self.text_resolution_not_allowed,
316            ),
317            rejection::UNKNOWN_OPTION => (rejection::UNKNOWN_OPTION, &self.unknown_option),
318            rejection::UNKNOWN_WORKFLOW => (rejection::UNKNOWN_WORKFLOW, &self.unknown_workflow),
319            rejection::UNKNOWN_OPERATION => (rejection::UNKNOWN_OPERATION, &self.unknown_operation),
320            rejection::INVALID_ARGUMENTS => (rejection::INVALID_ARGUMENTS, &self.invalid_arguments),
321            rejection::NOT_PROPOSABLE => (rejection::NOT_PROPOSABLE, &self.not_proposable),
322            rejection::BLOCKED_BY_MODE => (rejection::BLOCKED_BY_MODE, &self.blocked_by_mode),
323            rejection::FORBIDDEN_RISK_CLASS => {
324                (rejection::FORBIDDEN_RISK_CLASS, &self.forbidden_risk_class)
325            }
326            rejection::HUMAN_REVIEW_REQUIRED => (
327                rejection::HUMAN_REVIEW_REQUIRED,
328                &self.human_review_required,
329            ),
330            rejection::PREREQUISITE_NOT_DONE => (
331                rejection::PREREQUISITE_NOT_DONE,
332                &self.prerequisite_not_done,
333            ),
334            _ => return None,
335        };
336        Some(found)
337    }
338}
339
340impl Default for NoticeCopy {
341    fn default() -> Self {
342        Self::standard()
343    }
344}
345
346crate::copy::server_copy!(
347    NoticeCopy,
348    [
349        nothing_submitted,
350        nothing_deleted,
351        draft_only,
352        no_external_effects,
353        partial_result,
354        select_target_title,
355        select_target_cancel,
356        condition_title,
357        condition_yes,
358        condition_no,
359        act_refused,
360        target_policy_mismatch,
361        target_unauthorized,
362        target_missing,
363        target_stale,
364        target_unresolved,
365        target_too_many_candidates,
366        several_open_cases,
367        no_active_interaction,
368        text_resolution_not_allowed,
369        unknown_option,
370        unknown_workflow,
371        unknown_operation,
372        already_answered_running,
373        already_answered_done,
374        already_answered_failed,
375        already_answered_closed,
376        invalid_arguments,
377        not_proposable,
378        blocked_by_mode,
379        forbidden_risk_class,
380        human_review_required,
381        prerequisite_not_done,
382        record_not_found,
383        record_not_found_yet,
384        record_not_unique,
385        not_understood,
386        message_unreadable,
387        kept_unchanged
388    ]
389);
390
391/// The built-in Italian of [`NoticeCopy`], by field.
392const ITALIAN: &[(&str, &str)] = &[
393    ("nothing_submitted", "Non è stato inviato nulla."),
394    ("nothing_deleted", "Non è stato eliminato nulla."),
395    ("draft_only", "È rimasto tutto in bozza."),
396    (
397        "no_external_effects",
398        "Non è stata tentata nessuna operazione verso l'esterno.",
399    ),
400    (
401        "partial_result",
402        "Una parte di questa richiesta aspetta ancora una tua risposta.",
403    ),
404    ("select_target_title", "Quale intendevi?"),
405    ("select_target_cancel", "Nessuno di questi"),
406    ("condition_title", "Quella condizione vale?"),
407    ("condition_yes", "Sì, applicala"),
408    ("condition_no", "No, lascia stare"),
409    (
410        "act_refused",
411        "Una delle cose che hai chiesto non si è potuta fare.",
412    ),
413    (
414        "target_policy_mismatch",
415        "Non ho capito quale scheda andava modificata.",
416    ),
417    (
418        "target_unauthorized",
419        "Da qui non puoi accedere a quella scheda.",
420    ),
421    ("target_missing", "Non ho trovato la scheda che intendevi."),
422    (
423        "target_stale",
424        "Quella scheda è cambiata mentre parlavamo, quindi non l'ho sovrascritta.",
425    ),
426    (
427        "target_unresolved",
428        "Non ho capito quale scheda intendevi, quindi non ho scritto nulla.",
429    ),
430    (
431        "target_too_many_candidates",
432        "Troppe schede corrispondono a quella descrizione perché possa sceglierne una.",
433    ),
434    (
435        "several_open_cases",
436        "Ce n'è già più di una aperta, quindi non ne ho aperta un'altra. Dimmi quale intendi.",
437    ),
438    (
439        "no_active_interaction",
440        "Non c'è nessuna domanda sullo schermo a cui rispondere così.",
441    ),
442    (
443        "text_resolution_not_allowed",
444        "A questa si risponde con i pulsanti, non per iscritto.",
445    ),
446    ("unknown_option", "Non è una delle scelte disponibili."),
447    ("unknown_workflow", "Questo assistente non se ne occupa."),
448    ("unknown_operation", "Qui non posso farlo."),
449    (
450        "already_answered_running",
451        "Ci sto ancora lavorando: non serve premerlo di nuovo.",
452    ),
453    ("already_answered_done", "È già fatto."),
454    (
455        "already_answered_failed",
456        "Non è andato a buon fine, quindi non ha cambiato nulla.",
457    ),
458    ("already_answered_closed", "Non è più aperto."),
459    (
460        "invalid_arguments",
461        "Mancava quello che serviva, quindi non l'ho fatto.",
462    ),
463    (
464        "not_proposable",
465        "Succede solo quando premi il pulsante apposito.",
466    ),
467    ("blocked_by_mode", "Qui non sono configurato per farlo."),
468    (
469        "forbidden_risk_class",
470        "Non mi è permesso farlo in nessun caso.",
471    ),
472    ("human_review_required", "Deve approvarlo qualcun altro."),
473    (
474        "prerequisite_not_done",
475        "Dipendeva da un'altra cosa che hai chiesto, che non è stata fatta.",
476    ),
477    ("record_not_found", "Non trovo «{named}» come {workflow}."),
478    (
479        "record_not_found_yet",
480        "Non trovo ancora «{named}» come {workflow}: posso registrarlo io, oppure puoi indicare un altro nome.",
481    ),
482    (
483        "record_not_unique",
484        "Per «{named}» come {workflow} trovo più di un risultato.",
485    ),
486    ("not_understood", "Non ho capito questa parte: «{words}»."),
487    (
488        "message_unreadable",
489        "Non ho capito il messaggio, quindi non ho fatto nulla. Puoi dirlo in un altro modo?",
490    ),
491    (
492        "kept_unchanged",
493        "Lasciato com'è, come mi hai chiesto: «{words}».",
494    ),
495];