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}