roam_codegen/targets/typescript/
mod.rs1pub mod client;
12pub mod decode;
13pub mod encode;
14pub mod http_client;
15pub mod schema;
16pub mod server;
17pub mod types;
18
19use crate::code_writer::CodeWriter;
20use roam_schema::{MethodDetail, ServiceDetail};
21
22pub use client::generate_client;
23pub use http_client::generate_http_client;
24pub use schema::generate_schemas;
25pub use server::generate_server;
26pub use types::{collect_named_types, generate_named_types};
27
28pub fn generate_method_ids(methods: &[MethodDetail]) -> String {
30 use crate::render::{fq_name, hex_u64};
31
32 let mut items = methods
33 .iter()
34 .map(|m| (fq_name(m), crate::method_id(m)))
35 .collect::<Vec<_>>();
36 items.sort_by(|a, b| a.0.cmp(&b.0));
37
38 let mut out = String::new();
39 out.push_str("// @generated by roam-codegen\n");
40 out.push_str("// This file defines canonical roam method IDs.\n\n");
41 out.push_str("export const METHOD_ID: Record<string, bigint> = {\n");
42 for (name, id) in items {
43 out.push_str(&format!(" \"{name}\": {}n,\n", hex_u64(id)));
44 }
45 out.push_str("} as const;\n");
46 out
47}
48
49pub fn generate_service(service: &ServiceDetail) -> String {
53 use crate::code_writer::CodeWriter;
54 use crate::{cw_writeln, render::hex_u64};
55 use heck::ToLowerCamelCase;
56
57 let mut output = String::new();
58 let mut w = CodeWriter::with_indent_spaces(&mut output, 2);
59
60 cw_writeln!(w, "// @generated by roam-codegen").unwrap();
62 cw_writeln!(
63 w,
64 "// DO NOT EDIT - regenerate with `cargo xtask codegen --typescript`"
65 )
66 .unwrap();
67 w.blank_line().unwrap();
68
69 generate_imports(service, &mut w);
71 w.blank_line().unwrap();
72
73 cw_writeln!(w, "export const METHOD_ID = {{").unwrap();
75 {
76 let _indent = w.indent();
77 for method in &service.methods {
78 let id = crate::method_id(method);
79 let method_name = method.method_name.to_lower_camel_case();
80 cw_writeln!(w, "{method_name}: {}n,", hex_u64(id)).unwrap();
81 }
82 }
83 cw_writeln!(w, "}} as const;").unwrap();
84 w.blank_line().unwrap();
85
86 let named_types = collect_named_types(service);
88 output.push_str(&generate_named_types(&named_types));
89
90 output.push_str(&generate_request_response_types(service, &named_types));
92
93 output.push_str(&generate_client(service));
95
96 output.push_str(&generate_server(service));
98
99 output.push_str(&generate_schemas(service));
101
102 output
103}
104
105fn generate_imports(service: &ServiceDetail, w: &mut CodeWriter<&mut String>) {
107 use crate::cw_writeln;
108 use roam_schema::{ShapeKind, classify_shape, is_rx, is_tx};
109
110 let has_streaming = service.methods.iter().any(|m| {
112 m.args.iter().any(|a| is_tx(a.ty) || is_rx(a.ty))
113 || is_tx(m.return_type)
114 || is_rx(m.return_type)
115 });
116
117 let has_fallible = service
119 .methods
120 .iter()
121 .any(|m| matches!(classify_shape(m.return_type), ShapeKind::Result { .. }));
122
123 cw_writeln!(w, "import type {{ MethodHandler, Connection, MessageTransport, DecodeResult, MethodSchema }} from \"@bearcove/roam-core\";").unwrap();
125
126 cw_writeln!(w, "import {{").unwrap();
128 {
129 let _indent = w.indent();
130 cw_writeln!(w, "encodeResultOk, encodeResultErr, encodeInvalidPayload,").unwrap();
131 cw_writeln!(
132 w,
133 "concat, encodeVarint, decodeVarintNumber, decodeRpcResult,"
134 )
135 .unwrap();
136 cw_writeln!(w, "encodeWithSchema, decodeWithSchema,").unwrap();
137 cw_writeln!(w, "encodeBool, decodeBool,").unwrap();
138 cw_writeln!(w, "encodeU8, decodeU8, encodeI8, decodeI8,").unwrap();
139 cw_writeln!(w, "encodeU16, decodeU16, encodeI16, decodeI16,").unwrap();
140 cw_writeln!(w, "encodeU32, decodeU32, encodeI32, decodeI32,").unwrap();
141 cw_writeln!(w, "encodeU64, decodeU64, encodeI64, decodeI64,").unwrap();
142 cw_writeln!(w, "encodeF32, decodeF32, encodeF64, decodeF64,").unwrap();
143 cw_writeln!(w, "encodeString, decodeString,").unwrap();
144 cw_writeln!(w, "encodeBytes, decodeBytes,").unwrap();
145 cw_writeln!(w, "encodeOption, decodeOption,").unwrap();
146 cw_writeln!(w, "encodeVec, decodeVec,").unwrap();
147 cw_writeln!(w, "encodeTuple2, decodeTuple2, encodeTuple3, decodeTuple3,").unwrap();
148 cw_writeln!(w, "encodeEnumVariant, decodeEnumVariant,").unwrap();
149 }
150 cw_writeln!(w, " helloExchangeInitiator, defaultHello,").unwrap();
151 cw_writeln!(w, "}} from \"@bearcove/roam-core\";").unwrap();
152
153 cw_writeln!(
155 w,
156 "import {{ connectWs, type WsTransport }} from \"@bearcove/roam-ws\";"
157 )
158 .unwrap();
159
160 if has_fallible {
162 cw_writeln!(w, "import {{ RpcError }} from \"@bearcove/roam-core\";").unwrap();
163 }
164
165 if has_streaming {
166 cw_writeln!(
167 w,
168 "import {{ Tx, Rx, createServerTx, createServerRx, bindChannels }} from \"@bearcove/roam-core\";"
169 )
170 .unwrap();
171 cw_writeln!(
172 w,
173 "import type {{ ChannelId, ChannelRegistry, TaskSender, BindingSerializers, Schema }} from \"@bearcove/roam-core\";"
174 )
175 .unwrap();
176 }
177}
178
179fn generate_request_response_types(
181 service: &ServiceDetail,
182 named_types: &[(String, &'static facet_core::Shape)],
183) -> String {
184 use heck::ToUpperCamelCase;
185 use std::collections::HashSet;
186 use types::ts_type;
187
188 let type_names: HashSet<&str> = named_types.iter().map(|(name, _)| name.as_str()).collect();
190
191 let mut out = String::new();
192 out.push_str("// Request/Response type aliases\n");
193
194 for method in &service.methods {
195 let method_name = method.method_name.to_upper_camel_case();
196 let request_name = format!("{method_name}Request");
197 let response_name = format!("{method_name}Response");
198
199 if !type_names.contains(request_name.as_str()) {
201 if method.args.is_empty() {
202 out.push_str(&format!("export type {request_name} = [];\n"));
203 } else if method.args.len() == 1 {
204 let ty = ts_type(method.args[0].ty);
205 out.push_str(&format!("export type {request_name} = [{ty}];\n"));
206 } else {
207 out.push_str(&format!("export type {request_name} = [\n"));
208 for arg in &method.args {
209 let ty = ts_type(arg.ty);
210 out.push_str(&format!(" {ty}, // {}\n", arg.name));
211 }
212 out.push_str("];\n");
213 }
214 }
215
216 if !type_names.contains(response_name.as_str()) {
218 let ret_ty = ts_type(method.return_type);
219 out.push_str(&format!("export type {response_name} = {ret_ty};\n"));
220 }
221
222 out.push('\n');
223 }
224
225 out
226}