Skip to content

Commit 12cbd71

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 12cbd71

File tree

3 files changed

+71
-5
lines changed

3 files changed

+71
-5
lines changed

src/api.rs

+62-3
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
@@ -343,7 +354,7 @@ impl Operation {
343354
}
344355
}
345356

346-
let request_body_schema_name = op.request_body.and_then(|b| match b {
357+
let request_body_schema_name = op.request_body.clone().and_then(|b| match b {
347358
ReferenceOr::Item(mut req_body) => {
348359
assert!(req_body.required);
349360
assert!(req_body.extensions.is_empty());
@@ -372,6 +383,53 @@ impl Operation {
372383
}
373384
});
374385

386+
let request_body_all_optional = op
387+
.request_body
388+
.map(|r| {
389+
match r {
390+
ReferenceOr::Reference { .. } => {
391+
todo!("reference")
392+
}
393+
ReferenceOr::Item(body) => {
394+
if let Some(mt) = body.content.get("application/json") {
395+
match mt.schema.as_ref().map(|so| &so.json_schema) {
396+
Some(Schema::Object(schemars::schema::SchemaObject {
397+
object: Some(ov),
398+
..
399+
})) => {
400+
dbg!((&op_id, ov.required.is_empty()));
401+
return ov.required.is_empty();
402+
}
403+
Some(Schema::Object(schemars::schema::SchemaObject {
404+
reference: Some(s),
405+
..
406+
})) => {
407+
match component_schemas
408+
.get(
409+
&get_schema_name(Some(s.clone()))
410+
.expect("schema should exist"),
411+
)
412+
.map(|so| &so.json_schema)
413+
{
414+
Some(Schema::Object(schemars::schema::SchemaObject {
415+
object: Some(ov),
416+
..
417+
})) => {
418+
dbg!((&op_id, ov.required.is_empty()));
419+
return ov.required.is_empty();
420+
}
421+
_ => todo!("just give up at the double ref"),
422+
}
423+
}
424+
_ => {}
425+
}
426+
}
427+
}
428+
}
429+
false
430+
})
431+
.unwrap_or_default();
432+
375433
let response_body_schema_name = op.responses.and_then(|r| {
376434
assert_eq!(r.default, None);
377435
assert!(r.extensions.is_empty());
@@ -415,6 +473,7 @@ impl Operation {
415473
header_params,
416474
query_params,
417475
request_body_schema_name,
476+
request_body_all_optional,
418477
response_body_schema_name,
419478
};
420479
Some((res_path, op))

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:#?}")?;

templates/svix_cli_resource.rs.jinja

+6-1
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 -#}

0 commit comments

Comments
 (0)