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, HttpHeaders }} from '@angular/common/http';"
13    )?;
14    writeln!(out, "import type {{ Observable }} from 'rxjs';")?;
15
16    let imports = collect_type_imports(api);
17    if !imports.is_empty() {
18        writeln!(
19            out,
20            "import type {{ {} }} from './types.gen';",
21            imports.join(", ")
22        )?;
23    }
24    writeln!(out)?;
25
26    writeln!(out, "@Injectable({{ providedIn: 'root' }})")?;
27    writeln!(out, "export class ApiService {{")?;
28    writeln!(out, "  constructor(private http: HttpClient) {{}}")?;
29    writeln!(out)?;
30
31    for endpoint in &api.endpoints {
32        emit_method(endpoint, out)?;
33        writeln!(out)?;
34    }
35
36    writeln!(out, "}}")?;
37
38    Ok(())
39}
40
41fn emit_method(endpoint: &Endpoint, out: &mut String) -> Result<(), std::fmt::Error> {
42    let id = &endpoint.operation_id;
43    let method = endpoint.method.as_lower();
44
45    let has_path = endpoint.has_params(&ParamLocation::Path);
46    let has_query = endpoint.has_params(&ParamLocation::Query);
47    let has_header = endpoint.has_params(&ParamLocation::Header);
48    let has_cookie = endpoint.has_params(&ParamLocation::Cookie);
49    let has_body = endpoint.request_body.is_some();
50    let has_headers = has_header || has_cookie;
51
52    let mut params = Vec::new();
53    if has_path {
54        params.push(format!("pathParams: {id}PathParams"));
55    }
56    if has_query {
57        params.push(format!("queryParams?: {id}QueryParams"));
58    }
59    if has_header {
60        params.push(format!("headerParams?: {id}HeaderParams"));
61    }
62    if has_cookie {
63        params.push(format!("cookieParams?: {id}CookieParams"));
64    }
65    if has_body {
66        params.push(format!("body: {id}Body"));
67    }
68
69    let return_type = endpoint.return_type_ts();
70    let url = path_to_template_literal(&endpoint.path);
71
72    writeln!(
73        out,
74        "  {id}({params}): Observable<{return_type}> {{",
75        params = params.join(", ")
76    )?;
77
78    // Build HttpParams for query
79    if has_query {
80        writeln!(out, "    let params = new HttpParams();")?;
81        writeln!(out, "    if (queryParams) {{")?;
82        writeln!(
83            out,
84            "      for (const [key, value] of Object.entries(queryParams)) {{"
85        )?;
86        writeln!(out, "        if (Array.isArray(value)) {{")?;
87        writeln!(
88            out,
89            "          for (const v of value) params = params.append(key, String(v));"
90        )?;
91        writeln!(out, "        }} else if (value !== undefined) {{")?;
92        writeln!(out, "          params = params.set(key, String(value));")?;
93        writeln!(out, "        }}")?;
94        writeln!(out, "      }}")?;
95        writeln!(out, "    }}")?;
96    }
97
98    // Build HttpHeaders for header/cookie params
99    if has_headers {
100        writeln!(out, "    let headers = new HttpHeaders();")?;
101    }
102    if has_header {
103        writeln!(out, "    if (headerParams) {{")?;
104        writeln!(
105            out,
106            "      for (const [key, value] of Object.entries(headerParams)) {{"
107        )?;
108        writeln!(
109            out,
110            "        if (value !== undefined) headers = headers.set(key, String(value));"
111        )?;
112        writeln!(out, "      }}")?;
113        writeln!(out, "    }}")?;
114    }
115    if has_cookie {
116        writeln!(out, "    if (cookieParams) {{")?;
117        writeln!(
118            out,
119            "      const cookie = Object.entries(cookieParams).filter(([, v]) => v !== undefined).map(([k, v]) => `${{k}}=${{v}}`).join('; ');"
120        )?;
121        writeln!(out, "      headers = headers.set('Cookie', cookie);")?;
122        writeln!(out, "    }}")?;
123    }
124
125    // Build options object
126    let options = build_options(has_query, has_headers, endpoint.response_type.clone());
127
128    if has_body {
129        if options.is_empty() {
130            writeln!(
131                out,
132                "    return this.http.{method}<{return_type}>(`{url}`, body);"
133            )?;
134        } else {
135            writeln!(
136                out,
137                "    return this.http.{method}<{return_type}>(`{url}`, body, {options});"
138            )?;
139        }
140    } else if matches!(
141        endpoint.method,
142        HttpMethod::Post | HttpMethod::Put | HttpMethod::Patch
143    ) {
144        if options.is_empty() {
145            writeln!(
146                out,
147                "    return this.http.{method}<{return_type}>(`{url}`, null);"
148            )?;
149        } else {
150            writeln!(
151                out,
152                "    return this.http.{method}<{return_type}>(`{url}`, null, {options});"
153            )?;
154        }
155    } else if options.is_empty() {
156        writeln!(
157            out,
158            "    return this.http.{method}<{return_type}>(`{url}`);"
159        )?;
160    } else {
161        writeln!(
162            out,
163            "    return this.http.{method}<{return_type}>(`{url}`, {options});"
164        )?;
165    }
166
167    writeln!(out, "  }}")?;
168
169    Ok(())
170}
171
172fn build_options(has_query: bool, has_headers: bool, response_type: ResponseType) -> String {
173    let mut parts = Vec::new();
174
175    if has_query {
176        parts.push("params".to_string());
177    }
178    if has_headers {
179        parts.push("headers".to_string());
180    }
181
182    match response_type {
183        ResponseType::Text => parts.push("responseType: 'text' as const".to_string()),
184        ResponseType::Blob => parts.push("responseType: 'blob' as const".to_string()),
185        _ => {}
186    }
187
188    if parts.is_empty() {
189        String::new()
190    } else {
191        format!("{{ {} }}", parts.join(", "))
192    }
193}