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 let name = placed
60 .roles
61 .command_ty
62 .con_name()
63 .unwrap_or("Command")
64 .to_string();
65 let variants = match placed.program.types.get(name.as_str()) {
66 Some(TyDecl::Union { variants, .. }) => variants
67 .iter()
68 .map(|v| Variant {
69 name: v.name.to_string(),
70 fields: v
71 .fields
72 .iter()
73 .map(|(f, ty)| (f.to_string(), FieldTy::of(ty, placed)))
74 .collect(),
75 })
76 .collect(),
77 // Not a union, so nothing can be decoded against it. The refusal happens per command
78 // rather than here: a library has no command type at all, and building one of these
79 // for it must not be an error.
80 _ => Vec::new(),
81 };
82 Schema { ty: name, variants }
83 }
84
85 /// Decode a command from the wire, against the program's own `Command` union.
86 pub fn decode(&self, json: &serde_json::Value) -> Result<Value, String> {
87 let tag = json
88 .get("c")
89 .and_then(|c| c.as_str())
90 .ok_or("a command needs a `c` tag naming its variant")?;
91 let variant = self
92 .variants
93 .iter()
94 .find(|v| v.name == tag)
95 .ok_or_else(|| format!("`{tag}` is not a variant of `{}`", self.ty))?;
96
97 let mut fields = Fields::new();
98 for (field, ty) in &variant.fields {
99 let raw = json
100 .get(field.as_str())
101 .ok_or_else(|| format!("`{tag}` needs a `{field}`"))?;
102 fields.insert(Arc::from(field.as_str()), ty.decode(raw)?);
103 }
104 Ok(Value::data(
105 Arc::from(self.ty.as_str()),
106 Some(Arc::from(variant.name.as_str())),
107 fields,
108 ))
109 }
110}
111
112impl FieldTy {
113 fn of(ty: &Ty, placed: &Placed) -> FieldTy {
114 let name = ty.con_name().unwrap_or("");
115 if let Some(TyDecl::Newtype { inner, .. }) = placed.program.types.get(name) {
116 return FieldTy::Newtype(name.to_string(), Box::new(FieldTy::of(inner, placed)));
117 }
118 match name {
119 Ty::STR => FieldTy::Str,
120 Ty::INT => FieldTy::Int,
121 Ty::BOOL => FieldTy::Bool,
122 Ty::FLOAT => FieldTy::Float,
123 other => FieldTy::Undecodable(other.to_string()),
124 }
125 }
126
127 fn decode(&self, raw: &serde_json::Value) -> Result<Value, String> {
128 match self {
129 FieldTy::Newtype(name, inner) => Ok(Value::data(
130 Arc::from(name.as_str()),
131 None,
132 Fields::from_iter([(Arc::from("value"), inner.decode(raw)?)]),
133 )),
134 FieldTy::Str => raw
135 .as_str()
136 .map(Value::str_)
137 .ok_or_else(|| format!("expected a string, got {raw}")),
138 FieldTy::Int => raw
139 .as_i64()
140 .map(Value::Int)
141 .ok_or_else(|| format!("expected an integer, got {raw}")),
142 FieldTy::Bool => raw
143 .as_bool()
144 .map(Value::Bool)
145 .ok_or_else(|| format!("expected a boolean, got {raw}")),
146 // A real crosses the wire as a JSON number, and an integral one arrives as an integer
147 // — `1` and `1.0` are the same JSON token — so this accepts either and canonicalises
148 // through `Value::float` (`docs/32` §32.6).
149 FieldTy::Float => raw
150 .as_f64()
151 .map(Value::float)
152 .ok_or_else(|| format!("expected a number, got {raw}")),
153 FieldTy::Undecodable(other) => Err(format!("cannot decode `{other}` from the wire")),
154 }
155 }
156}