Skip to main content

oa_forge_emitter_angular/
lib.rs

1use std::fmt::Write;
2
3use oa_forge_ir::*;
4
5/// Emit an Angular `@Injectable` service using `HttpClient`.
6pub fn emit(api: &ApiSpec, out: &mut String) -> Result<(), std::fmt::Error> {
7    writeln!(out, "// Generated by oa-forge. Do not edit.")?;
8    writeln!(out)?;
9    writeln!(out, "import {{ Injectable }} from '@angular/core';")?;
10    writeln!(
11        out,
12        "import {{ HttpClient, HttpParams }} from '@angular/common/http';"
13    )?;
14    writeln!(out, "import type {{ Observable }} from 'rxjs';")?;
15
16    emit_type_imports(api, out)?;
17    writeln!(out)?;
18
19    writeln!(out, "@Injectable({{ providedIn: 'root' }})")?;
20    writeln!(out, "export class ApiService {{")?;
21    writeln!(out, "  constructor(private http: HttpClient) {{}}")?;
22    writeln!(out)?;
23
24    for endpoint in &api.endpoints {
25        emit_method(endpoint, out)?;
26        writeln!(out)?;
27    }
28
29    writeln!(out, "}}")?;
30
31    Ok(())
32}
33
34fn emit_type_imports(api: &ApiSpec, out: &mut String) -> Result<(), std::fmt::Error> {
35    let mut imports = Vec::new();
36
37    for endpoint in &api.endpoints {
38        let id = &endpoint.operation_id;
39        let has_path = endpoint
40            .parameters
41            .iter()
42            .any(|p| p.location == ParamLocation::Path);
43        let has_query = endpoint
44            .parameters
45            .iter()
46            .any(|p| p.location == ParamLocation::Query);
47
48        if has_path {
49            imports.push(format!("{id}PathParams"));
50        }
51        if has_query {
52            imports.push(format!("{id}QueryParams"));
53        }
54        if endpoint.response.is_some() && endpoint.response_type == ResponseType::Json {
55            imports.push(format!("{id}Response"));
56        }
57        if endpoint.request_body.is_some() {
58            imports.push(format!("{id}Body"));
59        }
60    }
61
62    if !imports.is_empty() {
63        writeln!(
64            out,
65            "import type {{ {} }} from './types.gen';",
66            imports.join(", ")
67        )?;
68    }
69
70    Ok(())
71}
72
73fn emit_method(endpoint: &Endpoint, out: &mut String) -> Result<(), std::fmt::Error> {
74    let id = &endpoint.operation_id;
75    let method = match endpoint.method {
76        HttpMethod::Get => "get",
77        HttpMethod::Post => "post",
78        HttpMethod::Put => "put",
79        HttpMethod::Patch => "patch",
80        HttpMethod::Delete => "delete",
81    };
82
83    let has_path = endpoint
84        .parameters
85        .iter()
86        .any(|p| p.location == ParamLocation::Path);
87    let has_query = endpoint
88        .parameters
89        .iter()
90        .any(|p| p.location == ParamLocation::Query);
91    let has_body = endpoint.request_body.is_some();
92
93    let mut params = Vec::new();
94    if has_path {
95        params.push(format!("pathParams: {id}PathParams"));
96    }
97    if has_query {
98        params.push(format!("queryParams?: {id}QueryParams"));
99    }
100    if has_body {
101        params.push(format!("body: {id}Body"));
102    }
103
104    let return_type = match endpoint.response_type {
105        ResponseType::Json if endpoint.response.is_some() => format!("{id}Response"),
106        ResponseType::Text => "string".to_string(),
107        ResponseType::Blob => "Blob".to_string(),
108        _ => "void".to_string(),
109    };
110
111    let url = build_url_template(&endpoint.path);
112
113    writeln!(
114        out,
115        "  {id}({params}): Observable<{return_type}> {{",
116        params = params.join(", ")
117    )?;
118
119    // Build HttpParams for query
120    if has_query {
121        writeln!(out, "    let params = new HttpParams();")?;
122        writeln!(out, "    if (queryParams) {{")?;
123        writeln!(
124            out,
125            "      for (const [key, value] of Object.entries(queryParams)) {{"
126        )?;
127        writeln!(
128            out,
129            "        if (value !== undefined) params = params.set(key, String(value));"
130        )?;
131        writeln!(out, "      }}")?;
132        writeln!(out, "    }}")?;
133    }
134
135    let options = match endpoint.response_type {
136        ResponseType::Text => {
137            if has_query {
138                "{{ params, responseType: 'text' as const }}"
139            } else {
140                "{{ responseType: 'text' as const }}"
141            }
142        }
143        ResponseType::Blob => {
144            if has_query {
145                "{{ params, responseType: 'blob' as const }}"
146            } else {
147                "{{ responseType: 'blob' as const }}"
148            }
149        }
150        _ => {
151            if has_query {
152                "{{ params }}"
153            } else {
154                ""
155            }
156        }
157    };
158
159    if has_body {
160        if options.is_empty() {
161            writeln!(
162                out,
163                "    return this.http.{method}<{return_type}>(`{url}`, body);"
164            )?;
165        } else {
166            writeln!(
167                out,
168                "    return this.http.{method}<{return_type}>(`{url}`, body, {options});"
169            )?;
170        }
171    } else if matches!(
172        endpoint.method,
173        HttpMethod::Post | HttpMethod::Put | HttpMethod::Patch
174    ) {
175        if options.is_empty() {
176            writeln!(
177                out,
178                "    return this.http.{method}<{return_type}>(`{url}`, null);"
179            )?;
180        } else {
181            writeln!(
182                out,
183                "    return this.http.{method}<{return_type}>(`{url}`, null, {options});"
184            )?;
185        }
186    } else if options.is_empty() {
187        writeln!(
188            out,
189            "    return this.http.{method}<{return_type}>(`{url}`);"
190        )?;
191    } else {
192        writeln!(
193            out,
194            "    return this.http.{method}<{return_type}>(`{url}`, {options});"
195        )?;
196    }
197
198    writeln!(out, "  }}")?;
199
200    Ok(())
201}
202
203fn build_url_template(path: &str) -> String {
204    let mut result = String::new();
205    let mut chars = path.chars().peekable();
206
207    while let Some(c) = chars.next() {
208        if c == '{' {
209            let param_name: String = chars.by_ref().take_while(|&c| c != '}').collect();
210            result.push_str(&format!("${{pathParams.{param_name}}}"));
211        } else {
212            result.push(c);
213        }
214    }
215
216    result
217}