1use std::collections::BTreeMap;
4
5use crate::error::{err, index, member, ErrorCode, Result};
6use crate::json::{self, jcs_sorted, Value};
7use crate::lexical::{self, limits};
8use crate::model::{Arg, Cell, Scalar, Type};
9use crate::schema;
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub enum RateSide {
14 Currency(String),
16 Arg(String),
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22pub enum Unit {
23 Literal(String),
25 Arg(String),
27 Rate {
29 of: RateSide,
31 per: RateSide,
33 },
34}
35
36impl Unit {
37 pub fn to_value(&self) -> Value {
39 fn arg(n: &str) -> Value {
40 Value::Object(vec![("arg".into(), Value::String(n.into()))])
41 }
42 fn side(s: &RateSide) -> Value {
43 match s {
44 RateSide::Currency(c) => Value::String(c.clone()),
45 RateSide::Arg(n) => arg(n),
46 }
47 }
48 match self {
49 Unit::Literal(s) => Value::String(s.clone()),
50 Unit::Arg(n) => arg(n),
51 Unit::Rate { of, per } => Value::Object(vec![("of".into(), side(of)), ("per".into(), side(per))]),
52 }
53 }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq, Hash)]
58pub struct ArgDecl {
59 pub ty: Type,
61 pub unit: Option<Unit>,
63 pub valid: bool,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Hash)]
69pub struct Returns {
70 pub ty: Type,
72 pub unit: Option<Unit>,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, Hash)]
78pub struct Signature {
79 pub args: BTreeMap<String, ArgDecl>,
81 pub returns: Returns,
83}
84
85impl Signature {
86 pub fn to_value(&self) -> Value {
88 let args = self
89 .args
90 .iter()
91 .map(|(k, d)| {
92 let mut m = vec![("type".to_string(), Value::String(d.ty.as_str().into()))];
93 if let Some(u) = &d.unit {
94 m.push(("unit".into(), u.to_value()));
95 }
96 m.push(("valid".into(), Value::Bool(d.valid)));
97 (k.clone(), Value::Object(m))
98 })
99 .collect();
100 let returns = match &self.returns.unit {
101 Some(u) => Value::Object(vec![
102 ("type".into(), Value::String(self.returns.ty.as_str().into())),
103 ("unit".into(), u.to_value()),
104 ]),
105 None => Value::String(self.returns.ty.as_str().into()),
106 };
107 Value::Object(vec![("args".into(), Value::Object(args)), ("returns".into(), returns)])
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, Hash)]
113pub struct Descriptor {
114 pub lang: String,
116 pub body: String,
118 pub inputs: Vec<String>,
120 pub signature: Signature,
122}
123
124impl Descriptor {
125 pub fn from_json(bytes: &[u8]) -> Result<Descriptor> {
127 Descriptor::from_value(&json::parse(bytes)?)
128 }
129
130 pub fn from_value(v: &Value) -> Result<Descriptor> {
132 json::check_value_depth(v)?;
133 descriptor_structure(v)?;
134 descriptor_canonical(v)
135 }
136
137 pub fn to_value(&self) -> Value {
139 Value::Object(vec![
140 (
141 "code".into(),
142 Value::Object(vec![
143 ("body".into(), Value::String(self.body.clone())),
144 ("lang".into(), Value::String(self.lang.clone())),
145 ]),
146 ),
147 ("inputs".into(), Value::Array(self.inputs.iter().map(|s| Value::String(s.clone())).collect())),
148 ("signature".into(), self.signature.to_value()),
149 ])
150 }
151
152 pub fn canonical(&self) -> Vec<u8> {
154 self.to_value().to_jcs()
155 }
156
157 pub fn hash(&self) -> String {
159 crate::crypto::sha256_hex(&self.canonical())
160 }
161}
162
163fn unit_shape(v: &Value, path: &str) -> Result<()> {
164 let bad = || err(ErrorCode::Schema, path, "a unit is a string, {\"arg\": name} or {\"of\", \"per\"}");
165 match v {
166 Value::String(_) => Ok(()),
167 Value::Object(m) if m.len() == 1 && m[0].0 == "arg" => match m[0].1 {
168 Value::String(_) => Ok(()),
169 _ => bad(),
170 },
171 Value::Object(m) if m.len() == 2 && schema::opt(m, "of").is_some() && schema::opt(m, "per").is_some() => {
172 for side in ["of", "per"] {
173 let ok = match schema::get(m, side) {
174 Value::String(_) => true,
175 Value::Object(a) => a.len() == 1 && a[0].0 == "arg" && matches!(a[0].1, Value::String(_)),
176 _ => false,
177 };
178 if !ok {
179 return err(
180 ErrorCode::Schema,
181 &member(path, side),
182 "a rate side is a currency code or {\"arg\": name}",
183 );
184 }
185 }
186 Ok(())
187 }
188 _ => bad(),
189 }
190}
191
192pub(crate) fn descriptor_structure(v: &Value) -> Result<()> {
193 let m = schema::object(v, "$")?;
194 schema::exact(m, &["code", "inputs", "signature"], &[], "$", ErrorCode::Schema)?;
195 let code = schema::object(schema::get(m, "code"), "$.code")?;
196 schema::exact(code, &["body", "lang"], &[], "$.code", ErrorCode::Schema)?;
197 schema::string(code, "body", "$.code")?;
198 schema::string(code, "lang", "$.code")?;
199 match schema::get(m, "inputs") {
200 Value::Array(items) if items.iter().all(|i| matches!(i, Value::String(_))) => {}
201 _ => return err(ErrorCode::Schema, "$.inputs", "inputs must be an array of strings"),
202 }
203 let sig = schema::object(schema::get(m, "signature"), "$.signature")?;
204 schema::exact(sig, &["args", "returns"], &[], "$.signature", ErrorCode::Schema)?;
205 let args = schema::object(schema::get(sig, "args"), "$.signature.args")?;
206 for (k, d) in jcs_sorted(args) {
207 let p = member("$.signature.args", k);
208 let dm = schema::object(d, &p)?;
209 schema::exact(dm, &["type", "valid"], &["unit"], &p, ErrorCode::Schema)?;
210 schema::string(dm, "type", &p)?;
211 if !matches!(schema::get(dm, "valid"), Value::Bool(_)) {
212 return err(ErrorCode::Schema, &member(&p, "valid"), "valid must be a boolean");
213 }
214 if let Some(u) = schema::opt(dm, "unit") {
215 unit_shape(u, &member(&p, "unit"))?;
216 }
217 }
218 let rp = "$.signature.returns";
219 match schema::get(sig, "returns") {
220 Value::String(_) => {}
221 Value::Object(rm) => {
222 schema::exact(rm, &["type"], &["unit"], rp, ErrorCode::Schema)?;
223 schema::string(rm, "type", rp)?;
224 if let Some(u) = schema::opt(rm, "unit") {
225 unit_shape(u, &member(rp, "unit"))?;
226 }
227 }
228 _ => return err(ErrorCode::Schema, rp, "returns must be a type name or an object"),
229 }
230 Ok(())
231}
232
233fn unit_arg_ref(name: &str, args: &[(String, Value)], path: &str) -> Result<()> {
234 let is_string = schema::opt(args, name)
235 .and_then(Value::as_object)
236 .map(|d| schema::get(d, "type").as_str() == Some("string"))
237 .unwrap_or(false);
238 if !is_string {
239 return err(ErrorCode::CanonUnit, path, format!("a unit names a string argument of the signature: {name:?}"));
240 }
241 Ok(())
242}
243
244fn side_value(v: &Value) -> RateSide {
245 match v {
246 Value::String(s) => RateSide::Currency(s.clone()),
247 other => RateSide::Arg(other.get("arg").and_then(Value::as_str).unwrap_or("").into()),
248 }
249}
250
251fn unit_canonical(v: &Value, args: &[(String, Value)], path: &str) -> Result<Unit> {
253 match v {
254 Value::String(s) => {
255 if !lexical::is_literal_unit(s) {
256 return err(ErrorCode::CanonUnit, path, format!("not a unit listed in units.md: {s:?}"));
257 }
258 Ok(Unit::Literal(s.clone()))
259 }
260 Value::Object(m) if m.len() == 1 => {
261 let name = schema::get(m, "arg").as_str().unwrap_or("");
262 unit_arg_ref(name, args, path)?;
263 Ok(Unit::Arg(name.into()))
264 }
265 Value::Object(m) => {
266 for side in ["of", "per"] {
267 let sp = member(path, side);
268 match schema::get(m, side) {
269 Value::String(s) => {
270 if !lexical::is_currency(s) {
271 return err(ErrorCode::CanonUnit, &sp, format!("a rate side is a currency code: {s:?}"));
272 }
273 }
274 other => unit_arg_ref(other.get("arg").and_then(Value::as_str).unwrap_or(""), args, &sp)?,
275 }
276 }
277 Ok(Unit::Rate { of: side_value(schema::get(m, "of")), per: side_value(schema::get(m, "per")) })
278 }
279 _ => err(ErrorCode::Schema, path, "not a unit"),
280 }
281}
282
283pub(crate) fn descriptor_canonical(v: &Value) -> Result<Descriptor> {
284 let m = schema::object(v, "$")?;
285 let code = schema::object(schema::get(m, "code"), "$.code")?;
286 let body = schema::string(code, "body", "$.code")?;
287 lexical::check_text(body, limits::BODY, true, "$.code.body")?;
288 let lang = schema::string(code, "lang", "$.code")?;
289 lexical::check_name(lang, "$.code.lang")?;
290
291 let items: Vec<&str> = match schema::get(m, "inputs") {
292 Value::Array(items) => items.iter().filter_map(Value::as_str).collect(),
293 _ => Vec::new(),
294 };
295 for (i, s) in items.iter().enumerate() {
296 lexical::check_function_name(s, &index("$.inputs", i))?;
297 }
298 if items.windows(2).any(|w| w[0] >= w[1]) {
299 return err(ErrorCode::CanonSet, "$.inputs", "entries must be strictly ascending");
300 }
301 if items.len() > limits::INPUTS {
302 return err(ErrorCode::CanonLimit, "$.inputs", "more than 4096 inputs");
303 }
304
305 let sig = schema::object(schema::get(m, "signature"), "$.signature")?;
306 let args = schema::object(schema::get(sig, "args"), "$.signature.args")?;
307 if args.len() > limits::ARGS {
308 return err(ErrorCode::CanonLimit, "$.signature.args", "more than 32 arguments");
309 }
310 let mut decls = BTreeMap::new();
311 for (k, d) in jcs_sorted(args) {
312 let p = member("$.signature.args", k);
313 lexical::check_arg_name(k, &p)?;
314 let dm = schema::object(d, &p)?;
315 let ty = Type::parse(schema::string(dm, "type", &p)?, &member(&p, "type"))?;
316 let unit = match (ty, schema::opt(dm, "unit")) {
317 (Type::Number, None) => return err(ErrorCode::CanonUnit, &p, "a number argument declares its unit"),
318 (Type::Number, Some(u)) => Some(unit_canonical(u, args, &member(&p, "unit"))?),
319 (_, Some(_)) => return err(ErrorCode::CanonUnit, &member(&p, "unit"), "only number arguments have a unit"),
320 (_, None) => None,
321 };
322 let valid = matches!(schema::get(dm, "valid"), Value::Bool(true));
323 if valid && !ty.may_be_valid_time() {
324 return err(ErrorCode::CanonType, &p, "a valid-time argument must be a date, instant or string");
325 }
326 decls.insert(k.clone(), ArgDecl { ty, unit, valid });
327 }
328
329 let rp = "$.signature.returns";
330 let returns = match schema::get(sig, "returns") {
331 Value::String(name) => {
332 let ty = Type::parse(name, rp)?;
333 if ty == Type::Number {
334 return err(
335 ErrorCode::CanonUnit,
336 rp,
337 "a number return is written {\"type\": \"number\", \"unit\": ...}",
338 );
339 }
340 Returns { ty, unit: None }
341 }
342 Value::Object(rm) => {
343 let ty = Type::parse(schema::string(rm, "type", rp)?, &member(rp, "type"))?;
344 if ty != Type::Number {
345 return err(ErrorCode::CanonUnit, rp, "only a number return is written as an object");
346 }
347 let Some(u) = schema::opt(rm, "unit") else {
348 return err(ErrorCode::CanonUnit, rp, "a number return declares its unit");
349 };
350 Returns { ty, unit: Some(unit_canonical(u, args, &member(rp, "unit"))?) }
351 }
352 _ => return err(ErrorCode::Schema, rp, "returns must be a type name or an object"),
353 };
354 Ok(Descriptor {
355 lang: lang.into(),
356 body: body.into(),
357 inputs: items.iter().map(|s| s.to_string()).collect(),
358 signature: Signature { args: decls, returns },
359 })
360}
361
362pub fn resolved_unit(descriptor: &[u8], cell: &[u8]) -> Result<Option<String>> {
368 let cell_v = json::parse(cell)?;
369 let desc_v = json::parse(descriptor)?;
370 resolved_unit_value(&desc_v, &cell_v)
371}
372
373pub fn resolved_unit_value(descriptor: &Value, cell: &Value) -> Result<Option<String>> {
375 json::check_value_depth(descriptor)?;
376 json::check_value_depth(cell)?;
377 let cell = Cell::from_value(cell)?;
378 let hash = crate::crypto::sha256_hex(&descriptor.to_jcs());
379 if hash != cell.fn_hash {
380 return err(ErrorCode::FnHashMismatch, "$.descriptor", "the descriptor does not hash to cell.fn_hash");
381 }
382 let d = Descriptor::from_value(descriptor)?;
383 resolve_with(&d, &cell)
384}
385
386pub fn resolve_with(d: &Descriptor, cell: &Cell) -> Result<Option<String>> {
388 let Some(unit) = &d.signature.returns.unit else { return Ok(None) };
389 let arg_value = |name: &str| -> Result<String> {
390 let p = member("$.cell.args", name);
391 match cell.args.get(name) {
392 None => err(
393 ErrorCode::ArgMissing,
394 &p,
395 format!("the unit refers to argument {name:?}, which the cell does not bind"),
396 ),
397 Some(Arg::Slice) => err(ErrorCode::Slice, &p, "a slice has no single unit"),
398 Some(Arg::Value(Scalar::String(s))) => Ok(s.clone()),
399 Some(Arg::Value(other)) => {
400 err(ErrorCode::ArgType, &p, format!("bound as {}, not string", other.ty().as_str()))
401 }
402 }
403 };
404 let side = |s: &RateSide| -> Result<String> {
405 match s {
406 RateSide::Currency(c) => Ok(c.clone()),
407 RateSide::Arg(n) => {
408 let v = arg_value(n)?;
409 if !lexical::is_currency(&v) {
410 return err(
411 ErrorCode::CanonUnit,
412 &member("$.cell.args", n),
413 format!("a rate side must be a currency code: {v:?}"),
414 );
415 }
416 Ok(v)
417 }
418 }
419 };
420 Ok(Some(match unit {
421 Unit::Literal(s) => s.clone(),
422 Unit::Arg(n) => {
423 let v = arg_value(n)?;
424 if !lexical::is_literal_unit(&v) {
425 return err(ErrorCode::CanonUnit, &member("$.cell.args", n), format!("not a unit: {v:?}"));
426 }
427 v
428 }
429 Unit::Rate { of, per } => {
430 let o = side(of)?;
431 let p = side(per)?;
432 format!("{o}/{p}")
433 }
434 }))
435}