summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCyborus <cyborus@cyborus.xyz>2024-01-16 23:23:02 +0100
committerCyborus <cyborus@cyborus.xyz>2024-01-16 23:23:02 +0100
commitc17c1f8638ec920d185e905f7e60e56784319229 (patch)
treecaf9f2974a844ef0b69e1decbd04387bd3d2315e
parentautogenerate structs (diff)
downloadforgejo-api-c17c1f8638ec920d185e905f7e60e56784319229.tar.xz
forgejo-api-c17c1f8638ec920d185e905f7e60e56784319229.zip
generate query structs
-rw-r--r--generator/src/main.rs339
-rw-r--r--generator/src/openapi.rs2
2 files changed, 313 insertions, 28 deletions
diff --git a/generator/src/main.rs b/generator/src/main.rs
index 04a3ede..26e56e7 100644
--- a/generator/src/main.rs
+++ b/generator/src/main.rs
@@ -5,8 +5,8 @@ mod openapi;
use eyre::Context;
use heck::{ToPascalCase, ToSnakeCase};
use openapi::{
- MaybeRef, OpenApiV2, Operation, Parameter, ParameterIn, ParameterType, Primitive, Response,
- Schema, SchemaType,
+ CollectionFormat, Items, MaybeRef, OpenApiV2, Operation, Parameter, ParameterIn, ParameterType,
+ Primitive, Response, Schema, SchemaType,
};
use std::fmt::Write;
@@ -26,6 +26,10 @@ fn main() -> eyre::Result<()> {
s.push_str(&strukt);
}
}
+ for (path, item) in &spec.paths {
+ let strukt = create_query_structs_for_path(&spec, path, item)?;
+ s.push_str(&strukt);
+ }
save_generated(&mut s)?;
Ok(())
}
@@ -84,7 +88,9 @@ fn fn_signature_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String
.replace("o_auth2", "oauth2");
let args = fn_args_from_op(spec, op)?;
let ty = fn_return_from_op(spec, op)?;
- Ok(format!("pub async fn {name}({args}) -> Result<{ty}, ForgejoError>"))
+ Ok(format!(
+ "pub async fn {name}({args}) -> Result<{ty}, ForgejoError>"
+ ))
}
fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
@@ -100,11 +106,11 @@ fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
};
match param._in {
ParameterIn::Path => {
- let type_name = path_param_type(&param)?;
+ let type_name = param_type(&param, false)?;
args.push_str(", ");
args.push_str(&sanitize_ident(param.name.to_snake_case()));
args.push_str(": ");
- args.push_str(type_name);
+ args.push_str(&type_name);
}
ParameterIn::Query => has_query = true,
ParameterIn::Header => has_headers = true,
@@ -249,32 +255,71 @@ fn schema_type_name(
}
}
-fn path_param_type(param: &Parameter) -> eyre::Result<&'static str> {
+fn param_type(param: &Parameter, owned: bool) -> eyre::Result<String> {
let _type = param
._type
.as_ref()
.ok_or_else(|| eyre::eyre!("no type provided for path param"))?;
- let type_name = match _type {
- ParameterType::String => match param.format.as_deref() {
+ param_type_inner(_type, param.format.as_deref(), param.items.as_ref(), owned)
+}
+
+fn param_type_inner(
+ ty: &ParameterType,
+ format: Option<&str>,
+ items: Option<&Items>,
+ owned: bool,
+) -> eyre::Result<String> {
+ let ty_name = match ty {
+ ParameterType::String => match format.as_deref() {
Some("date") => "time::Date",
Some("date-time") => "time::OffsetDateTime",
- _ => "&str",
- },
- ParameterType::Number => match param.format.as_deref() {
+ _ => {
+ if owned {
+ "String"
+ } else {
+ "&str"
+ }
+ }
+ }
+ .into(),
+ ParameterType::Number => match format.as_deref() {
Some("float") => "f32",
Some("double") => "f64",
_ => "f64",
- },
- ParameterType::Integer => match param.format.as_deref() {
+ }
+ .into(),
+ ParameterType::Integer => match format.as_deref() {
Some("int32") => "u32",
Some("int64") => "u64",
_ => "u32",
- },
- ParameterType::Boolean => "bool",
- ParameterType::Array => eyre::bail!("todo: support returning arrays"),
- ParameterType::File => "Vec<u8>",
+ }
+ .into(),
+ ParameterType::Boolean => "bool".into(),
+ ParameterType::Array => {
+ let item = items
+ .as_ref()
+ .ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
+ let item_ty_name = param_type_inner(
+ &item._type,
+ item.format.as_deref(),
+ item.items.as_deref(),
+ owned,
+ )?;
+ if owned {
+ format!("Vec<{item_ty_name}>")
+ } else {
+ format!("&[{item_ty_name}]")
+ }
+ }
+ ParameterType::File => {
+ if owned {
+ format!("Vec<u8>")
+ } else {
+ format!("&[u8]")
+ }
+ }
};
- Ok(type_name)
+ Ok(ty_name)
}
fn method_docs(op: &Operation) -> eyre::Result<String> {
@@ -403,7 +448,7 @@ fn create_method_request(
let mut fmt_args = String::new();
if has_query {
fmt_str.push_str("?{}");
- fmt_args.push_str(", query");
+ fmt_args.push_str(", query.to_string()");
}
let path_arg = if fmt_str.contains("{") {
format!("&format!(\"{fmt_str}\"{fmt_args})")
@@ -579,12 +624,59 @@ fn create_patch_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
fn sanitize_ident(mut s: String) -> String {
let keywords = [
- "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
- "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
- "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
- "use", "where", "while", "abstract", "become", "box", "do", "final", "macro", "override",
- "priv", "typeof", "unsized", "virtual", "yield", "async", "await", "dyn", "try",
- "macro_rules", "union"
+ "as",
+ "break",
+ "const",
+ "continue",
+ "crate",
+ "else",
+ "enum",
+ "extern",
+ "false",
+ "fn",
+ "for",
+ "if",
+ "impl",
+ "in",
+ "let",
+ "loop",
+ "match",
+ "mod",
+ "move",
+ "mut",
+ "pub",
+ "ref",
+ "return",
+ "self",
+ "Self",
+ "static",
+ "struct",
+ "super",
+ "trait",
+ "true",
+ "type",
+ "unsafe",
+ "use",
+ "where",
+ "while",
+ "abstract",
+ "become",
+ "box",
+ "do",
+ "final",
+ "macro",
+ "override",
+ "priv",
+ "typeof",
+ "unsized",
+ "virtual",
+ "yield",
+ "async",
+ "await",
+ "dyn",
+ "try",
+ "macro_rules",
+ "union",
];
if s == "self" {
s = "this".into();
@@ -595,7 +687,11 @@ fn sanitize_ident(mut s: String) -> String {
s
}
-fn create_struct_for_definition(spec: &OpenApiV2, name: &str, schema: &Schema) -> eyre::Result<String> {
+fn create_struct_for_definition(
+ spec: &OpenApiV2,
+ name: &str,
+ schema: &Schema,
+) -> eyre::Result<String> {
if matches!(schema._type, Some(SchemaType::One(Primitive::Array))) {
return Ok(String::new());
}
@@ -633,8 +729,197 @@ fn create_struct_docs(schema: &Schema) -> eyre::Result<String> {
out.push_str("\n/// \n");
}
out
- },
+ }
None => String::new(),
};
Ok(doc)
}
+
+fn create_query_structs_for_path(
+ spec: &OpenApiV2,
+ path: &str,
+ item: &openapi::PathItem,
+) -> eyre::Result<String> {
+ let mut s = String::new();
+ if let Some(op) = &item.get {
+ s.push_str(&create_query_struct(spec, path, op).wrap_err("GET")?);
+ }
+ if let Some(op) = &item.put {
+ s.push_str(&create_query_struct(spec, path, op).wrap_err("PUT")?);
+ }
+ if let Some(op) = &item.post {
+ s.push_str(&create_query_struct(spec, path, op).wrap_err("POST")?);
+ }
+ if let Some(op) = &item.delete {
+ s.push_str(&create_query_struct(spec, path, op).wrap_err("DELETE")?);
+ }
+ if let Some(op) = &item.options {
+ s.push_str(&create_query_struct(spec, path, op).wrap_err("OPTIONS")?);
+ }
+ if let Some(op) = &item.head {
+ s.push_str(&create_query_struct(spec, path, op).wrap_err("HEAD")?);
+ }
+ if let Some(op) = &item.patch {
+ s.push_str(&create_query_struct(spec, path, op).wrap_err("PATCH")?);
+ }
+ Ok(s)
+}
+
+fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let params = match &op.parameters {
+ Some(params) => params,
+ None => return Ok(String::new()),
+ };
+
+ let mut fields = String::new();
+ let mut imp = String::new();
+ imp.push_str("let mut s = String::new();\n");
+ for param in params {
+ let param = match &param {
+ MaybeRef::Value { value } => value,
+ MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
+ };
+ if param._in == ParameterIn::Query {
+ let ty = param_type(param, true)?;
+ let field_name = sanitize_ident(param.name.to_snake_case());
+ let required = param.required.unwrap_or_default();
+ fields.push_str(&field_name);
+ fields.push_str(": ");
+ if required {
+ fields.push_str(&ty);
+ } else {
+ fields.push_str("Option<");
+ fields.push_str(&ty);
+ fields.push_str(">");
+ }
+ fields.push_str(",\n");
+
+ let mut handler = String::new();
+ let ty = param
+ ._type
+ .as_ref()
+ .ok_or_else(|| eyre::eyre!("no type provided for query field"))?;
+ if required {
+ writeln!(&mut handler, "let {field_name} = self.{field_name};")?;
+ } else {
+ writeln!(
+ &mut handler,
+ "if let Some({field_name}) = self.{field_name} {{"
+ )?;
+ }
+ match ty {
+ ParameterType::String => match param.format.as_deref() {
+ Some("date-time" | "date") => {
+ writeln!(&mut handler, "s.push_str(\"{}=\");", param.name)?;
+ writeln!(&mut handler, "{field_name}.format_into(&mut s, &time::format_description::well_known::Rfc3339).unwrap();")?;
+ writeln!(&mut handler, "s.push('&');")?;
+ }
+ _ => {
+ writeln!(&mut handler, "s.push_str(\"{}=\");", param.name)?;
+ writeln!(&mut handler, "s.push_str(&{field_name});")?;
+ writeln!(&mut handler, "s.push('&');")?;
+ }
+ },
+ ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => {
+ writeln!(
+ &mut handler,
+ "s.write_fmt(format_args!(\"{}={{}}&\", {field_name})).unwrap();",
+ param.name
+ )?;
+ }
+ ParameterType::Array => {
+ let format = param.collection_format.unwrap_or(CollectionFormat::Csv);
+ let item = param
+ .items
+ .as_ref()
+ .ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
+ let item_pusher = match item._type {
+ ParameterType::String => {
+ match param.format.as_deref() {
+ Some("date-time" | "date") => {
+ "item.format_into(&mut s, &time::format_description::well_known::Rfc3339).unwrap();"
+ },
+ _ => {
+ "s.push_str(&item);"
+ }
+ }
+ },
+ ParameterType::Number |
+ ParameterType::Integer |
+ ParameterType::Boolean => {
+ "s.write_fmt(format_args!(\"{{item}}\")).unwrap();"
+ },
+ ParameterType::Array => {
+ eyre::bail!("nested arrays not supported in query");
+ },
+ ParameterType::File => eyre::bail!("cannot send file in query"),
+ };
+ match format {
+ CollectionFormat::Csv => {
+ handler.push_str(&simple_query_array(param, item_pusher, &field_name, ",")?);
+ }
+ CollectionFormat::Ssv => {
+ handler.push_str(&simple_query_array(param, item_pusher, &field_name, " ")?);
+ }
+ CollectionFormat::Tsv => {
+ handler.push_str(&simple_query_array(param, item_pusher, &field_name, "\\t")?);
+ }
+ CollectionFormat::Pipes => {
+ handler.push_str(&simple_query_array(param, item_pusher, &field_name, "|")?);
+ }
+ CollectionFormat::Multi => {
+ writeln!(&mut handler, "")?;
+ writeln!(&mut handler, "if !{field_name}.is_empty() {{")?;
+ writeln!(
+ &mut handler,
+ "for (i, item) in {field_name}.iter().enumerate() {{"
+ )?;
+ writeln!(&mut handler, "s.push_str(\"{}=\");", param.name)?;
+ handler.push_str(item_pusher);
+ handler.push('\n');
+ writeln!(&mut handler, "s.push('&')")?;
+ writeln!(&mut handler, "}}")?;
+ writeln!(&mut handler, "}}")?;
+ }
+ }
+ }
+ ParameterType::File => eyre::bail!("cannot send file in query"),
+ }
+ if !required {
+ writeln!(&mut handler, "}}")?;
+ }
+ imp.push_str(&handler);
+ }
+ }
+ imp.push_str("s\n");
+ if fields.is_empty() {
+ return Ok(String::new());
+ } else {
+ let op_name = op
+ .operation_id
+ .as_ref()
+ .ok_or_else(|| eyre::eyre!("no op id found"))?
+ .to_pascal_case();
+ return Ok(format!("pub struct {op_name}Query {{\n{fields}\n}}\n\nimpl {op_name}Query {{\nfn to_string(&self) -> String {{\n{imp}\n}}\n}}"));
+ }
+}
+
+fn simple_query_array(param: &Parameter, item_pusher: &str, name: &str, sep: &str) -> eyre::Result<String,> {
+ let mut out = String::new();
+ writeln!(&mut out, "s.push_str(\"{}=\");", param.name)?;
+ writeln!(&mut out, "")?;
+ writeln!(&mut out, "if !{name}.is_empty() {{")?;
+ writeln!(
+ &mut out,
+ "for (i, item) in {name}.iter().enumerate() {{"
+ )?;
+ out.push_str(item_pusher);
+ out.push('\n');
+ writeln!(&mut out, "if i < {name}.len() - 1 {{")?;
+ writeln!(&mut out, "s.push('{sep}')")?;
+ writeln!(&mut out, "}}")?;
+ writeln!(&mut out, "}}")?;
+ writeln!(&mut out, "s.push('&')")?;
+ writeln!(&mut out, "}}")?;
+ Ok(out)
+}
diff --git a/generator/src/openapi.rs b/generator/src/openapi.rs
index f86ab88..79a57d3 100644
--- a/generator/src/openapi.rs
+++ b/generator/src/openapi.rs
@@ -139,7 +139,7 @@ pub enum ParameterType {
File,
}
-#[derive(serde::Deserialize, Debug, PartialEq)]
+#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)]
#[serde(rename_all(deserialize = "camelCase"))]
pub enum CollectionFormat {
Csv,