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