diff --git a/src/api.rs b/src/api.rs index 39c1ccf..b17489f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -28,7 +28,11 @@ pub(crate) struct Api { } impl Api { - pub(crate) fn new(paths: openapi::Paths, with_deprecated: bool) -> anyhow::Result { + pub(crate) fn new( + paths: openapi::Paths, + with_deprecated: bool, + component_schemas: &IndexMap, + ) -> anyhow::Result { let mut resources = BTreeMap::new(); for (path, pi) in paths { @@ -46,7 +50,9 @@ impl Api { continue; } - if let Some((res_path, op)) = Operation::from_openapi(&path, method, op) { + if let Some((res_path, op)) = + Operation::from_openapi(&path, method, op, component_schemas) + { let resource = get_or_insert_resource(&mut resources, res_path); if op.method == "post" { resource.has_post_operation = true; @@ -223,6 +229,10 @@ struct Operation { /// Name of the request body type, if any. #[serde(skip_serializing_if = "Option::is_none")] request_body_schema_name: Option, + /// Some request bodies are required, but all the fields are optional (i.e. the CLI can omit + /// this from the argument list). + /// Only useful when `request_body_schema_name` is `Some`. + request_body_all_optional: bool, /// Name of the response body type, if any. #[serde(skip_serializing_if = "Option::is_none")] response_body_schema_name: Option, @@ -234,6 +244,7 @@ impl Operation { path: &str, method: &str, op: openapi::Operation, + component_schemas: &IndexMap, ) -> Option<(Vec, Self)> { let Some(op_id) = op.operation_id else { // ignore operations without an operationId @@ -343,6 +354,51 @@ impl Operation { } } + let request_body_all_optional = op + .request_body + .as_ref() + .map(|r| { + match r { + ReferenceOr::Reference { .. } => { + unimplemented!("reference") + } + ReferenceOr::Item(body) => { + if let Some(mt) = body.content.get("application/json") { + match mt.schema.as_ref().map(|so| &so.json_schema) { + Some(Schema::Object(schemars::schema::SchemaObject { + object: Some(ov), + .. + })) => { + return ov.required.is_empty(); + } + Some(Schema::Object(schemars::schema::SchemaObject { + reference: Some(s), + .. + })) => { + match component_schemas + .get( + &get_schema_name(Some(s)).expect("schema should exist"), + ) + .map(|so| &so.json_schema) + { + Some(Schema::Object(schemars::schema::SchemaObject { + object: Some(ov), + .. + })) => { + return ov.required.is_empty(); + } + _ => unimplemented!("double ref not supported"), + } + } + _ => {} + } + } + } + } + false + }) + .unwrap_or_default(); + let request_body_schema_name = op.request_body.and_then(|b| match b { ReferenceOr::Item(mut req_body) => { assert!(req_body.required); @@ -362,7 +418,7 @@ impl Operation { if !obj.is_ref() { tracing::error!(?obj, "unexpected non-$ref json body schema"); } - get_schema_name(obj.reference) + get_schema_name(obj.reference.as_deref()) } } } @@ -415,6 +471,7 @@ impl Operation { header_params, query_params, request_body_schema_name, + request_body_all_optional, response_body_schema_name, }; Some((res_path, op)) @@ -458,7 +515,7 @@ fn response_body_schema_name(resp: ReferenceOr) -> Option anyhow::Result<()> { let mut components = spec.components.unwrap_or_default(); if let Some(paths) = spec.paths { - let api = Api::new(paths, with_deprecated).unwrap(); + let api = Api::new(paths, with_deprecated, &components.schemas).unwrap(); { let mut api_file = BufWriter::new(File::create("api.ron")?); writeln!(api_file, "{api:#?}")?; diff --git a/src/types.rs b/src/types.rs index 1248eac..89b2b96 100644 --- a/src/types.rs +++ b/src/types.rs @@ -72,7 +72,7 @@ impl FieldType { Some(SingleOrVec::Vec(types)) => { bail!("unsupported multi-typed parameter: `{types:?}`") } - None => match get_schema_name(obj.reference) { + None => match get_schema_name(obj.reference.as_deref()) { Some(name) => Self::SchemaRef(name), None => bail!("unsupported type-less parameter"), }, diff --git a/src/util.rs b/src/util.rs index ff44653..098a2d2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeSet, io, process::Command, sync::Mutex}; use camino::Utf8Path; -pub(crate) fn get_schema_name(maybe_ref: Option) -> Option { +pub(crate) fn get_schema_name(maybe_ref: Option<&str>) -> Option { let r = maybe_ref?; let schema_name = r.strip_prefix("#/components/schemas/"); if schema_name.is_none() { diff --git a/templates/svix_cli_resource.rs.jinja b/templates/svix_cli_resource.rs.jinja index ecf32d6..0af6957 100644 --- a/templates/svix_cli_resource.rs.jinja +++ b/templates/svix_cli_resource.rs.jinja @@ -86,7 +86,12 @@ pub enum {{ resource_type_name }}Commands { {# body parameter struct -#} {% if op.request_body_schema_name is defined -%} {{ op.request_body_schema_name | to_snake_case }}: - JsonOf<{{ op.request_body_schema_name }}>, + {% if op.request_body_all_optional %} + Option> + {% else %} + JsonOf<{{ op.request_body_schema_name }}> + {% endif %} + , {% endif -%} {# query parameters -#} @@ -151,7 +156,13 @@ impl {{ resource_type_name }}Commands { {# body parameter struct -#} {% if op.request_body_schema_name is defined -%} - {{ op.request_body_schema_name | to_snake_case }}.into_inner(), + {{ op.request_body_schema_name | to_snake_case }} + {% if op.request_body_all_optional -%} + .map(|x| x.into_inner()).unwrap_or_default() + {% else -%} + .into_inner() + {% endif -%} + , {% endif -%} {# query parameters -#}