use facet_core::{Facet, Shape};
use heck::ToLowerCamelCase;
use vox_types::{
Schema, SchemaHash, SchemaKind, ServiceDescriptor, ShapeKind, TypeRef, VariantPayload,
VariantSchema, VoxError, classify_shape,
};
pub fn generate_send_schema_table(service: &ServiceDescriptor) -> String {
use crate::render::hex_u64;
let service_name_lower = service.service_name.to_lower_camel_case();
let mut schema_ids_seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut all_schemas: Vec<Schema> = Vec::new();
fn extract_into(shape: &'static Shape, all_schemas: &mut Vec<Schema>) -> TypeRef<SchemaHash> {
let extracted = vox_types::extract_schemas(shape).expect("schema extraction");
let root = extracted.root.clone();
all_schemas.extend(extracted.schemas.iter().cloned());
root
}
fn type_id_of(type_ref: &TypeRef<SchemaHash>) -> SchemaHash {
match type_ref {
TypeRef::Concrete { type_id, .. } => *type_id,
TypeRef::Var { .. } => panic!("schema root cannot be a type variable"),
}
}
struct MethodSchemaInfo {
method_id: u64,
args_root: TypeRef<SchemaHash>,
response_root: TypeRef<SchemaHash>,
}
let mut method_schema_infos: Vec<MethodSchemaInfo> = Vec::new();
let result_template_root = extract_into(
<Result<bool, u32> as Facet<'static>>::SHAPE,
&mut all_schemas,
);
let result_type_id = type_id_of(&result_template_root);
let vox_error_template_root = extract_into(
<VoxError<std::convert::Infallible> as Facet<'static>>::SHAPE,
&mut all_schemas,
);
let vox_error_type_id = type_id_of(&vox_error_template_root);
for method in service.methods {
let method_id = crate::method_id(method);
let args_root = extract_into(method.args_shape, &mut all_schemas);
let (ok_ref, err_ref) = match classify_shape(method.return_shape) {
ShapeKind::Result { ok, err } => (
extract_into(ok, &mut all_schemas),
extract_into(err, &mut all_schemas),
),
_ => {
let ok = extract_into(method.return_shape, &mut all_schemas);
let err = extract_into(
<std::convert::Infallible as Facet<'static>>::SHAPE,
&mut all_schemas,
);
(ok, err)
}
};
let vox_error_ref = TypeRef::generic(vox_error_type_id, vec![err_ref]);
method_schema_infos.push(MethodSchemaInfo {
method_id,
args_root,
response_root: TypeRef::generic(result_type_id, vec![ok_ref, vox_error_ref]),
});
}
let mut deduped_schemas: Vec<&Schema> = Vec::new();
for schema in &all_schemas {
let id = schema.id.0;
if schema_ids_seen.insert(id) {
deduped_schemas.push(schema);
}
}
let mut out = String::new();
out.push_str("// Schema objects for wire schema exchange (TypeScript \u{2192} Rust)\n");
out.push_str("// Generated from Rust Facet shapes \u{2014} do not modify.\n");
out.push_str(&format!(
"export const {service_name_lower}_send_schemas: import(\"@bearcove/vox-core\").ServiceSendSchemas = {{\n"
));
out.push_str(" schemas: new Map<bigint, import(\"@bearcove/vox-postcard\").Schema>([\n");
for schema in &deduped_schemas {
let id_hex = hex_u64(schema.id.0);
let schema_ts = render_schema(schema);
out.push_str(&format!(" [{id_hex}n, {schema_ts}],\n"));
}
out.push_str(" ]),\n");
out.push_str(
" methods: new Map<bigint, import(\"@bearcove/vox-core\").MethodSendSchemas>([\n",
);
for info in &method_schema_infos {
let id_hex = hex_u64(info.method_id);
let args_root_ref_ts = render_type_ref(&info.args_root);
let response_root_ref_ts = render_type_ref(&info.response_root);
out.push_str(&format!(
" [{}n, {{ argsRootRef: {}, responseRootRef: {} }}],\n",
id_hex, args_root_ref_ts, response_root_ref_ts,
));
}
out.push_str(" ]),\n");
out.push_str("};\n\n");
out
}
pub fn generate_descriptor(service: &ServiceDescriptor) -> String {
use crate::render::hex_u64;
let mut out = String::new();
let service_name_lower = service.service_name.to_lower_camel_case();
for method in service.methods {
let method_name = method.method_name.to_lower_camel_case();
let id = crate::method_id(method);
let method_descriptor_name = format!("{service_name_lower}_{method_name}_method");
out.push_str(&format!(
"export const {method_descriptor_name}: MethodDescriptor = {{\n"
));
out.push_str(&format!(" name: '{method_name}',\n"));
out.push_str(&format!(" id: {}n,\n", hex_u64(id)));
out.push_str(&format!(
" retry: {{ persist: {}, idem: {} }},\n",
method.retry.persist, method.retry.idem
));
out.push_str("};\n\n");
}
out.push_str("// Service descriptor for runtime dispatch metadata\n");
out.push_str(&format!(
"export const {service_name_lower}_descriptor: ServiceDescriptor = {{\n"
));
out.push_str(&format!(" service_name: '{}',\n", service.service_name));
out.push_str(&format!(
" send_schemas: {service_name_lower}_send_schemas,\n"
));
out.push_str(" methods: new Map<bigint, MethodDescriptor>([\n");
for method in service.methods {
let method_name = method.method_name.to_lower_camel_case();
let method_descriptor_name = format!("{service_name_lower}_{method_name}_method");
out.push_str(&format!(
" [{method_descriptor_name}.id, {method_descriptor_name}],\n"
));
}
out.push_str(" ]),\n");
out.push_str("};\n\n");
out
}
use vox_types::{ChannelDirection, FieldSchema, PrimitiveType};
pub(crate) fn render_schema(schema: &Schema) -> String {
use crate::render::hex_u64;
let id_hex = hex_u64(schema.id.0);
let type_params = if schema.type_params.is_empty() {
"[]".to_string()
} else {
let params: Vec<String> = schema
.type_params
.iter()
.map(|p| format!("'{}'", p.as_str()))
.collect();
format!("[{}]", params.join(", "))
};
let kind = render_schema_kind(&schema.kind);
format!("{{ id: {id_hex}n, type_params: {type_params}, kind: {kind} }}")
}
fn render_schema_kind(kind: &SchemaKind) -> String {
match kind {
SchemaKind::Struct { name, fields } => {
let fields_ts: Vec<String> = fields.iter().map(render_field_schema).collect();
format!(
"{{ tag: 'struct', name: '{}', fields: [{}] }}",
name,
fields_ts.join(", ")
)
}
SchemaKind::Enum { name, variants } => {
let variants_ts: Vec<String> = variants.iter().map(render_variant_schema).collect();
format!(
"{{ tag: 'enum', name: '{}', variants: [{}] }}",
name,
variants_ts.join(", ")
)
}
SchemaKind::Tuple { elements } => {
let elems: Vec<String> = elements.iter().map(render_type_ref).collect();
format!("{{ tag: 'tuple', elements: [{}] }}", elems.join(", "))
}
SchemaKind::List { element } => {
format!("{{ tag: 'list', element: {} }}", render_type_ref(element))
}
SchemaKind::Map { key, value } => {
format!(
"{{ tag: 'map', key: {}, value: {} }}",
render_type_ref(key),
render_type_ref(value)
)
}
SchemaKind::Array { element, length } => {
format!(
"{{ tag: 'array', element: {}, length: {} }}",
render_type_ref(element),
length
)
}
SchemaKind::Option { element } => {
format!("{{ tag: 'option', element: {} }}", render_type_ref(element))
}
SchemaKind::Channel { direction, element } => {
let dir = match direction {
ChannelDirection::Tx => "tx",
ChannelDirection::Rx => "rx",
};
format!(
"{{ tag: 'channel', direction: '{}', element: {} }}",
dir,
render_type_ref(element)
)
}
SchemaKind::Primitive { primitive_type } => {
format!(
"{{ tag: 'primitive', primitive_type: '{}' }}",
render_primitive_type(primitive_type)
)
}
}
}
pub(crate) fn render_type_ref(type_ref: &TypeRef) -> String {
use crate::render::hex_u64;
match type_ref {
TypeRef::Concrete { type_id, args } => {
let id_hex = hex_u64(type_id.0);
let args_ts: Vec<String> = args.iter().map(render_type_ref).collect();
format!(
"{{ tag: 'concrete', type_id: {id_hex}n, args: [{}] }}",
args_ts.join(", ")
)
}
TypeRef::Var { name } => {
format!("{{ tag: 'var', name: '{}' }}", name.as_str())
}
}
}
fn render_field_schema(field: &FieldSchema) -> String {
format!(
"{{ name: '{}', type_ref: {}, required: {} }}",
field.name,
render_type_ref(&field.type_ref),
field.required
)
}
fn render_variant_schema(variant: &VariantSchema) -> String {
format!(
"{{ name: '{}', index: {}, payload: {} }}",
variant.name,
variant.index,
render_variant_payload(&variant.payload)
)
}
fn render_variant_payload(payload: &VariantPayload) -> String {
match payload {
VariantPayload::Unit => "{ tag: 'unit' }".to_string(),
VariantPayload::Newtype { type_ref } => {
format!(
"{{ tag: 'newtype', type_ref: {} }}",
render_type_ref(type_ref)
)
}
VariantPayload::Tuple { types } => {
let types_ts: Vec<String> = types.iter().map(render_type_ref).collect();
format!("{{ tag: 'tuple', types: [{}] }}", types_ts.join(", "))
}
VariantPayload::Struct { fields } => {
let fields_ts: Vec<String> = fields.iter().map(render_field_schema).collect();
format!("{{ tag: 'struct', fields: [{}] }}", fields_ts.join(", "))
}
}
}
fn render_primitive_type(pt: &PrimitiveType) -> &'static str {
match pt {
PrimitiveType::Bool => "bool",
PrimitiveType::U8 => "u8",
PrimitiveType::U16 => "u16",
PrimitiveType::U32 => "u32",
PrimitiveType::U64 => "u64",
PrimitiveType::U128 => "u128",
PrimitiveType::I8 => "i8",
PrimitiveType::I16 => "i16",
PrimitiveType::I32 => "i32",
PrimitiveType::I64 => "i64",
PrimitiveType::I128 => "i128",
PrimitiveType::F32 => "f32",
PrimitiveType::F64 => "f64",
PrimitiveType::Char => "char",
PrimitiveType::String => "string",
PrimitiveType::Unit => "unit",
PrimitiveType::Never => "never",
PrimitiveType::Bytes => "bytes",
PrimitiveType::Payload => "payload",
}
}