1#![warn(missing_docs)]
2
3use nargo_metadata::{ApiEndpoint, ApiMetadata, ApiParam, ApiParamType};
8use std::{collections::HashMap, fs::File, io::Write, path::Path};
9
10pub struct SdkGenerator;
14
15impl SdkGenerator {
16 pub fn generate(metadata: &ApiMetadata, output_dir: &Path) -> std::io::Result<()> {
25 std::fs::create_dir_all(output_dir)?;
26
27 Self::generate_types(metadata, &output_dir.join("types.ts"))?;
28
29 Self::generate_sdk(metadata, &output_dir.join("api.ts"))?;
30
31 Ok(())
32 }
33
34 fn generate_types(metadata: &ApiMetadata, output_path: &Path) -> std::io::Result<()> {
43 let mut file = File::create(output_path)?;
44
45 writeln!(file, "// Generated by Nargo SDK Generator")?;
46 writeln!(file)?;
47
48 for (name, type_def) in &metadata.types {
49 writeln!(file, "export interface {name} {{")?;
50 for field in &type_def.fields {
51 let optional = if field.optional { "?" } else { "" };
52 writeln!(file, " {}{}: {};", field.name, optional, field.type_name)?;
53 }
54 writeln!(file, "}}")?;
55 writeln!(file)?;
56 }
57
58 Ok(())
59 }
60
61 fn generate_sdk(metadata: &ApiMetadata, output_path: &Path) -> std::io::Result<()> {
70 let mut file = File::create(output_path)?;
71
72 writeln!(file, "/* eslint-disable */")?;
73 writeln!(file, "// @ts-nocheck")?;
74 writeln!(file, "import {{ useClient }} from '@nargo/client';")?;
75 writeln!(file, "import type * as Types from './types';")?;
76 writeln!(file)?;
77
78 let mut controllers: HashMap<String, Vec<&ApiEndpoint>> = HashMap::new();
79 for endpoint in &metadata.endpoints {
80 controllers.entry(endpoint.controller.clone()).or_default().push(endpoint);
81 }
82
83 for (controller_name, endpoints) in controllers {
84 Self::generate_controller(&mut file, &controller_name, &endpoints)?;
85 }
86
87 Ok(())
88 }
89
90 fn generate_controller(file: &mut File, controller_name: &str, endpoints: &[&ApiEndpoint]) -> std::io::Result<()> {
100 writeln!(file, "export const {controller_name} = {{")?;
101
102 for endpoint in endpoints {
103 Self::generate_endpoint_method(file, controller_name, endpoint)?;
104 }
105
106 writeln!(file, "}};")?;
107 writeln!(file)?;
108
109 for endpoint in endpoints {
110 Self::generate_endpoint_metadata(file, controller_name, endpoint)?;
111 }
112 writeln!(file)?;
113
114 Ok(())
115 }
116
117 fn generate_endpoint_method(file: &mut File, _controller_name: &str, endpoint: &ApiEndpoint) -> std::io::Result<()> {
127 let return_type = endpoint.return_type.as_deref().unwrap_or("any");
128
129 writeln!(file, " /**")?;
130 writeln!(file, " */")?;
131
132 let params_str = Self::generate_params_list(&endpoint.params);
133 writeln!(file, " async {name}({params_str}): Promise<{return_type}> {{", name = endpoint.name)?;
134
135 writeln!(file, " const client = useClient();")?;
136
137 let path = Self::generate_path(&endpoint.path, &endpoint.params);
138 let query_params = Self::generate_query_params(&endpoint.params);
139 let body_param = endpoint.params.iter().find(|p| p.param_type == ApiParamType::Body);
140 let method = endpoint.method.to_lowercase();
141
142 match method.as_str() {
143 "get" | "delete" => {
144 writeln!(file, " return client.{}(`{}`, {{ params: {{ {} }} }});", method, path, query_params)?;
145 }
146 "post" | "put" | "patch" => {
147 if let Some(body) = body_param {
148 writeln!(file, " return client.{}(`{}`, {}, {{ params: {{ {} }} }});", method, path, body.name, query_params)?;
149 }
150 else {
151 writeln!(file, " return client.{}(`{}`, undefined, {{ params: {{ {} }} }});", method, path, query_params)?;
152 }
153 }
154 _ => {
155 if let Some(body) = body_param {
156 writeln!(file, " return client.{}(`{}`, {}, {{ params: {{ {} }} }});", method, path, body.name, query_params)?;
157 }
158 else {
159 writeln!(file, " return client.{}(`{}`, {{ params: {{ {} }} }});", method, path, query_params)?;
160 }
161 }
162 }
163
164 writeln!(file, " }},")?;
165
166 Ok(())
167 }
168
169 fn generate_params_list(params: &[ApiParam]) -> String {
177 let filtered: Vec<String> = params.iter().filter(|p| p.param_type != ApiParamType::CurrentUser && p.param_type != ApiParamType::Session && p.param_type != ApiParamType::I18n).map(|p| format!("{}: {}", p.name, p.type_name)).collect();
178
179 filtered.join(", ")
180 }
181
182 fn generate_path(path: &str, params: &[ApiParam]) -> String {
191 let mut result = path.to_string();
192
193 for param in params {
194 if param.param_type == ApiParamType::Path {
195 let placeholder = if let Some(extractor_param) = ¶m.extractor_param { format!(":{}", extractor_param) } else { format!(":{}", param.name) };
196 result = result.replace(&placeholder, &format!("${{{}}}", param.name));
197 }
198 }
199
200 result
201 }
202
203 fn generate_query_params(params: &[ApiParam]) -> String {
211 let query_params: Vec<String> = params.iter().filter(|p| p.param_type == ApiParamType::Query).map(|p| p.name.clone()).collect();
212
213 query_params.join(", ")
214 }
215
216 fn generate_endpoint_metadata(file: &mut File, controller_name: &str, endpoint: &ApiEndpoint) -> std::io::Result<()> {
226 let nargo_id = format!("{}.{}", controller_name, endpoint.name);
227
228 writeln!(file, "( {}.{} as any)._nargo_id = '{}';", controller_name, endpoint.name, nargo_id)?;
229 writeln!(file, "( {}.{} as any).url = '{}';", controller_name, endpoint.name, endpoint.path)?;
230 writeln!(file, "( {}.{} as any).method = '{}';", controller_name, endpoint.name, endpoint.method)?;
231
232 Ok(())
233 }
234}