Skip to content

Commit ed39f9c

Browse files
authored
Merge pull request #26 from golemcloud/support_yaml
2 parents e8f0242 + e30d452 commit ed39f9c

File tree

4 files changed

+245
-66
lines changed

4 files changed

+245
-66
lines changed

src/rust/client_gen.rs

Lines changed: 199 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use openapiv3::{
2727
OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, ReferenceOr,
2828
RequestBody, Response, Schema, SchemaKind, StatusCode, Tag, Type,
2929
};
30-
use std::collections::{BTreeMap, HashSet};
30+
use std::collections::{BTreeMap, HashMap, HashSet};
3131

3232
#[derive(Debug, Clone, PartialEq, Eq)]
3333
enum PathElement {
@@ -186,6 +186,37 @@ struct Param {
186186
kind: ParamKind,
187187
}
188188

189+
#[derive(Debug, Clone)]
190+
pub struct RequestBodyParams {
191+
params: HashMap<ContentType, Vec<Param>>,
192+
}
193+
194+
impl RequestBodyParams {
195+
fn has_single_content_type(&self) -> bool {
196+
self.params.len() == 1
197+
}
198+
199+
fn get_default_request_body_param(&self) -> Option<&Vec<Param>> {
200+
self.params
201+
.values()
202+
.next()
203+
.filter(|_| self.has_single_content_type())
204+
}
205+
}
206+
207+
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
208+
pub struct ContentType(pub String);
209+
210+
impl ContentType {
211+
pub fn is_json(&self) -> bool {
212+
self.0 == "application/json"
213+
}
214+
215+
pub fn is_yaml(&self) -> bool {
216+
self.0 == "application/x-yaml"
217+
}
218+
}
219+
189220
impl Param {
190221
fn render_declaration(&self) -> RustPrinter {
191222
let type_name = self.tpe.render_declaration(true);
@@ -268,7 +299,7 @@ fn match_tag(tag: &Option<Tag>, path_item: &ReferenceOr<PathItem>) -> bool {
268299
fn param_data_to_type(data: &ParameterData, ref_cache: &mut RefCache) -> Result<DataType> {
269300
match &data.format {
270301
ParameterSchemaOrContent::Schema(ref_or_schema) => {
271-
ref_or_schema_type(ref_or_schema, ref_cache)
302+
ref_or_schema_type(ref_or_schema, ref_cache, None)
272303
}
273304
ParameterSchemaOrContent::Content(_) => {
274305
Err(Error::unimplemented("Content parameter is not supported."))
@@ -295,45 +326,74 @@ fn parameter(p: &ReferenceOr<Parameter>, ref_cache: &mut RefCache) -> Result<Par
295326
fn request_body_params(
296327
body: &ReferenceOr<RequestBody>,
297328
ref_cache: &mut RefCache,
298-
) -> Result<Vec<Param>> {
329+
) -> Result<RequestBodyParams> {
330+
let mut content_type_params = HashMap::new();
331+
299332
match body {
300-
ReferenceOr::Reference { reference } => Err(Error::unimplemented(format!(
301-
"Unexpected ref request body: '{reference}'."
302-
))),
333+
ReferenceOr::Reference { reference } => {
334+
return Err(Error::unimplemented(format!(
335+
"Unexpected ref request body: '{reference}'."
336+
)))
337+
}
303338
ReferenceOr::Item(body) => {
304-
if body.content.len() != 1 {
305-
Err(Error::unimplemented("Content with not exactly 1 option."))
306-
} else {
307-
let (content_type, media_type) = body.content.first().unwrap();
308-
309-
if content_type.starts_with("application/json") {
339+
for (content_type, media_type) in &body.content {
340+
if content_type.starts_with("application/json") || content_type == "*/*" {
310341
let schema = match &media_type.schema {
311342
None => Err(Error::unimplemented("JSON content without schema.")),
312343
Some(schema) => Ok(schema),
313344
};
314345

315-
Ok(vec![Param {
316-
original_name: "".to_string(),
317-
name: "value".to_string(),
318-
tpe: ref_or_schema_type(schema?, ref_cache)?,
319-
required: body.required,
320-
kind: ParamKind::Body,
321-
}])
346+
content_type_params.insert(
347+
ContentType(content_type.clone()),
348+
vec![Param {
349+
original_name: "".to_string(),
350+
name: "value".to_string(),
351+
tpe: ref_or_schema_type(
352+
schema?,
353+
ref_cache,
354+
Some(content_type.clone()),
355+
)?,
356+
required: body.required,
357+
kind: ParamKind::Body,
358+
}],
359+
);
322360
} else if content_type == "application/octet-stream" {
323-
Ok(vec![Param {
361+
content_type_params.insert(
362+
ContentType(content_type.clone()),
363+
vec![Param {
364+
original_name: "".to_string(),
365+
name: "value".to_string(),
366+
tpe: DataType::Binary,
367+
required: body.required,
368+
kind: ParamKind::Body,
369+
}],
370+
);
371+
} else if content_type.contains("application/x-yaml") {
372+
let schema = match &media_type.schema {
373+
None => Err(Error::unimplemented("YAML content without schema.")),
374+
Some(schema) => Ok(schema),
375+
};
376+
377+
let param = Param {
324378
original_name: "".to_string(),
325379
name: "value".to_string(),
326-
tpe: DataType::Binary,
380+
tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type.clone()))?,
327381
required: body.required,
328382
kind: ParamKind::Body,
329-
}])
383+
};
384+
385+
content_type_params.insert(ContentType(content_type.clone()), vec![param]);
330386
} else if content_type == "multipart/form-data" {
331387
match &media_type.schema {
332-
None => Err(Error::unimplemented("Multipart content without schema.")),
388+
None => {
389+
return Err(Error::unimplemented("Multipart content without schema."))
390+
}
333391
Some(schema) => match schema {
334-
ReferenceOr::Reference { reference } => Err(Error::unimplemented(
335-
format!("Unexpected ref multipart schema: '{reference}'."),
336-
)),
392+
ReferenceOr::Reference { reference } => {
393+
return Err(Error::unimplemented(format!(
394+
"Unexpected ref multipart schema: '{reference}'."
395+
)))
396+
}
337397
ReferenceOr::Item(schema) => match &schema.schema_kind {
338398
SchemaKind::Type(Type::Object(obj)) => {
339399
fn multipart_param(
@@ -351,7 +411,8 @@ fn request_body_params(
351411
})
352412
}
353413

354-
obj.properties
414+
let params = obj
415+
.properties
355416
.iter()
356417
.map(|(name, schema)| {
357418
multipart_param(
@@ -361,41 +422,39 @@ fn request_body_params(
361422
ref_cache,
362423
)
363424
})
364-
.collect()
425+
.collect::<Result<Vec<_>>>()?;
426+
427+
content_type_params
428+
.insert(ContentType(content_type.clone()), params);
429+
}
430+
_ => {
431+
return Err(Error::unimplemented(
432+
"Object schema expected for multipart request body.",
433+
))
365434
}
366-
_ => Err(Error::unimplemented(
367-
"Object schema expected for multipart request body.",
368-
)),
369435
},
370436
},
371437
}
372438
} else {
373-
Err(Error::unimplemented(format!(
439+
return Err(Error::unimplemented(format!(
374440
"Request body content type: '{content_type}'."
375-
)))
441+
)));
376442
}
377443
}
378444
}
379445
}
446+
447+
Ok(RequestBodyParams {
448+
params: content_type_params,
449+
})
380450
}
381451

382452
fn parameters(op: &PathOperation, ref_cache: &mut RefCache) -> Result<Vec<Param>> {
383-
let params: Result<Vec<Param>> = op
384-
.op
453+
op.op
385454
.parameters
386455
.iter()
387456
.map(|p| parameter(p, ref_cache))
388-
.collect();
389-
390-
let mut params = params?;
391-
392-
if let Some(body) = &op.op.request_body {
393-
for p in request_body_params(body, ref_cache)? {
394-
params.push(p);
395-
}
396-
}
397-
398-
Ok(params)
457+
.collect()
399458
}
400459

401460
fn as_code(code: &StatusCode) -> Option<u16> {
@@ -449,7 +508,11 @@ fn response_type(response: &ReferenceOr<Response>, ref_cache: &mut RefCache) ->
449508
Some(schema) => Ok(schema),
450509
};
451510

452-
Ok(ref_or_schema_type(schema?, ref_cache)?)
511+
Ok(ref_or_schema_type(
512+
schema?,
513+
ref_cache,
514+
Some(content_type.clone()),
515+
)?)
453516
} else if content_type == "application/octet-stream" {
454517
Ok(DataType::Binary)
455518
} else {
@@ -494,40 +557,107 @@ fn method_errors(
494557
Ok(MethodErrors { codes: codes? })
495558
}
496559

497-
fn trait_method(
560+
fn trait_methods_specific_to_content_type(
498561
op: &PathOperation,
499562
prefix_length: usize,
500563
ref_cache: &mut RefCache,
501-
) -> Result<Method> {
564+
) -> Result<Vec<Method>> {
502565
let (result_code, result_type) = method_result(&op.op.responses.responses, ref_cache)?;
503566

504-
let name = if let Some(op_id) = &op.op.operation_id {
505-
op_id.to_case(Case::Snake)
567+
let name = op
568+
.op
569+
.operation_id
570+
.as_ref()
571+
.map(|op_id| op_id.to_case(Case::Snake))
572+
.unwrap_or_else(|| op.path.strip_prefix(prefix_length).method_name(&op.method));
573+
574+
let mut main_params = parameters(op, ref_cache)?;
575+
576+
if let Some(body) = &op.op.request_body {
577+
let content_specific = request_body_params(body, ref_cache)?;
578+
579+
if let Some(request_body_params) = content_specific.get_default_request_body_param() {
580+
main_params.extend(request_body_params.iter().cloned());
581+
return Ok(vec![create_method(
582+
name,
583+
op,
584+
&main_params,
585+
result_type,
586+
result_code,
587+
ref_cache,
588+
)?]);
589+
}
590+
591+
let mut methods = Vec::new();
592+
for (content_type, params) in content_specific.params {
593+
let method_name = match_content_type(content_type, &name)?;
594+
let new_params = [main_params.clone(), params].concat();
595+
methods.push(create_method(
596+
method_name,
597+
op,
598+
&new_params,
599+
result_type.clone(),
600+
result_code.clone(),
601+
ref_cache,
602+
)?);
603+
}
604+
605+
Ok(methods)
506606
} else {
507-
op.path.strip_prefix(prefix_length).method_name(&op.method)
508-
};
607+
Ok(vec![create_method(
608+
name,
609+
op,
610+
&main_params,
611+
result_type,
612+
result_code,
613+
ref_cache,
614+
)?])
615+
}
616+
}
509617

618+
fn create_method(
619+
name: String,
620+
op: &PathOperation,
621+
params: &[Param],
622+
result_type: DataType,
623+
result_code: StatusCode,
624+
ref_cache: &mut RefCache,
625+
) -> Result<Method> {
510626
Ok(Method {
511627
name,
512628
path: op.path.clone(),
513629
original_path: op.original_path.clone(),
514630
http_method: op.method.to_string(),
515-
params: parameters(op, ref_cache)?,
631+
params: params.to_vec(),
516632
result: result_type,
517633
result_status_code: result_code.clone(),
518634
errors: method_errors(&op.op.responses.responses, result_code, ref_cache)?,
519635
})
520636
}
521637

638+
fn match_content_type(content_type: ContentType, base_name: &str) -> Result<String> {
639+
if content_type.is_json() {
640+
Ok(format!("{}_json", base_name))
641+
} else if content_type.is_yaml() {
642+
Ok(format!("{}_yaml", base_name))
643+
} else {
644+
Err(Error::unimplemented(
645+
"Multiple content types supported only for JSON and YAML",
646+
))
647+
}
648+
}
649+
522650
fn trait_methods(
523651
operations: &[PathOperation],
524652
prefix_length: usize,
525653
ref_cache: &mut RefCache,
526654
) -> Result<Vec<Method>> {
527-
operations
655+
let res = operations
528656
.iter()
529-
.map(|op| trait_method(op, prefix_length, ref_cache))
530-
.collect()
657+
.map(|op| trait_methods_specific_to_content_type(op, prefix_length, ref_cache))
658+
.collect::<Result<Vec<Vec<_>>>>()?;
659+
660+
Ok(res.into_iter().flatten().collect())
531661
}
532662

533663
fn render_errors(method_name: &str, error_kind: &ErrorKind, errors: &MethodErrors) -> RustResult {
@@ -837,7 +967,18 @@ fn render_method_implementation(method: &Method, error_kind: &ErrorKind) -> Rust
837967
r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/octet-stream");"#,
838968
)
839969
+ NewLine
840-
} else {
970+
} else if param.tpe == DataType::Yaml {
971+
line(
972+
unit()
973+
+ "request = request.body(serde_yaml::to_string("
974+
+ &param.name
975+
+ ").unwrap_or_default().into_bytes());",
976+
) + line(
977+
r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/x-yaml");"#,
978+
) + NewLine
979+
}
980+
// Not sure why everything else is assumed to be json (previously)
981+
else {
841982
line(unit() + "request = request.json(" + &param.name + ");") + NewLine
842983
}
843984
}

src/rust/lib_gen.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ mod tests {
142142
"lib",
143143
&[
144144
ModuleDef {
145-
name: ModuleName::new("abc".to_string()),
145+
name: ModuleName::new("abc"),
146146
exports: vec!["C".to_string(), "B".to_string()],
147147
},
148148
ModuleDef {
149-
name: ModuleName::new("xyz".to_string()),
149+
name: ModuleName::new("xyz"),
150150
exports: vec!["A".to_string(), "Y".to_string()],
151151
},
152152
],

0 commit comments

Comments
 (0)