Skip to main content

myko/
typegen_module.rs

1//! Language-neutral declarations for generated typegen modules.
2//!
3//! Downstream crates build this small IR from their Rust registrations.  The
4//! target-language renderer lives in Myko, so downstream code never has to
5//! assemble (or escape) TypeScript source text.
6
7use std::any::TypeId;
8
9use crate::codegen_types::RegisteredType;
10
11/// A typed owner for a generated module output path.
12pub trait TypegenModulePath {
13    const PATH: &'static str;
14}
15
16/// A generated typegen module.
17#[derive(Clone, Debug, PartialEq)]
18pub struct TypegenModule {
19    /// Output path, relative to the bindings directory. A missing `.ts`
20    /// extension is added by the TypeScript renderer.
21    pub path: String,
22    /// Declarations, in source order.
23    pub declarations: Vec<Declaration>,
24    /// Whether index modules should be generated for parent directories.
25    pub barrels: bool,
26    /// Registered types re-exported from this module.
27    pub registered_reexports: Vec<TypeId>,
28}
29
30impl TypegenModule {
31    /// Create an empty module at the path owned by a typed marker.
32    #[must_use]
33    pub fn new_typed<M: TypegenModulePath>() -> Self {
34        Self::new(M::PATH)
35    }
36
37    /// Create an empty module. Parent-directory barrels are enabled by default.
38    pub fn new(path: impl Into<String>) -> Self {
39        Self {
40            path: path.into(),
41            declarations: Vec::new(),
42            barrels: true,
43            registered_reexports: Vec::new(),
44        }
45    }
46
47    /// Re-export a registered generated type from this module.
48    #[must_use]
49    pub fn reexport_type<T: RegisteredType>(mut self) -> Self {
50        let type_id = TypeId::of::<T>();
51        if !self.registered_reexports.contains(&type_id) {
52            self.registered_reexports.push(type_id);
53        }
54        self
55    }
56
57    /// Append a declaration.
58    #[must_use]
59    pub fn declare(mut self, declaration: Declaration) -> Self {
60        self.declarations.push(declaration);
61        self
62    }
63
64    /// Enable or disable generated parent-directory barrels.
65    #[must_use]
66    pub const fn with_barrels(mut self, barrels: bool) -> Self {
67        self.barrels = barrels;
68        self
69    }
70
71    /// Append the conventional constants, array, index, and finder for a registry.
72    #[must_use]
73    pub fn declare_registry(mut self, registry: Registry) -> Self {
74        self.declarations.extend(registry.into_declarations());
75        self
76    }
77}
78
79/// One named value in a generated registry.
80#[derive(Clone, Debug, PartialEq)]
81pub struct RegistryEntry {
82    pub name: String,
83    pub key: Option<Value>,
84    pub value: Value,
85}
86
87impl RegistryEntry {
88    pub fn new(name: impl Into<String>, value: Value) -> Self {
89        Self {
90            name: name.into(),
91            key: None,
92            value,
93        }
94    }
95
96    pub fn keyed(name: impl Into<String>, key: impl Into<Value>, value: Value) -> Self {
97        Self {
98            name: name.into(),
99            key: Some(key.into()),
100            value,
101        }
102    }
103}
104
105/// Centralized public symbol names for a generated registry.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct RegistryNames {
108    pub all: String,
109    pub index: String,
110    pub find: String,
111    pub parameter: String,
112}
113
114impl RegistryNames {
115    pub fn new(all: impl Into<String>, index: impl Into<String>, find: impl Into<String>) -> Self {
116        Self {
117            all: all.into(),
118            index: index.into(),
119            find: find.into(),
120            parameter: "key".into(),
121        }
122    }
123
124    #[must_use]
125    pub fn with_parameter(mut self, parameter: impl Into<String>) -> Self {
126        self.parameter = parameter.into();
127        self
128    }
129}
130
131/// The common generated registry shape: named constants, an all-values array,
132/// a key-field index, and a finder function.
133#[derive(Clone, Debug, PartialEq)]
134pub struct Registry {
135    pub entries: Vec<RegistryEntry>,
136    pub value_type: Type,
137    pub key_field: String,
138    pub all_name: String,
139    pub index_name: String,
140    pub find_name: String,
141    pub parameter: String,
142    pub key_type: Type,
143}
144
145impl Registry {
146    #[must_use]
147    pub fn for_registered<T: RegisteredType>(names: RegistryNames) -> Self {
148        Self {
149            entries: Vec::new(),
150            value_type: Type::registered::<T>(),
151            key_field: String::new(),
152            all_name: names.all,
153            index_name: names.index,
154            find_name: names.find,
155            parameter: names.parameter,
156            key_type: Type::String,
157        }
158    }
159
160    pub fn new(
161        value_type: Type,
162        key_field: impl Into<String>,
163        all_name: impl Into<String>,
164        index_name: impl Into<String>,
165        find_name: impl Into<String>,
166    ) -> Self {
167        Self {
168            entries: Vec::new(),
169            value_type,
170            key_field: key_field.into(),
171            all_name: all_name.into(),
172            index_name: index_name.into(),
173            find_name: find_name.into(),
174            parameter: "key".into(),
175            key_type: Type::String,
176        }
177    }
178
179    #[must_use]
180    pub fn entry(mut self, entry: RegistryEntry) -> Self {
181        self.entries.push(entry);
182        self
183    }
184
185    #[must_use]
186    pub fn with_parameter(mut self, parameter: impl Into<String>) -> Self {
187        self.parameter = parameter.into();
188        self
189    }
190
191    #[must_use]
192    pub fn with_key_type(mut self, key_type: Type) -> Self {
193        self.key_type = key_type;
194        self
195    }
196
197    fn into_declarations(self) -> Vec<Declaration> {
198        let mut declarations = self
199            .entries
200            .iter()
201            .map(|entry| {
202                Declaration::constant(
203                    entry.name.clone(),
204                    self.value_type.clone(),
205                    entry.value.clone(),
206                )
207            })
208            .collect::<Vec<_>>();
209        declarations.push(Declaration::constant(
210            self.all_name.clone(),
211            Type::array(self.value_type.clone()),
212            Value::Array(
213                self.entries
214                    .iter()
215                    .map(|entry| Value::reference(entry.name.clone()))
216                    .collect(),
217            ),
218        ));
219        let keyed_entries = self
220            .entries
221            .iter()
222            .map(|entry| entry.key.clone().map(|key| (key, entry.name.clone())))
223            .collect::<Option<Vec<_>>>();
224        if let Some(entries) = keyed_entries {
225            declarations.push(Declaration::KeyedIndex {
226                name: self.index_name.clone(),
227                entries,
228                value_type: self.value_type.clone(),
229            });
230        } else {
231            declarations.push(Declaration::Index {
232                name: self.index_name.clone(),
233                source: self.all_name,
234                key_field: self.key_field,
235                value_type: self.value_type.clone(),
236            });
237        }
238        declarations.push(Declaration::Find {
239            name: self.find_name,
240            index: self.index_name,
241            parameter: self.parameter,
242            key_type: self.key_type,
243            value_type: self.value_type,
244        });
245        declarations
246    }
247}
248
249/// A type in the portable SDK-module IR.
250#[derive(Clone, Debug, PartialEq, Eq)]
251pub enum Type {
252    Registered(TypeId),
253    String,
254    Boolean,
255    Number,
256    Named(String),
257    Array(Box<Self>),
258    Optional(Box<Self>),
259    StringUnion(Vec<String>),
260    Object(Vec<Field>),
261    Record(Box<Self>, Box<Self>),
262}
263
264impl Type {
265    #[must_use]
266    pub const fn registered<T: RegisteredType>() -> Self {
267        Self::Registered(TypeId::of::<T>())
268    }
269
270    pub fn named(name: impl Into<String>) -> Self {
271        Self::Named(name.into())
272    }
273
274    #[must_use]
275    pub fn array(item: Self) -> Self {
276        Self::Array(Box::new(item))
277    }
278
279    #[must_use]
280    pub fn optional(inner: Self) -> Self {
281        Self::Optional(Box::new(inner))
282    }
283
284    #[must_use]
285    pub fn record(key: Self, value: Self) -> Self {
286        Self::Record(Box::new(key), Box::new(value))
287    }
288}
289
290/// A field in an object type.
291#[derive(Clone, Debug, PartialEq, Eq)]
292pub struct Field {
293    pub name: String,
294    pub ty: Type,
295    pub optional: bool,
296}
297
298impl Field {
299    pub fn new(name: impl Into<String>, ty: Type) -> Self {
300        Self {
301            name: name.into(),
302            ty,
303            optional: false,
304        }
305    }
306
307    #[must_use]
308    pub const fn optional(mut self) -> Self {
309        self.optional = true;
310        self
311    }
312}
313
314/// A data value or reference used by a declaration.
315#[derive(Clone, Debug, PartialEq)]
316pub enum Value {
317    Null,
318    String(String),
319    Bool(bool),
320    Integer(i64),
321    Unsigned(u64),
322    Float(f64),
323    Reference(String),
324    Array(Vec<Self>),
325    Object(Vec<(String, Self)>),
326}
327
328impl Value {
329    /// Convert any serializable Rust value into the portable module IR.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error when serialization fails or produces an unrepresentable number.
334    pub fn from_serializable(value: &impl serde::Serialize) -> serde_json::Result<Self> {
335        Self::try_from(serde_json::to_value(value)?)
336    }
337
338    pub fn string(value: impl Into<String>) -> Self {
339        Self::String(value.into())
340    }
341
342    pub fn reference(name: impl Into<String>) -> Self {
343        Self::Reference(name.into())
344    }
345
346    pub fn object(entries: impl IntoIterator<Item = (impl Into<String>, Self)>) -> Self {
347        Self::Object(
348            entries
349                .into_iter()
350                .map(|(key, value)| (key.into(), value))
351                .collect(),
352        )
353    }
354}
355
356impl TryFrom<serde_json::Value> for Value {
357    type Error = serde_json::Error;
358
359    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
360        Ok(match value {
361            serde_json::Value::Null => Self::Null,
362            serde_json::Value::Bool(value) => Self::Bool(value),
363            serde_json::Value::String(value) => Self::String(value),
364            serde_json::Value::Number(value) => {
365                if let Some(value) = value.as_i64() {
366                    Self::Integer(value)
367                } else if let Some(value) = value.as_u64() {
368                    Self::Unsigned(value)
369                } else {
370                    Self::Float(value.as_f64().ok_or_else(|| {
371                        <serde_json::Error as serde::de::Error>::custom(
372                            "JSON number cannot be represented in typegen IR",
373                        )
374                    })?)
375                }
376            }
377            serde_json::Value::Array(values) => Self::Array(
378                values
379                    .into_iter()
380                    .map(Self::try_from)
381                    .collect::<Result<_, _>>()?,
382            ),
383            serde_json::Value::Object(entries) => {
384                let mut entries = entries
385                    .into_iter()
386                    .map(|(key, value)| Ok((key, Self::try_from(value)?)))
387                    .collect::<Result<Vec<_>, serde_json::Error>>()?;
388                entries.sort_by(|(left, _), (right, _)| left.cmp(right));
389                Self::Object(entries)
390            }
391        })
392    }
393}
394
395impl From<&str> for Value {
396    fn from(value: &str) -> Self {
397        Self::string(value)
398    }
399}
400
401impl From<String> for Value {
402    fn from(value: String) -> Self {
403        Self::String(value)
404    }
405}
406
407impl From<bool> for Value {
408    fn from(value: bool) -> Self {
409        Self::Bool(value)
410    }
411}
412
413/// A leaf value accepted in a generated predicate.
414#[derive(Clone, Debug, PartialEq, Eq)]
415pub enum Operand {
416    ParameterField(String),
417    String(String),
418    Bool(bool),
419}
420
421/// A portable boolean predicate.
422#[derive(Clone, Debug, PartialEq, Eq)]
423pub enum Predicate {
424    Equal(Operand, Operand),
425    NotEqual(Operand, Operand),
426    And(Vec<Self>),
427    Or(Vec<Self>),
428}
429
430/// A declaration supported by the SDK renderer.
431#[derive(Clone, Debug, PartialEq)]
432pub enum Declaration {
433    Import {
434        names: Vec<String>,
435        from: String,
436        type_only: bool,
437    },
438    TypeAlias {
439        name: String,
440        doc: Option<String>,
441        ty: Type,
442    },
443    Const {
444        name: String,
445        doc: Option<String>,
446        ty: Option<Type>,
447        value: Value,
448        immutable: bool,
449        satisfies: Option<Type>,
450    },
451    FilteredArray {
452        name: String,
453        source: String,
454        parameter: String,
455        predicate: Predicate,
456    },
457    Index {
458        name: String,
459        source: String,
460        key_field: String,
461        value_type: Type,
462    },
463    KeyedIndex {
464        name: String,
465        entries: Vec<(Value, String)>,
466        value_type: Type,
467    },
468    Find {
469        name: String,
470        index: String,
471        parameter: String,
472        key_type: Type,
473        value_type: Type,
474    },
475    LookupOr {
476        name: String,
477        index: String,
478        parameter: String,
479        key_type: Type,
480        value_type: Type,
481        fallback: Value,
482    },
483}
484
485impl Declaration {
486    /// Import generated values or types from another module.
487    pub fn import(
488        names: impl IntoIterator<Item = impl Into<String>>,
489        from: impl Into<String>,
490    ) -> Self {
491        Self::Import {
492            names: names.into_iter().map(Into::into).collect(),
493            from: from.into(),
494            type_only: false,
495        }
496    }
497
498    /// Import generated types from another module.
499    pub fn import_type(
500        names: impl IntoIterator<Item = impl Into<String>>,
501        from: impl Into<String>,
502    ) -> Self {
503        Self::Import {
504            names: names.into_iter().map(Into::into).collect(),
505            from: from.into(),
506            type_only: true,
507        }
508    }
509
510    pub fn type_alias(name: impl Into<String>, ty: Type) -> Self {
511        Self::TypeAlias {
512            name: name.into(),
513            doc: None,
514            ty,
515        }
516    }
517
518    pub fn constant(name: impl Into<String>, ty: Type, value: Value) -> Self {
519        Self::Const {
520            name: name.into(),
521            doc: None,
522            ty: Some(ty),
523            value,
524            immutable: false,
525            satisfies: None,
526        }
527    }
528
529    pub fn inferred_constant(name: impl Into<String>, value: Value) -> Self {
530        Self::Const {
531            name: name.into(),
532            doc: None,
533            ty: None,
534            value,
535            immutable: false,
536            satisfies: None,
537        }
538    }
539
540    /// Attach documentation to declarations which support it.
541    #[must_use]
542    pub fn documented(mut self, doc: impl Into<String>) -> Self {
543        match &mut self {
544            Self::TypeAlias { doc: target, .. } | Self::Const { doc: target, .. } => {
545                *target = Some(doc.into());
546            }
547            _ => {}
548        }
549        self
550    }
551
552    /// Render a constant with a const assertion.
553    #[must_use]
554    pub const fn immutable(mut self) -> Self {
555        if let Self::Const { immutable, .. } = &mut self {
556            *immutable = true;
557        }
558        self
559    }
560
561    /// Render a constant with a target-language `satisfies` check.
562    #[must_use]
563    pub fn satisfies(mut self, ty: Type) -> Self {
564        if let Self::Const { satisfies, .. } = &mut self {
565            *satisfies = Some(ty);
566        }
567        self
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use serde::Serialize;
574
575    use super::{Declaration, Registry, RegistryEntry, RegistryNames, TypegenModule, Value};
576
577    #[derive(Serialize)]
578    struct Example<'a> {
579        name: &'a str,
580        optional: Option<bool>,
581        count: u64,
582    }
583
584    #[test]
585    fn converts_serializable_values_without_redeclaring_their_shape() {
586        let value = Value::from_serializable(&Example {
587            name: "example",
588            optional: None,
589            count: u64::MAX,
590        });
591        assert!(value.is_ok(), "example should serialize");
592        let Ok(value) = value else {
593            return;
594        };
595
596        assert_eq!(
597            value,
598            Value::Object(vec![
599                ("count".into(), Value::Unsigned(u64::MAX)),
600                ("name".into(), Value::String("example".into())),
601                ("optional".into(), Value::Null),
602            ])
603        );
604    }
605
606    #[test]
607    fn registry_appends_constants_array_index_and_finder() {
608        let module = TypegenModule::new("registry").declare_registry(
609            Registry::for_registered::<String>(RegistryNames::new(
610                "allEntries",
611                "entriesByKey",
612                "findEntry",
613            ))
614            .entry(RegistryEntry::keyed(
615                "FIRST",
616                "first",
617                Value::object([("value", "example".into())]),
618            )),
619        );
620
621        assert_eq!(module.declarations.len(), 4);
622        assert!(matches!(
623            module.declarations.get(2),
624            Some(Declaration::KeyedIndex { name, entries, .. })
625                if name == "entriesByKey"
626                    && entries == &vec![(Value::string("first"), "FIRST".into())]
627        ));
628        assert!(matches!(
629            module.declarations.get(3),
630            Some(Declaration::Find { name, index, .. })
631                if name == "findEntry" && index == "entriesByKey"
632        ));
633    }
634}