use serde_json::{json, Value};
use thiserror::Error;
use super::schema::{ParamType, VariantCase};
#[derive(Debug, Error)]
pub enum EncodeError {
#[error("expected {expected} for a `{kind}` argument, got `{got}`")]
WrongShape {
kind: &'static str,
expected: &'static str,
got: String,
},
#[error("tuple arity mismatch: expected {expected} element(s), got {got}")]
TupleArity {
expected: usize,
got: usize,
},
#[error("missing record field `{0}`")]
MissingField(String),
#[error("unknown record field `{0}`")]
UnknownField(String),
#[error("unknown variant case `{0}`")]
UnknownCase(String),
#[error("variant value must be a single-key object naming the case")]
BadVariant,
}
fn shape_of(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
pub fn encode(param: &ParamType, value: &Value) -> Result<Value, EncodeError> {
marshal(param, value, false)
}
fn marshal(param: &ParamType, value: &Value, nested: bool) -> Result<Value, EncodeError> {
match param {
ParamType::Integer => match value {
Value::Number(_) | Value::String(_) => Ok(leaf("int", value, nested)),
other => Err(wrong_shape("integer", "number or decimal/hex string", other)),
},
ParamType::Boolean => match value {
Value::Bool(_) | Value::Number(_) | Value::String(_) => Ok(leaf("bool", value, nested)),
other => Err(wrong_shape("boolean", "bool", other)),
},
ParamType::Bytes => match value {
Value::String(_) | Value::Object(_) => Ok(leaf("bytes", value, nested)),
other => Err(wrong_shape("bytes", "hex string or bytes envelope", other)),
},
ParamType::Address => match value {
Value::String(_) => Ok(leaf("address", value, nested)),
other => Err(wrong_shape("address", "bech32 or hex string", other)),
},
ParamType::UtxoRef => match value {
Value::String(_) => Ok(leaf("utxoRef", value, nested)),
other => Err(wrong_shape("utxoRef", "txid#index string", other)),
},
ParamType::Unit => Ok(json!({ "struct": { "constructor": 0, "fields": [] } })),
ParamType::List(inner) => {
let items = value
.as_array()
.ok_or_else(|| wrong_shape("list", "array", value))?;
let encoded = items
.iter()
.map(|v| marshal(inner, v, true))
.collect::<Result<Vec<_>, _>>()?;
Ok(json!({ "list": encoded }))
}
ParamType::Tuple(elem_types) => {
let items = value
.as_array()
.ok_or_else(|| wrong_shape("tuple", "array", value))?;
if items.len() != elem_types.len() {
return Err(EncodeError::TupleArity {
expected: elem_types.len(),
got: items.len(),
});
}
let encoded = elem_types
.iter()
.zip(items)
.map(|(t, v)| marshal(t, v, true))
.collect::<Result<Vec<_>, _>>()?;
Ok(json!({ "tuple": encoded }))
}
ParamType::Map(value_type) => {
let obj = value
.as_object()
.ok_or_else(|| wrong_shape("map", "object", value))?;
let mut keys: Vec<&String> = obj.keys().collect();
keys.sort();
let pairs = keys
.into_iter()
.map(|k| Ok(json!([json!({ "string": k }), marshal(value_type, &obj[k], true)?])))
.collect::<Result<Vec<_>, EncodeError>>()?;
Ok(json!({ "map": pairs }))
}
ParamType::Record(fields) => Ok(json!({
"struct": { "constructor": 0, "fields": marshal_record_fields(fields, value)? }
})),
ParamType::Variant(cases) => marshal_variant(cases, value),
ParamType::Utxo | ParamType::AnyAsset | ParamType::Unknown(_) => Ok(value.clone()),
}
}
fn leaf(tag: &str, value: &Value, nested: bool) -> Value {
if nested {
json!({ tag: value })
} else {
value.clone()
}
}
fn wrong_shape(kind: &'static str, expected: &'static str, got: &Value) -> EncodeError {
EncodeError::WrongShape {
kind,
expected,
got: shape_of(got).to_string(),
}
}
fn marshal_record_fields(
fields: &[(String, ParamType)],
value: &Value,
) -> Result<Vec<Value>, EncodeError> {
let obj = value
.as_object()
.ok_or_else(|| wrong_shape("record", "object", value))?;
for key in obj.keys() {
if !fields.iter().any(|(name, _)| name == key) {
return Err(EncodeError::UnknownField(key.clone()));
}
}
fields
.iter()
.map(|(name, ty)| {
let field_value = obj
.get(name)
.ok_or_else(|| EncodeError::MissingField(name.clone()))?;
marshal(ty, field_value, true)
})
.collect()
}
fn marshal_variant(cases: &[VariantCase], value: &Value) -> Result<Value, EncodeError> {
let obj = value.as_object().ok_or(EncodeError::BadVariant)?;
if obj.len() != 1 {
return Err(EncodeError::BadVariant);
}
let (tag, payload) = obj.iter().next().expect("one entry");
let index = cases
.iter()
.position(|c| &c.tag == tag)
.ok_or_else(|| EncodeError::UnknownCase(tag.clone()))?;
let fields = match &*cases[index].fields {
ParamType::Record(field_types) => marshal_record_fields(field_types, payload)?,
other => vec![marshal(other, payload, true)?],
};
Ok(json!({ "struct": { "constructor": index, "fields": fields } }))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
fn param_type(schema: &Value, components: &HashMap<String, Value>) -> ParamType {
ParamType::from_json_schema(schema, components)
}
fn wire_vectors() -> Value {
let manifest = env!("CARGO_MANIFEST_DIR");
let candidates = [
format!("{manifest}/tests/fixtures/wire-vectors.json"),
format!("{manifest}/../../sdk-spec/test-vectors/complex-types/wire-vectors.json"),
format!("{manifest}/../../../sdks/sdk-spec/test-vectors/complex-types/wire-vectors.json"),
];
for path in candidates {
if let Ok(contents) = std::fs::read_to_string(&path) {
return serde_json::from_str(&contents).expect("wire-vectors.json parses");
}
}
panic!("could not locate wire-vectors.json in any known path");
}
fn components(vectors: &Value) -> HashMap<String, Value> {
vectors["components"]
.as_object()
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
#[test]
fn encodes_all_accept_vectors() {
let vectors = wire_vectors();
let components = components(&vectors);
for vector in vectors["accept"].as_array().unwrap() {
let name = vector["name"].as_str().unwrap();
let param = param_type(&vector["schema"], &components);
let got = encode(¶m, &vector["value"])
.unwrap_or_else(|e| panic!("vector `{name}` failed to encode: {e}"));
assert_eq!(got, vector["tagged"], "vector `{name}` wire mismatch");
}
}
#[test]
fn rejects_all_reject_vectors() {
let vectors = wire_vectors();
let components = components(&vectors);
for vector in vectors["reject"].as_array().unwrap() {
let name = vector["name"].as_str().unwrap();
let param = param_type(&vector["schema"], &components);
let result = encode(¶m, &vector["value"]);
assert!(
result.is_err(),
"vector `{name}` should have been rejected, got {result:?}"
);
}
}
#[test]
fn record_field_order_follows_required_not_alphabetical() {
let schema = json!({
"type": "object",
"properties": {
"level": { "type": "integer" },
"tags": { "type": "array", "items": { "type": "integer" } }
},
"required": ["tags", "level"]
});
let param = param_type(&schema, &HashMap::new());
let got = encode(¶m, &json!({ "level": 7, "tags": [1, 2, 3] })).unwrap();
assert_eq!(
got,
json!({
"struct": {
"constructor": 0,
"fields": [{ "list": [{ "int": 1 }, { "int": 2 }, { "int": 3 }] }, { "int": 7 }]
}
})
);
}
#[test]
fn top_level_scalars_render_bare() {
let int = param_type(&json!({ "type": "integer" }), &HashMap::new());
assert_eq!(encode(&int, &json!(5)).unwrap(), json!(5));
let bytes = param_type(
&json!({ "$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes" }),
&HashMap::new(),
);
assert_eq!(encode(&bytes, &json!("cafe")).unwrap(), json!("cafe"));
}
#[test]
fn nested_scalars_are_tagged() {
let list = param_type(
&json!({ "type": "array", "items": { "type": "integer" } }),
&HashMap::new(),
);
assert_eq!(
encode(&list, &json!([5])).unwrap(),
json!({ "list": [{ "int": 5 }] })
);
}
}