Skip to content

Commit 7a36da7

Browse files
committed
Optional request bodies
The idea is to identify args for the CLI that can be omitted since the entire body is technically optional.
1 parent 0524d30 commit 7a36da7

File tree

5 files changed

+78
-9
lines changed

5 files changed

+78
-9
lines changed

src/api.rs

+60-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ pub(crate) struct Api {
2828
}
2929

3030
impl Api {
31-
pub(crate) fn new(paths: openapi::Paths, with_deprecated: bool) -> anyhow::Result<Self> {
31+
pub(crate) fn new(
32+
paths: openapi::Paths,
33+
with_deprecated: bool,
34+
component_schemas: IndexMap<String, aide::openapi::SchemaObject>,
35+
) -> anyhow::Result<Self> {
3236
let mut resources = BTreeMap::new();
3337

3438
for (path, pi) in paths {
@@ -46,7 +50,9 @@ impl Api {
4650
continue;
4751
}
4852

49-
if let Some((res_path, op)) = Operation::from_openapi(&path, method, op) {
53+
if let Some((res_path, op)) =
54+
Operation::from_openapi(&path, method, op, &component_schemas)
55+
{
5056
let resource = get_or_insert_resource(&mut resources, res_path);
5157
if op.method == "post" {
5258
resource.has_post_operation = true;
@@ -223,6 +229,10 @@ struct Operation {
223229
/// Name of the request body type, if any.
224230
#[serde(skip_serializing_if = "Option::is_none")]
225231
request_body_schema_name: Option<String>,
232+
/// Some request bodies are required, but all the fields are optional (i.e. the CLI can omit
233+
/// this from the argument list).
234+
/// Only useful when `request_body_schema_name` is `Some`.
235+
request_body_all_optional: bool,
226236
/// Name of the response body type, if any.
227237
#[serde(skip_serializing_if = "Option::is_none")]
228238
response_body_schema_name: Option<String>,
@@ -234,6 +244,7 @@ impl Operation {
234244
path: &str,
235245
method: &str,
236246
op: openapi::Operation,
247+
component_schemas: &IndexMap<String, aide::openapi::SchemaObject>,
237248
) -> Option<(Vec<String>, Self)> {
238249
let Some(op_id) = op.operation_id else {
239250
// ignore operations without an operationId
@@ -342,6 +353,50 @@ impl Operation {
342353
}
343354
}
344355
}
356+
let request_body_all_optional = op
357+
.request_body
358+
.as_ref()
359+
.map(|r| {
360+
match r {
361+
ReferenceOr::Reference { .. } => {
362+
unimplemented!("reference")
363+
}
364+
ReferenceOr::Item(body) => {
365+
if let Some(mt) = body.content.get("application/json") {
366+
match mt.schema.as_ref().map(|so| &so.json_schema) {
367+
Some(Schema::Object(schemars::schema::SchemaObject {
368+
object: Some(ov),
369+
..
370+
})) => {
371+
return ov.required.is_empty();
372+
}
373+
Some(Schema::Object(schemars::schema::SchemaObject {
374+
reference: Some(s),
375+
..
376+
})) => {
377+
match component_schemas
378+
.get(
379+
&get_schema_name(Some(s)).expect("schema should exist"),
380+
)
381+
.map(|so| &so.json_schema)
382+
{
383+
Some(Schema::Object(schemars::schema::SchemaObject {
384+
object: Some(ov),
385+
..
386+
})) => {
387+
return ov.required.is_empty();
388+
}
389+
_ => unimplemented!("double ref not supported"),
390+
}
391+
}
392+
_ => {}
393+
}
394+
}
395+
}
396+
}
397+
false
398+
})
399+
.unwrap_or_default();
345400

346401
let request_body_schema_name = op.request_body.and_then(|b| match b {
347402
ReferenceOr::Item(mut req_body) => {
@@ -362,7 +417,7 @@ impl Operation {
362417
if !obj.is_ref() {
363418
tracing::error!(?obj, "unexpected non-$ref json body schema");
364419
}
365-
get_schema_name(obj.reference)
420+
get_schema_name(obj.reference.as_deref())
366421
}
367422
}
368423
}
@@ -415,6 +470,7 @@ impl Operation {
415470
header_params,
416471
query_params,
417472
request_body_schema_name,
473+
request_body_all_optional,
418474
response_body_schema_name,
419475
};
420476
Some((res_path, op))
@@ -458,7 +514,7 @@ fn response_body_schema_name(resp: ReferenceOr<openapi::Response>) -> Option<Str
458514
if !obj.is_ref() {
459515
tracing::error!(?obj, "unexpected non-$ref json body schema");
460516
}
461-
get_schema_name(obj.reference)
517+
get_schema_name(obj.reference.as_deref())
462518
}
463519
}
464520
}

src/main.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ fn main() -> anyhow::Result<()> {
5757

5858
let output_dir = TempDir::new().context("failed to create tempdir")?;
5959

60+
let component_schemas = spec.components.clone().unwrap_or_default().schemas;
61+
6062
let mut components = spec.components.unwrap_or_default();
6163

6264
if let Some(paths) = spec.paths {
63-
let api = Api::new(paths, with_deprecated).unwrap();
65+
let api = Api::new(paths, with_deprecated, component_schemas).unwrap();
6466
{
6567
let mut api_file = BufWriter::new(File::create("api.ron")?);
6668
writeln!(api_file, "{api:#?}")?;

src/types.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl FieldType {
7272
Some(SingleOrVec::Vec(types)) => {
7373
bail!("unsupported multi-typed parameter: `{types:?}`")
7474
}
75-
None => match get_schema_name(obj.reference) {
75+
None => match get_schema_name(obj.reference.as_deref()) {
7676
Some(name) => Self::SchemaRef(name),
7777
None => bail!("unsupported type-less parameter"),
7878
},

src/util.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{collections::BTreeSet, io, process::Command, sync::Mutex};
22

33
use camino::Utf8Path;
44

5-
pub(crate) fn get_schema_name(maybe_ref: Option<String>) -> Option<String> {
5+
pub(crate) fn get_schema_name(maybe_ref: Option<&str>) -> Option<String> {
66
let r = maybe_ref?;
77
let schema_name = r.strip_prefix("#/components/schemas/");
88
if schema_name.is_none() {

templates/svix_cli_resource.rs.jinja

+13-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ pub enum {{ resource_type_name }}Commands {
8686
{# body parameter struct -#}
8787
{% if op.request_body_schema_name is defined -%}
8888
{{ op.request_body_schema_name | to_snake_case }}:
89-
JsonOf<{{ op.request_body_schema_name }}>,
89+
{% if op.request_body_all_optional %}
90+
Option<JsonOf<{{ op.request_body_schema_name }}>>
91+
{% else %}
92+
JsonOf<{{ op.request_body_schema_name }}>
93+
{% endif %}
94+
,
9095
{% endif -%}
9196

9297
{# query parameters -#}
@@ -151,7 +156,13 @@ impl {{ resource_type_name }}Commands {
151156

152157
{# body parameter struct -#}
153158
{% if op.request_body_schema_name is defined -%}
154-
{{ op.request_body_schema_name | to_snake_case }}.into_inner(),
159+
{{ op.request_body_schema_name | to_snake_case }}
160+
{% if op.request_body_all_optional -%}
161+
.map(|x| x.into_inner()).unwrap_or_default()
162+
{% else -%}
163+
.into_inner()
164+
{% endif -%}
165+
,
155166
{% endif -%}
156167

157168
{# query parameters -#}

0 commit comments

Comments
 (0)