webwire_cli/codegen/
ts.rs

1use crate::schema;
2
3struct Generator {
4    level: usize,
5    output: String,
6}
7
8impl Generator {
9    fn new() -> Self {
10        Self {
11            level: 0,
12            output: String::new(),
13        }
14    }
15    fn begin(&mut self, line: &str) {
16        self.line(line);
17        self.level += 1;
18    }
19    fn end(&mut self, line: &str) {
20        self.level -= 1;
21        if !line.is_empty() {
22            self.line(line);
23        }
24    }
25    fn line(&mut self, line: &str) {
26        if !line.is_empty() {
27            for _ in 0..self.level {
28                self.output += "    ";
29            }
30            self.output += line;
31        }
32        self.output += "\n";
33    }
34}
35
36impl From<Generator> for String {
37    fn from(gen: Generator) -> Self {
38        gen.output
39    }
40}
41
42pub fn gen(doc: &schema::Document) -> String {
43    let mut gen = Generator::new();
44    gen.line("// GENERATED CODE - DO NOT EDIT!");
45    gen.line("");
46    // XXX This should actually be an absolute import from the webwire
47    // npm package (which doesn't exist, yet.)
48    gen.line("import * as webwire from './webwire'");
49    gen.line("");
50    gen_namespace(&doc.ns, &mut gen);
51    gen.into()
52}
53
54fn gen_namespace(ns: &schema::Namespace, gen: &mut Generator) {
55    for type_ in ns.types.values() {
56        gen.line("");
57        gen_type(&*type_, gen);
58    }
59    for service in ns.services.values() {
60        gen.line("");
61        gen_service(service, gen);
62        gen.line("");
63        gen_consumer(ns, service, gen);
64    }
65    for child_ns in ns.namespaces.values() {
66        gen.line("");
67        gen.begin(&format!("export namespace {} {{", child_ns.name()));
68        gen_namespace(child_ns, gen);
69        gen.end("}");
70    }
71}
72
73fn gen_type(type_: &schema::UserDefinedType, gen: &mut Generator) {
74    match type_ {
75        schema::UserDefinedType::Enum(enum_) => gen_enum(&*enum_.borrow(), gen),
76        schema::UserDefinedType::Struct(struct_) => gen_struct(&*struct_.borrow(), gen),
77        schema::UserDefinedType::Fieldset(fieldset) => gen_fieldset(&*fieldset.borrow(), gen),
78    }
79}
80
81fn gen_enum(enum_: &schema::Enum, gen: &mut Generator) {
82    let enum_name = &enum_.fqtn.name;
83    if enum_.all_variants.is_empty() {
84        gen.line(&format!("export type _{}Variants = never", enum_name));
85        gen.line(&format!("export type {} = never", enum_.fqtn.name));
86        return;
87    }
88    gen.line(&format!(
89        "export type _{}Variants = {}",
90        enum_name,
91        enum_
92            .all_variants
93            .iter()
94            .map(|v| format!("\"{}\"", v.name))
95            .collect::<Vec<_>>()
96            .join(" | ")
97    ));
98    gen.begin(&format!("export type {} =", enum_.fqtn.name));
99    for variant in enum_.all_variants.iter() {
100        gen.line(&match &variant.value_type {
101            Some(value_type) => format!(
102                "| {{ [P in Exclude<_{}Variants, \"{}\">]?: never }} & {{ {}: {} }}",
103                enum_name,
104                variant.name,
105                variant.name,
106                gen_typeref(value_type)
107            ),
108            None => format!("| \"{}\"", variant.name),
109        });
110        // FIXME this is not the way enum variants should be generated. Actually
111        // a pattern matching where one value is required would be better.
112    }
113    gen.end("");
114}
115
116fn gen_struct(struct_: &schema::Struct, gen: &mut Generator) {
117    let generics = if struct_.generics.is_empty() {
118        "".to_string()
119    } else {
120        format!("<{}>", struct_.generics.join(", "))
121    };
122    gen.begin(&format!(
123        "export interface {}{} {{",
124        struct_.fqtn.name, generics
125    ));
126    for field in struct_.fields.iter() {
127        let opt = if field.optional { "?" } else { "" };
128        gen.line(&format!(
129            "{}{}: {},",
130            field.name,
131            opt,
132            gen_typeref(&field.type_)
133        ));
134    }
135    gen.end("}");
136}
137
138fn gen_fieldset(fieldset: &schema::Fieldset, gen: &mut Generator) {
139    let generics = if fieldset.generics.is_empty() {
140        "".to_string()
141    } else {
142        format!("<{}>", fieldset.generics.join(", "))
143    };
144    gen.begin(&format!(
145        "export interface {}{} {{",
146        fieldset.fqtn.name, generics
147    ));
148    for field in fieldset.fields.iter() {
149        // FIXME add support for optional fields
150        let opt = if field.optional { "?" } else { "" };
151        gen.line(&format!(
152            "{}{}: {},",
153            field.name,
154            opt,
155            gen_typeref(&field.field.as_ref().unwrap().type_)
156        ));
157    }
158    gen.end("}");
159}
160
161fn method_signature(method: &schema::Method) -> String {
162    let input = match &method.input {
163        Some(t) => format!("input: {}", gen_typeref(&t)),
164        None => String::new(),
165    };
166    let output = match &method.output {
167        Some(t) => gen_typeref(t),
168        None => "void".to_string(),
169    };
170    format!("{}({}): Promise<{}>", method.name, input, output)
171}
172
173fn gen_service(service: &schema::Service, gen: &mut Generator) {
174    gen.begin(&format!("export interface {} {{", service.name));
175    for method in service.methods.iter() {
176        gen.line(&format!("{},", method_signature(&method)));
177    }
178    gen.end("}");
179}
180
181fn gen_consumer(ns: &schema::Namespace, service: &schema::Service, gen: &mut Generator) {
182    gen.begin(&format!(
183        "export class {}Consumer implements {} {{",
184        service.name, service.name
185    ));
186    gen.line("_client: webwire.Client");
187    gen.begin("constructor(client: webwire.Client) {");
188    gen.line("this._client = client");
189    gen.end("}");
190    for method in service.methods.iter() {
191        gen.begin(&format!("async {} {{", method_signature(&method)));
192        let fqsn = if ns.path.is_empty() {
193            service.name.to_owned()
194        } else {
195            format!("{}.{}", ns.path.join("."), service.name)
196        };
197        let input_param = if method.input.is_some() {
198            ", input"
199        } else {
200            ""
201        };
202        gen.line(&format!(
203            "return await this._client.request('{}', '{}'{})",
204            fqsn, method.name, input_param,
205        ));
206        gen.end("}");
207    }
208    gen.end("}");
209}
210
211pub fn gen_typeref(type_: &schema::Type) -> String {
212    match type_ {
213        schema::Type::None => "null".to_string(),
214        schema::Type::Boolean => "boolean".to_string(),
215        schema::Type::Integer => "number".to_string(),
216        schema::Type::Float => "number".to_string(),
217        schema::Type::String => "string".to_string(),
218        schema::Type::UUID => "webwire.UUID".to_string(),
219        schema::Type::Date => "webwire.Date".to_string(),
220        schema::Type::Time => "webwire.Time".to_string(),
221        schema::Type::DateTime => "webwire.DateTime".to_string(),
222        schema::Type::Option(some) => format!("webwire.Option<{}>", gen_typeref(some)),
223        schema::Type::Result(ok, err) => {
224            format!("webwire.Result<{}, {}>", gen_typeref(ok), gen_typeref(err))
225        }
226        // complex types
227        schema::Type::Array(array) => format!("Array<{}>", gen_typeref(&array.item_type)),
228        schema::Type::Map(map) => format!(
229            "Map<{}, {}>",
230            gen_typeref(&map.key_type),
231            gen_typeref(&map.value_type)
232        ),
233        // named
234        schema::Type::Ref(typeref) => {
235            let typeref_fqtn = typeref.fqtn();
236            let fqtn = if typeref_fqtn.ns.is_empty() {
237                typeref_fqtn.name.clone()
238            } else {
239                let ns = typeref_fqtn.ns.join(".");
240                format!("{}.{}", ns, typeref_fqtn.name)
241            };
242            let generics = typeref.generics();
243            if !generics.is_empty() {
244                let generics = generics
245                    .iter()
246                    .map(gen_typeref)
247                    .collect::<Vec<_>>()
248                    .join(", ");
249                format!("{}<{}>", fqtn, generics)
250            } else {
251                fqtn
252            }
253        }
254        schema::Type::Builtin(name) => name.to_string(),
255    }
256}