ic-wire 0.3.4

Bounded canonical Postcard contracts for controlled Internet Computer endpoints
Documentation
//! Host-side TypeScript projection from the same schema used by Rust derives.
use crate::{Method, Wire, schema::Schema};
use alloc::{
    format,
    string::{String, ToString},
    vec::Vec,
};
use serde_json::{Value, json};

/// Shared bounded Postcard implementation. Write beside generated method modules
/// as `wire-runtime.ts`; it imports no SDK and owns no transport/auth policy.
pub const RUNTIME: &str = include_str!("runtime.ts");

/// A contract cannot be faithfully projected into a TypeScript module.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GenerationError {
    /// The method's static resource admission failed.
    Limits,
    /// Missing/custom metadata, duplicate identities, or unsupported widths.
    Schema,
    /// Metadata is too large, too deep, or outside exact JavaScript integer range.
    Complexity,
}

fn describe(
    schema: &Schema,
    depth: usize,
    remaining: &mut usize,
) -> Result<(Value, String), GenerationError> {
    if depth > 64 || *remaining == 0 {
        return Err(GenerationError::Complexity);
    }
    *remaining -= 1;
    let mut child = |schema| describe(schema, depth + 1, remaining);
    Ok(match schema {
        Schema::Opaque => return Err(GenerationError::Schema),
        Schema::Unit => (json!({"k":"unit"}), "null".into()),
        Schema::Bool => (json!({"k":"bool"}), "boolean".into()),
        Schema::Float(bits) => {
            if ![32, 64].contains(bits) {
                return Err(GenerationError::Schema);
            }
            (json!({"k":"float","bits":bits}), "number".into())
        }
        Schema::Unsigned(bits) | Schema::Signed(bits) => {
            if ![8, 16, 32, 64, 128].contains(bits) {
                return Err(GenerationError::Schema);
            }
            (
                json!({"k":if matches!(schema, Schema::Signed(_)) {"i"} else {"u"},"bits":bits}),
                if *bits <= 32 { "number" } else { "bigint" }.into(),
            )
        }
        Schema::Bytes(max) | Schema::Text(max) => {
            exact(*max)?;
            (
                json!({"k":if matches!(schema, Schema::Bytes(_)) {"bytes"} else {"text"},"max":max}),
                if matches!(schema, Schema::Bytes(_)) {
                    "Uint8Array"
                } else {
                    "string"
                }
                .into(),
            )
        }
        Schema::List(value, max) | Schema::Array(value, max) => {
            exact(*max)?;
            let (node, ty) = child(value)?;
            (
                json!({"k":if matches!(schema, Schema::List(..)) {"list"} else {"array"},"max":max,"item":node}),
                format!("ReadonlyArray<{ty}>"),
            )
        }
        Schema::Option(value) => {
            let (node, ty) = child(value)?;
            (
                json!({"k":"option","item":node}),
                format!("null | {{ readonly some: {ty} }}"),
            )
        }
        Schema::Result(ok, err) => {
            let (ok_node, ok_ty) = child(ok)?;
            let (err_node, err_ty) = child(err)?;
            (
                json!({"k":"result","ok":ok_node,"err":err_node}),
                format!("{{ readonly ok: {ok_ty} }} | {{ readonly err: {err_ty} }}"),
            )
        }
        Schema::Record(fields) => {
            let mut nodes = Vec::new();
            let mut types = Vec::new();
            for (index, field) in fields.iter().enumerate() {
                if fields[..index]
                    .iter()
                    .any(|f| f.name == field.name || f.id >= field.id)
                {
                    return Err(GenerationError::Schema);
                }
                let (node, ty) = child(field.value)?;
                nodes.push(json!({"name":field.name,"id":field.id,"value":node}));
                types.push(format!("readonly {}: {ty}", json!(field.name)));
            }
            (
                json!({"k":"record","fields":nodes}),
                format!("{{ {} }}", types.join("; ")),
            )
        }
        Schema::Enum(variants) => {
            if variants.is_empty() {
                return Err(GenerationError::Schema);
            }
            let mut variants: Vec<_> = variants.iter().collect();
            variants.sort_by_key(|v| v.id);
            let mut nodes = Vec::new();
            let mut types = Vec::new();
            for (index, variant) in variants.iter().enumerate() {
                if variants[..index]
                    .iter()
                    .any(|v| v.name == variant.name || v.id == variant.id)
                {
                    return Err(GenerationError::Schema);
                }
                let (node, ty) = child(variant.value)?;
                nodes.push(json!({"name":variant.name,"id":variant.id,"value":node}));
                types.push(format!(
                    "{{ readonly tag: {}; readonly value: {ty} }}",
                    json!(variant.name)
                ));
            }
            (json!({"k":"enum","variants":nodes}), types.join(" | "))
        }
    })
}

fn exact(value: usize) -> Result<(), GenerationError> {
    if value as u128 > (1_u128 << 53) - 1 {
        Err(GenerationError::Complexity)
    } else {
        Ok(())
    }
}

/// Generate one typed method module. Write RUNTIME as `wire-runtime.ts` in the
/// same directory. Source field/variant names are JSON-escaped, never interpolated
/// as executable identifiers. Custom metadata is trusted like custom Wire impls.
pub fn generate<M: Method>() -> Result<String, GenerationError> {
    let request_bytes = crate::contract::admit::<M::Request>(M::REQUEST_LIMITS)
        .map_err(|_| GenerationError::Limits)?;
    let response_bytes = crate::contract::admit::<M::Response>(M::RESPONSE_LIMITS)
        .map_err(|_| GenerationError::Limits)?;
    for n in [
        request_bytes,
        response_bytes,
        M::REQUEST_LIMITS.nodes,
        M::REQUEST_LIMITS.depth,
        M::RESPONSE_LIMITS.nodes,
        M::RESPONSE_LIMITS.depth,
    ] {
        exact(n)?;
    }
    let (request, request_ty) = describe(&M::Request::SCHEMA, 0, &mut 4096)?;
    let (response, response_ty) = describe(&M::Response::SCHEMA, 0, &mut 4096)?;
    let metadata = json!({"id":M::ID.to_string(),"request":request,"response":response,"requestBytes":request_bytes,"responseBytes":response_bytes,"requestNodes":M::REQUEST_LIMITS.nodes,"responseNodes":M::RESPONSE_LIMITS.nodes,"requestDepth":M::REQUEST_LIMITS.depth,"responseDepth":M::RESPONSE_LIMITS.depth});
    Ok(format!(
        "// Generated from IC Wire schema; edit the Rust contract, not this file.\nimport {{ createCodec }} from './wire-runtime.js';\nexport type Request = {request_ty};\nexport type Response = {response_ty};\nexport const methodName = {};\nconst codec = createCodec({metadata});\nexport const encodeRequest = (value: Request): Uint8Array => codec.encodeRequest(value);\nexport const decodeRequest = (bytes: Uint8Array): Request => codec.decodeRequest(bytes) as Request;\nexport const encodeResponse = (value: Response): Uint8Array => codec.encodeResponse(value);\nexport const decodeResponse = (bytes: Uint8Array): Response => codec.decodeResponse(bytes) as Response;\n",
        json!(M::NAME)
    ))
}