oa_forge_emitter_angular/
lib.rs1use std::fmt::Write;
2
3use oa_forge_ir::*;
4
5pub 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 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}