Skip to main content

alux_jsonrpc_typescript/
client.rs

1//! The interpreter: a program, read as a client module.
2
3use crate::TsParams;
4use alux_ext::{ApplyAlg, HandlerContextAlg};
5use alux_jsonrpc::{JsonRpcAlg, JsonRpcFallibleAlg, JsonRpcMethodAlg, OutcomeAlg};
6use alux_shape::{ShapeOf, Spelling, words_of};
7use alux_shape_typescript::{TsShape, TsType};
8use std::collections::BTreeMap;
9
10/// The package that interprets a program: what turns one into a client, installed once.
11///
12/// A generated module imports `method` from here rather than restating it, so a surface and what
13/// reads a surface are upgraded separately — which is the whole reason a program is a value.
14pub const RUNTIME_PACKAGE: &str = "@alux-network/api";
15
16/// Owns a shape's declarations, so a module can keep them.
17fn owned(shape: &TsType) -> Vec<(String, String)> {
18    shape.declarations().map(|(name, declaration)| (name.to_owned(), declaration.to_owned())).collect()
19}
20
21/// A client module: the declarations its calls depend on, and the calls themselves.
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct TsModule {
24    declarations: BTreeMap<String, String>,
25    entries: BTreeMap<String, String>,
26}
27
28impl TsModule {
29    /// Writes the module: every declaration a call depends on, then the program the calls form.
30    #[must_use]
31    pub fn render(&self) -> String {
32        let declarations: Vec<&str> = self.declarations.values().map(String::as_str).collect();
33        let entries: Vec<String> = self.entries.values().map(|entry| format!("  {entry},")).collect();
34        let program = format!("export const program = {{\n{}\n}} as const", entries.join("\n"));
35
36        if declarations.is_empty() { program } else { format!("{}\n\n{program}", declarations.join("\n\n")) }
37    }
38
39    /// The calls this module states, by the method name each answers to.
40    #[must_use]
41    pub fn method_names(&self) -> Vec<&str> {
42        self.entries.keys().map(String::as_str).collect()
43    }
44}
45
46/// Interprets a JSON-RPC program as a TypeScript client module.
47#[derive(Debug, Clone, Copy)]
48pub struct TsClient {
49    shapes: TsShape,
50    members: Spelling,
51}
52
53impl TsClient {
54    /// Emits a client whose member names are spelled this way.
55    #[must_use]
56    pub fn new(members: Spelling) -> Self {
57        Self { shapes: TsShape::new(members), members }
58    }
59
60    /// States one call: its name, the parameters it takes, and what it answers with.
61    /// Renders one method entry.
62    ///
63    /// `labels` name the parameters for whoever writes the call, and every method has them. `wire`
64    /// names them for the request document, which only a method decoded from a parameter object does.
65    fn method(
66        &self,
67        name: &'static str,
68        params: &[TsType],
69        labels: &[&str],
70        wire: &[&str],
71        answer: &TsType,
72    ) -> TsModule {
73        let mut declarations = BTreeMap::new();
74        let mut written = Vec::new();
75
76        for (index, param) in params.iter().enumerate() {
77            declarations.extend(owned(param));
78
79            // Every parameter the operation named is labelled, however the wire carries it.
80            written.push(match labels.get(index) {
81                // An argument name arrives as it was authored, so its words are read out of it.
82                Some(label) => format!("{}: {}", self.members.spell(&words_of(label)), param.expr()),
83                None => param.expr().to_owned(),
84            });
85        }
86
87        declarations.extend(owned(answer));
88
89        // The name a method answers to is already an identifier, so a caller writes `api.{name}(…)`
90        // rather than indexing a quoted key. Two namespaces cannot collide, since the whole name is
91        // kept rather than a stem of it.
92        let entry = format!(
93            "{name}: method<[{}], {}>(\"{name}\", [{}])",
94            written.join(", "),
95            answer.expr(),
96            wire.iter().map(|name| format!("\"{name}\"")).collect::<Vec<_>>().join(", "),
97        );
98
99        TsModule { declarations, entries: BTreeMap::from([(name.to_owned(), entry)]) }
100    }
101}
102
103impl JsonRpcAlg for TsClient {
104    type Methods = TsModule;
105
106    fn jsonrpc_empty(&self) -> TsModule {
107        TsModule::default()
108    }
109
110    fn jsonrpc_merge(&self, left: TsModule, right: TsModule) -> TsModule {
111        let mut merged = left;
112        merged.declarations.extend(right.declarations);
113        merged.entries.extend(right.entries);
114
115        merged
116    }
117}
118
119impl<Context> HandlerContextAlg<Context> for TsClient
120where
121    Context: Send + Sync + 'static,
122{
123    // A client applies nothing, so the handle it names is only what the program's obligation asks
124    // for: an owned reference to the domain the operations read.
125    type Handle = std::sync::Arc<Context>;
126}
127
128impl<Handle, Args, Output> JsonRpcMethodAlg<Handle, Args, Output> for TsClient
129where
130    Args: TsParams,
131    Output: ShapeOf<TsShape, Shape = TsType>,
132{
133    fn finish_jsonrpc_positional_method<Handler>(
134        &self,
135        name: &'static str,
136        arg_names: &'static [&'static str],
137        _handler: Handler,
138    ) -> TsModule
139    where
140        Handler: ApplyAlg<Handle, Args, Output = Output> + Send + Sync + 'static,
141    {
142        // The request document carries an array, so nothing names the parameters on the wire. The
143        // caller still gets the names, because the operation carries them either way.
144        self.method(name, &Args::params(&self.shapes), arg_names, &[], &Output::shape_of(&self.shapes))
145    }
146
147    fn finish_jsonrpc_named_method<Handler>(
148        &self,
149        name: &'static str,
150        arg_names: &'static [&'static str],
151        _handler: Handler,
152    ) -> TsModule
153    where
154        Handler: ApplyAlg<Handle, Args, Output = Output> + Send + Sync + 'static,
155    {
156        self.method(name, &Args::params(&self.shapes), arg_names, arg_names, &Output::shape_of(&self.shapes))
157    }
158}
159
160impl<Handle, Args, Output> JsonRpcFallibleAlg<Handle, Args, Output> for TsClient
161where
162    Args: TsParams,
163    Output: OutcomeAlg,
164    Output::Value: ShapeOf<TsShape, Shape = TsType>,
165{
166    fn finish_jsonrpc_positional_fallible<Handler>(
167        &self,
168        name: &'static str,
169        arg_names: &'static [&'static str],
170        _handler: Handler,
171    ) -> TsModule
172    where
173        Handler: ApplyAlg<Handle, Args, Output = Output> + Send + Sync + 'static,
174    {
175        // A failure reaches a caller as a rejected call, so only the value it answers with is a type.
176        self.method(name, &Args::params(&self.shapes), arg_names, &[], &Output::Value::shape_of(&self.shapes))
177    }
178
179    fn finish_jsonrpc_named_fallible<Handler>(
180        &self,
181        name: &'static str,
182        arg_names: &'static [&'static str],
183        _handler: Handler,
184    ) -> TsModule
185    where
186        Handler: ApplyAlg<Handle, Args, Output = Output> + Send + Sync + 'static,
187    {
188        self.method(name, &Args::params(&self.shapes), arg_names, arg_names, &Output::Value::shape_of(&self.shapes))
189    }
190}