Skip to main content

soothfast_sdk/
lib.rs

1//! Soothfast SDK engine: native client emitters over the spec IR.
2//!
3//! Consumes the same pre-dialect operations (`dialect::Operation` +
4//! `RouteShape`) the spec emitters render, and prints client source in
5//! another language instead of a spec document. Emission is deterministic:
6//! the same operations produce byte-identical output, so `sdk gen --check`
7//! can gate staleness the way `spec gen --check` does.
8
9pub mod envtemplate;
10pub mod lower;
11pub mod model;
12mod naming;
13mod python;
14pub mod target;
15mod typescript;
16
17use std::collections::BTreeMap;
18
19use soothfast_spec::dialect::{Info, Operation};
20
21/// A language the SDK engine can emit.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SdkKind {
24    Python,
25    TypeScript,
26}
27
28impl SdkKind {
29    /// Every kind, for listings and error messages.
30    pub const ALL: &'static [SdkKind] = &[SdkKind::Python, SdkKind::TypeScript];
31
32    /// Canonical name, as `[[sdk]]` config spells it.
33    pub fn name(self) -> &'static str {
34        match self {
35            SdkKind::Python => "python",
36            SdkKind::TypeScript => "typescript",
37        }
38    }
39
40    /// Parse a config name, accepting the common short forms.
41    pub fn parse_name(s: &str) -> Option<SdkKind> {
42        match s {
43            "python" | "py" => Some(SdkKind::Python),
44            "typescript" | "ts" => Some(SdkKind::TypeScript),
45            _ => None,
46        }
47    }
48
49    /// Emit a complete SDK package for these operations.
50    pub fn emit(
51        self,
52        info: &Info,
53        ops: &[Operation],
54        opts: &SdkOptions,
55    ) -> Result<SdkFileSet, String> {
56        let sdk = lower::lower(ops, opts, self)?;
57        match self {
58            SdkKind::Python => python::emit(info, &sdk, opts),
59            SdkKind::TypeScript => typescript::emit(info, &sdk, opts),
60        }
61    }
62}
63
64/// Everything an emitter needs that the operations don't carry.
65#[derive(Debug, Clone)]
66pub struct SdkOptions {
67    /// Distribution name, e.g. `finance-query`.
68    pub package: String,
69    /// Import name, e.g. `finance_query`.
70    pub module: String,
71    pub version: String,
72    /// Default server; falls back to the first `Info` server. Optional
73    /// when [`SdkOptions::embed`] supplies one at runtime instead.
74    pub base_url: Option<String>,
75    /// Server binary the package bundles and spawns, making the SDK
76    /// self-contained: a client built with no base URL starts this and
77    /// talks to it. `None` leaves the SDK a pure client for a hosted API.
78    pub embed: Option<String>,
79    /// Arguments passed to the embedded server on every launch.
80    pub embed_args: Vec<String>,
81    /// Environment applied to every launch, over the ambient one and under
82    /// the caller's own. Config beats ambient deliberately: an inherited
83    /// `PORT` from whatever runs the consumer's app must not decide where
84    /// a bundled server binds.
85    pub embed_env: BTreeMap<String, String>,
86    /// The knobs the embedded server documents, parsed from its dotenv
87    /// template. Becomes a typed `ServerEnv` on the client and a table in
88    /// the README, so a client configures the same server the same way a
89    /// deployment does.
90    pub embed_env_vars: Vec<envtemplate::EnvVar>,
91    /// Target triples the embedded server is built for. Empty means
92    /// [`target::Target::DEFAULT`].
93    pub targets: Vec<String>,
94    pub description: Option<String>,
95    pub repository: Option<String>,
96    /// Operation ids whose response switches to a `{items, pageInfo}`
97    /// envelope when `limit`/`cursor` are sent.
98    pub paginated: Vec<String>,
99    pub cursor_param: String,
100    pub limit_param: String,
101}
102
103impl Default for SdkOptions {
104    fn default() -> Self {
105        SdkOptions {
106            package: String::new(),
107            module: String::new(),
108            version: String::new(),
109            base_url: None,
110            embed: None,
111            embed_args: Vec::new(),
112            embed_env: BTreeMap::new(),
113            embed_env_vars: Vec::new(),
114            targets: Vec::new(),
115            description: None,
116            repository: None,
117            paginated: Vec::new(),
118            cursor_param: "cursor".into(),
119            limit_param: "limit".into(),
120        }
121    }
122}
123
124/// The default base URL an emitter should bake in, if any.
125///
126/// An SDK that embeds its server does not need one — the launcher supplies
127/// an address at runtime — so this is only fatal when neither is set.
128fn base_url_for(info: &Info, opts: &SdkOptions) -> Result<Option<String>, String> {
129    let base_url = opts
130        .base_url
131        .clone()
132        .or_else(|| info.servers.first().cloned());
133    if base_url.is_none() && opts.embed.is_none() {
134        return Err(
135            "no base URL: set `base_url` in the [[sdk]] entry, `servers` on the \
136                    [[spec]] entry, or `embed` to bundle the server itself"
137                .into(),
138        );
139    }
140    Ok(base_url)
141}
142
143/// Rendered SDK files, keyed by path relative to the output directory.
144#[derive(Debug, Clone, Default)]
145pub struct SdkFileSet {
146    pub files: BTreeMap<String, String>,
147    /// Shapes the target language cannot express precisely.
148    pub notes: Vec<String>,
149}