Skip to main content

typed_openapi/
plan.rs

1//! The gate: whether a built request may be sent, decided once.
2//!
3//! Every caller that has a request and what the user answered builds a [`Plan`]
4//! and executes what it gets back, rather than testing the flags again. That is
5//! what makes a dry run print the exact bytes a confirmed run sends — they are
6//! this one value.
7//!
8//! What the user answered is an [`Answers`]: the write confirmation, plus one
9//! answer per gate the operation names. Both halves are demanded together, so
10//! an operation that stands behind `enshrine` needs the confirmation *and* that
11//! word, and neither substitutes for the other.
12//!
13//! Nothing here knows about command lines. A CLI reaches it through
14//! the `tree` module (feature `clap`), and a Rust caller that wants the same gate over a typed
15//! wrapper calls [`Plan::decide`] directly, which is what the `finalize-voucher`
16//! verb in the example does.
17
18use std::collections::BTreeSet;
19
20use http::{Request, Uri};
21use thiserror::Error;
22
23use crate::model::{Effect, Gate, Operation};
24use crate::request::{Invocation, ValueError};
25use crate::values::Values;
26
27/// What a caller answered at the gate: the write confirmation, and one answer
28/// per named gate the operation carries.
29///
30/// Default-closed in both halves, because a question nobody was asked is a
31/// question nobody answered: an `Answers` built and never spoken to holds every
32/// write back, and answering a gate no operation names opens nothing. Adding an
33/// answer can only ever let a request through — never take one back.
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub struct Answers {
36    commit: bool,
37    gates: BTreeSet<String>,
38}
39
40impl Answers {
41    /// Nothing answered: what a read is put to the gate with, and what every
42    /// write is refused by.
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// The write confirmation — `--commit`, on a command line.
49    #[must_use]
50    pub fn commit(mut self) -> Self {
51        self.commit = true;
52        self
53    }
54
55    /// One named gate, answered.
56    ///
57    /// A name is taken as it is given rather than checked against a document:
58    /// which gates exist is the operation's say, so a name no operation carries
59    /// opens nothing and is not an error.
60    #[must_use]
61    pub fn gate(mut self, name: impl Into<String>) -> Self {
62        self.gates.insert(name.into());
63        self
64    }
65
66    /// Was the write confirmed?
67    #[must_use]
68    pub fn committed(&self) -> bool {
69        self.commit
70    }
71
72    /// Was this gate answered?
73    #[must_use]
74    pub fn answered(&self, gate: &Gate) -> bool {
75        self.gates.contains(gate.as_str())
76    }
77}
78
79/// What the gate decided, with the request already built.
80///
81/// This is the only place in the crate that decides whether something is sent.
82#[derive(Debug, Clone)]
83pub enum Plan {
84    /// A read, or a write the user confirmed.
85    Send(Request<Vec<u8>>),
86    /// A write without confirmation. Print it; send nothing.
87    DryRun(Request<Vec<u8>>),
88}
89
90impl Plan {
91    /// The gate: a read runs on sight, a write runs once it is confirmed and
92    /// every gate it names is answered.
93    #[must_use]
94    pub fn decide(op: &Operation, answers: &Answers, request: Request<Vec<u8>>) -> Self {
95        match (op.effect(), opened(op, answers)) {
96            (Effect::Read, _) | (Effect::Write, true) => Self::Send(request),
97            (Effect::Write, false) => Self::DryRun(request),
98        }
99    }
100
101    /// Values that satisfy an operation, rendered against a server and put to
102    /// the gate — validation, rendering and the verdict in one step, so no
103    /// caller can do two of the three and skip the last.
104    pub fn build(
105        op: &Operation,
106        base: &Uri,
107        values: Values,
108        answers: &Answers,
109    ) -> Result<Self, PlanError> {
110        let invocation = Invocation::new(op, values)?;
111        Ok(Self::decide(op, answers, invocation.request(base)?))
112    }
113
114    /// The request, whichever way the gate went — a dry run prints exactly the
115    /// bytes a confirmed run sends, because they are this one value.
116    #[must_use]
117    pub fn request(&self) -> &Request<Vec<u8>> {
118        match self {
119            Self::Send(request) | Self::DryRun(request) => request,
120        }
121    }
122}
123
124/// Is every question this operation asks answered?
125///
126/// The confirmation *and* each gate the document names, all of them together: a
127/// gate nobody answered can only hold a request back, never let one through.
128fn opened(op: &Operation, answers: &Answers) -> bool {
129    answers.committed() && op.gates().iter().all(|gate| answers.answered(gate))
130}
131
132/// Why values that parsed did not turn into a request.
133#[derive(Debug, Error)]
134pub enum PlanError {
135    #[error(transparent)]
136    Value(#[from] ValueError),
137    #[error("cannot build the request: {0}")]
138    Request(#[from] http::Error),
139}