beck_core/
command.rs

1//! What a client may send, resolved once: the `Command` union as a decoder.
2//!
3//! §3.5: "the client's entire write surface is `send(cmd)` into a typed `Command` union. There is
4//! no other mutation path — mass assignment and over-posting have no representation." That
5//! property is enforced here, and here only: a field the union does not declare is not decoded, it
6//! is rejected.
7//!
8//! # Why this is a schema rather than a lookup
9//!
10//! The decoder used to read the program's type table on every command, which is fine when the
11//! decoder and the type table are in the same process. Mode B's client has neither — it holds a
12//! bundle, not a program ([`crate::bundle`]) — and the one thing worse than a client that cannot
13//! decode a command is a *second* decoder written to a second reading of the same union. So the
14//! union is resolved to this at compile time, both tiers decode with the same function, and
15//! "the client and the server disagree about what `Toggle` is" stops being expressible.
16
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21use crate::core::{Fields, Value};
22use crate::split::Placed;
23use crate::ty::{Ty, TyDecl};
24
25/// The program's command union, flattened to what a decoder needs.
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Schema {
28    /// The union's name — `Command`, unless the program named it something else.
29    pub ty: String,
30    pub variants: Vec<Variant>,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Variant {
35    pub name: String,
36    pub fields: Vec<(String, FieldTy)>,
37}
38
39/// A field's type, resolved through however many newtypes wrap it.
40///
41/// "A newtype is transparent on the wire and nominal in the type system" — the whole point of
42/// §3.1's "ids of different entities must not be interchangeable" — so the wrappers are recorded
43/// in the order they have to be rebuilt.
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45pub enum FieldTy {
46    Str,
47    Int,
48    Bool,
49    Float,
50    /// `Id(value=<inner>)`.
51    Newtype(String, Box<FieldTy>),
52    /// A type the wire format has no encoding for. Carried rather than dropped so the refusal
53    /// names it, and so a bundle built for a program with one is not silently missing a field.
54    Undecodable(String),
55}
56
57impl Schema {
58    pub fn of(placed: &Placed) -> Schema {
59        Schema::of_union(
60            placed,
61            placed.roles.command_ty.con_name().unwrap_or("Command"),
62        )
63    }
64
65    /// The same, for any union a client may construct values of.
66    ///
67    /// D30's gestures need one: a gesture is built by a handler in the page exactly as a command
68    /// is, so it needs the same resolved decoder — and it needs a *separate* one, because the two
69    /// unions are different types and a client that could decode a gesture as a command would have
70    /// found the write surface §3.5 closes.
71    pub fn of_union(placed: &Placed, name: &str) -> Schema {
72        let name = name.to_string();
73        let variants = match placed.program.types.get(name.as_str()) {
74            Some(TyDecl::Union { variants, .. }) => variants
75                .iter()
76                .map(|v| Variant {
77                    name: v.name.to_string(),
78                    fields: v
79                        .fields
80                        .iter()
81                        .map(|(f, ty)| (f.to_string(), FieldTy::of(ty, placed)))
82                        .collect(),
83                })
84                .collect(),
85            // Not a union, so nothing can be decoded against it. The refusal happens per command
86            // rather than here: a library has no command type at all, and building one of these
87            // for it must not be an error.
88            _ => Vec::new(),
89        };
90        Schema { ty: name, variants }
91    }
92
93    /// Decode a command from the wire, against the program's own `Command` union.
94    pub fn decode(&self, json: &serde_json::Value) -> Result<Value, String> {
95        let tag = json
96            .get("c")
97            .and_then(|c| c.as_str())
98            .ok_or("a command needs a `c` tag naming its variant")?;
99        let variant = self
100            .variants
101            .iter()
102            .find(|v| v.name == tag)
103            .ok_or_else(|| format!("`{tag}` is not a variant of `{}`", self.ty))?;
104
105        let mut fields = Fields::new();
106        for (field, ty) in &variant.fields {
107            let raw = json
108                .get(field.as_str())
109                .ok_or_else(|| format!("`{tag}` needs a `{field}`"))?;
110            fields.insert(Arc::from(field.as_str()), ty.decode(raw)?);
111        }
112        Ok(Value::data(
113            Arc::from(self.ty.as_str()),
114            Some(Arc::from(variant.name.as_str())),
115            fields,
116        ))
117    }
118}
119
120impl FieldTy {
121    fn of(ty: &Ty, placed: &Placed) -> FieldTy {
122        let name = ty.con_name().unwrap_or("");
123        if let Some(TyDecl::Newtype { inner, .. }) = placed.program.types.get(name) {
124            return FieldTy::Newtype(name.to_string(), Box::new(FieldTy::of(inner, placed)));
125        }
126        match name {
127            Ty::STR => FieldTy::Str,
128            Ty::INT => FieldTy::Int,
129            Ty::BOOL => FieldTy::Bool,
130            Ty::FLOAT => FieldTy::Float,
131            other => FieldTy::Undecodable(other.to_string()),
132        }
133    }
134
135    fn decode(&self, raw: &serde_json::Value) -> Result<Value, String> {
136        match self {
137            FieldTy::Newtype(name, inner) => Ok(Value::data(
138                Arc::from(name.as_str()),
139                None,
140                Fields::from_iter([(Arc::from("value"), inner.decode(raw)?)]),
141            )),
142            FieldTy::Str => raw
143                .as_str()
144                .map(Value::str_)
145                .ok_or_else(|| format!("expected a string, got {raw}")),
146            FieldTy::Int => raw
147                .as_i64()
148                .map(Value::Int)
149                .ok_or_else(|| format!("expected an integer, got {raw}")),
150            FieldTy::Bool => raw
151                .as_bool()
152                .map(Value::Bool)
153                .ok_or_else(|| format!("expected a boolean, got {raw}")),
154            // A real crosses the wire as a JSON number, and an integral one arrives as an integer
155            // — `1` and `1.0` are the same JSON token — so this accepts either and canonicalises
156            // through `Value::float` (`docs/27` §27.2).
157            FieldTy::Float => raw
158                .as_f64()
159                .map(Value::float)
160                .ok_or_else(|| format!("expected a number, got {raw}")),
161            FieldTy::Undecodable(other) => Err(format!("cannot decode `{other}` from the wire")),
162        }
163    }
164}