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}