summaryrefslogtreecommitdiffstats
path: root/generator
diff options
context:
space:
mode:
authorCyborus <cyborus@cyborus.xyz>2024-01-27 18:47:51 +0100
committerCyborus <cyborus@cyborus.xyz>2024-01-27 19:00:11 +0100
commit2c467ea6cf02f87fb0663734b280cc323fa894da (patch)
tree3e120734faae4d12591b3fefa3dd85bf232ccf73 /generator
parentremove debug panic (diff)
downloadforgejo-api-2c467ea6cf02f87fb0663734b280cc323fa894da.tar.xz
forgejo-api-2c467ea6cf02f87fb0663734b280cc323fa894da.zip
split generator into modules
Diffstat (limited to 'generator')
-rw-r--r--generator/src/main.rs904
-rw-r--r--generator/src/methods.rs562
-rw-r--r--generator/src/structs.rs338
3 files changed, 906 insertions, 898 deletions
diff --git a/generator/src/main.rs b/generator/src/main.rs
index 0342a7b..4e4054c 100644
--- a/generator/src/main.rs
+++ b/generator/src/main.rs
@@ -1,38 +1,17 @@
use std::ffi::{OsStr, OsString};
+mod methods;
mod openapi;
+mod structs;
-use eyre::Context;
-use heck::{ToPascalCase, ToSnakeCase};
-use openapi::{
- CollectionFormat, Items, MaybeRef, OpenApiV2, Operation, Parameter, ParameterIn, ParameterType,
- Primitive, Response, Schema, SchemaType,
-};
-use std::fmt::Write;
+use heck::ToSnakeCase;
+use openapi::*;
fn main() -> eyre::Result<()> {
let spec = get_spec()?;
let mut s = String::new();
- s.push_str("use crate::ForgejoError;\n");
- s.push_str("impl crate::Forgejo {\n");
- for (path, item) in &spec.paths {
- s.push_str(&create_methods_for_path(&spec, path, item).wrap_err_with(|| path.clone())?);
- }
- s.push_str("}\n");
-
- s.push_str("use structs::*;\n");
- s.push_str("pub mod structs {\n");
- if let Some(definitions) = &spec.definitions {
- for (name, schema) in definitions {
- let strukt = create_struct_for_definition(&spec, name, schema)?;
- s.push_str(&strukt);
- }
- }
- for (path, item) in &spec.paths {
- let strukt = create_query_structs_for_path(&spec, path, item)?;
- s.push_str(&strukt);
- }
- s.push_str("\n}");
+ s.push_str(&methods::create_methods(&spec)?);
+ s.push_str(&structs::create_structs(&spec)?);
save_generated(&s)?;
Ok(())
}
@@ -65,187 +44,6 @@ fn run_rustfmt_on(path: &OsStr) {
}
}
-fn create_methods_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_get_method(spec, path, op).wrap_err("GET")?);
- }
- if let Some(op) = &item.put {
- s.push_str(&create_put_method(spec, path, op).wrap_err("PUT")?);
- }
- if let Some(op) = &item.post {
- s.push_str(&create_post_method(spec, path, op).wrap_err("POST")?);
- }
- if let Some(op) = &item.delete {
- s.push_str(&create_delete_method(spec, path, op).wrap_err("DELETE")?);
- }
- if let Some(op) = &item.options {
- s.push_str(&create_options_method(spec, path, op).wrap_err("OPTIONS")?);
- }
- if let Some(op) = &item.head {
- s.push_str(&create_head_method(spec, path, op).wrap_err("HEAD")?);
- }
- if let Some(op) = &item.patch {
- s.push_str(&create_patch_method(spec, path, op).wrap_err("PATCH")?);
- }
- Ok(s)
-}
-
-fn fn_signature_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
- let name = op
- .operation_id
- .as_deref()
- .ok_or_else(|| eyre::eyre!("operation did not have id"))?
- .to_snake_case()
- .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>"
- ))
-}
-
-fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
- let mut args = "&self".to_string();
- let mut has_query = false;
- let mut has_headers = false;
- let mut has_form = false;
- if let Some(params) = &op.parameters {
- for param in params {
- let param = match &param {
- MaybeRef::Value { value } => value,
- MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
- };
- match param._in {
- ParameterIn::Path => {
- let type_name = param_type(&param, false)?;
- args.push_str(", ");
- args.push_str(&sanitize_ident(&param.name));
- args.push_str(": ");
- args.push_str(&type_name);
- }
- ParameterIn::Query => has_query = true,
- ParameterIn::Header => has_headers = true,
- ParameterIn::Body => {
- let schema_ref = param.schema.as_ref().unwrap();
- let ty = schema_ref_type_name(spec, &schema_ref)?;
- args.push_str(", ");
- args.push_str(&sanitize_ident(&param.name));
- args.push_str(": ");
- args.push_str(&ty);
- }
- ParameterIn::FormData => {
- args.push_str(", ");
- args.push_str(&sanitize_ident(&param.name));
- args.push_str(": Vec<u8>");
- }
- }
- }
- }
- if has_query {
- let query_ty = query_struct_name(op)?;
- args.push_str(", query: ");
- args.push_str(&query_ty);
- }
- Ok(args)
-}
-
-fn query_struct_name(op: &Operation) -> eyre::Result<String> {
- let mut ty = op
- .operation_id
- .as_deref()
- .ok_or_else(|| eyre::eyre!("operation did not have id"))?
- .to_pascal_case()
- .replace("o_auth2", "oauth2");
- ty.push_str("Query");
- Ok(ty)
-}
-
-fn fn_return_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<ResponseType> {
- let mut responses = op
- .responses
- .http_codes
- .iter()
- .filter(|(k, _)| k.starts_with("2"))
- .map(|(_, v)| response_ref_type_name(spec, v))
- .collect::<Result<Vec<_>, _>>()?;
- let mut iter = responses.into_iter();
- let mut response = iter
- .next()
- .ok_or_else(|| eyre::eyre!("must have at least one response type"))?;
- for next in iter {
- response = response.merge(next)?;
- }
-
- Ok(response)
-}
-
-#[derive(Debug, Default)]
-struct ResponseType {
- headers: Option<String>,
- body: Option<String>,
-}
-
-impl ResponseType {
- fn merge(self, other: Self) -> eyre::Result<Self> {
- let mut new = Self::default();
- match (self.headers, other.headers) {
- (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"),
- (Some(a), None) => new.headers = Some(format!("Option<{a}>")),
- (None, Some(b)) => new.headers = Some(format!("Option<{b}>")),
- (a, b) => new.headers = a.or(b),
- };
- match (self.body.as_deref(), other.body.as_deref()) {
- (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"),
- (Some(a), Some("()") | None) => new.body = Some(format!("Option<{a}>")),
- (Some("()") | None, Some(b)) => new.body = Some(format!("Option<{b}>")),
- (a, b) => new.body = self.body.or(other.body),
- };
- Ok(new)
- }
-}
-
-impl std::fmt::Display for ResponseType {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let mut tys = Vec::new();
- tys.extend(self.headers.as_deref());
- tys.extend(self.body.as_deref());
- match tys[..] {
- [single] => f.write_str(single),
- _ => {
- write!(f, "(")?;
- for (i, item) in tys.iter().copied().enumerate() {
- f.write_str(item)?;
- if i + 1 < tys.len() {
- write!(f, ", ")?;
- }
- }
- write!(f, ")")?;
- Ok(())
- }
- }
- }
-}
-
-fn response_ref_type_name(
- spec: &OpenApiV2,
- schema: &MaybeRef<Response>,
-) -> eyre::Result<ResponseType> {
- let (_, response) = deref_response(spec, schema)?;
- let mut ty = ResponseType::default();
- if response.headers.is_some() {
- ty.headers = Some("reqwest::header::HeaderMap".into());
- }
- if let Some(schema) = &response.schema {
- ty.body = Some(schema_ref_type_name(spec, schema)?);
- };
- Ok(ty)
-}
-
fn schema_ref_type_name(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result<String> {
let (name, schema) = deref_definition(spec, &schema)?;
schema_type_name(spec, name, schema)
@@ -306,125 +104,6 @@ fn schema_type_name(
}
}
-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"))?;
- 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",
- _ => {
- if owned {
- "String"
- } else {
- "&str"
- }
- }
- }
- .into(),
- ParameterType::Number => match format.as_deref() {
- Some("float") => "f32",
- Some("double") => "f64",
- _ => "f64",
- }
- .into(),
- ParameterType::Integer => match format.as_deref() {
- Some("int32") => "u32",
- Some("int64") => "u64",
- _ => "u32",
- }
- .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(ty_name)
-}
-
-fn method_docs(op: &Operation) -> eyre::Result<String> {
- let mut out = String::new();
- let mut prev = false;
- if let Some(summary) = &op.summary {
- write!(&mut out, "/// {summary}\n")?;
- prev = true;
- }
- if let Some(params) = &op.parameters {
- if prev {
- out.push_str("///\n");
- }
- for param in params {
- let param = match &param {
- MaybeRef::Value { value } => value,
- MaybeRef::Ref { _ref } => eyre::bail!("pipis"),
- };
- match param._in {
- ParameterIn::Path | ParameterIn::Body | ParameterIn::FormData => {
- write!(&mut out, "/// - `{}`", param.name)?;
- if let Some(description) = &param.description {
- write!(&mut out, ": {}", description)?;
- }
- writeln!(&mut out)?;
- }
- _ => (),
- }
- }
- }
- Ok(out)
-}
-
-fn deref_response<'a>(
- spec: &'a OpenApiV2,
- r: &'a MaybeRef<Response>,
-) -> eyre::Result<(Option<&'a str>, &'a Response)> {
- let r = match r {
- MaybeRef::Value { value } => return Ok((None, value)),
- MaybeRef::Ref { _ref } => _ref,
- };
- let name = r
- .strip_prefix("#/responses/")
- .ok_or_else(|| eyre::eyre!("invalid response reference"))?;
- let global_responses = spec
- .responses
- .as_ref()
- .ok_or_else(|| eyre::eyre!("no global responses"))?;
- let response = global_responses
- .get(name)
- .ok_or_else(|| eyre::eyre!("referenced response does not exist"))?;
- Ok((Some(name), response))
-}
-
fn deref_definition<'a>(
spec: &'a OpenApiV2,
r: &'a MaybeRef<Schema>,
@@ -446,200 +125,6 @@ fn deref_definition<'a>(
Ok((Some(name), definition))
}
-fn create_method_body(
- spec: &OpenApiV2,
- method: &str,
- path: &str,
- op: &Operation,
-) -> eyre::Result<String> {
- let request = create_method_request(spec, method, path, op)?;
- let response = create_method_response(spec, method, path, op)?;
- Ok(format!("{request}\n {response}"))
-}
-
-fn create_method_request(
- spec: &OpenApiV2,
- method: &str,
- path: &str,
- op: &Operation,
-) -> eyre::Result<String> {
- let mut has_query = false;
- let mut has_headers = false;
- let mut body_method = String::new();
- if let Some(params) = &op.parameters {
- for param in params {
- let param = match &param {
- MaybeRef::Value { value } => value,
- MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
- };
- let name = sanitize_ident(&param.name);
- match param._in {
- ParameterIn::Path => (/* do nothing */),
- ParameterIn::Query => has_query = true,
- ParameterIn::Header => has_headers = true,
- ParameterIn::Body => {
- if !body_method.is_empty() {
- eyre::bail!("cannot have more than one body parameter");
- }
- if param_is_string(spec, param)? {
- body_method = format!(".body({name})");
- } else {
- body_method = format!(".json(&{name})");
- }
- }
- ParameterIn::FormData => {
- if !body_method.is_empty() {
- eyre::bail!("cannot have more than one body parameter");
- }
- body_method = format!(".multipart(reqwest::multipart::Form::new().part(\"attachment\", reqwest::multipart::Part::bytes({name}).file_name(\"file\").mime_str(\"*/*\").unwrap()))");
- }
- }
- }
- }
- let mut fmt_str = sanitize_path_arg(path)?;
- let mut fmt_args = String::new();
- if has_query {
- fmt_str.push_str("?{}");
- fmt_args.push_str(", query.to_string()");
- }
- let path_arg = if fmt_str.contains("{") {
- format!("&format!(\"{fmt_str}\"{fmt_args})")
- } else {
- format!("\"{fmt_str}\"")
- };
-
- let out = format!("let request = self.{method}({path_arg}){body_method}.build()?;");
- Ok(out)
-}
-
-fn sanitize_path_arg(mut path: &str) -> eyre::Result<String> {
- let mut out = String::new();
- loop {
- let (head, tail) = match path.split_once("{") {
- Some(i) => i,
- None => {
- out.push_str(path);
- break;
- }
- };
- path = tail;
- out.push_str(head);
- out.push('{');
- let (head, tail) = match path.split_once("}") {
- Some(i) => i,
- None => {
- eyre::bail!("unmatched bracket");
- }
- };
- path = tail;
- out.push_str(&head.to_snake_case());
- out.push('}');
- }
- if out.starts_with("/") {
- out.remove(0);
- }
- Ok(out)
-}
-
-fn create_method_response(
- spec: &OpenApiV2,
- method: &str,
- path: &str,
- op: &Operation,
-) -> eyre::Result<String> {
- let mut has_empty = false;
- let mut only_empty = true;
- for (code, res) in &op.responses.http_codes {
- let response = response_ref_type_name(spec, res)?;
- if !code.starts_with("2") {
- continue;
- }
- if matches!(response.body.as_deref(), Some("()") | None) {
- has_empty = true;
- } else {
- only_empty = false;
- }
- }
- let fn_ret = fn_return_from_op(spec, op)?;
- let optional = has_empty && !only_empty;
- let mut out = String::new();
- out.push_str("let response = self.execute(request).await?;\n");
- out.push_str("match response.status().as_u16() {\n");
- for (code, res) in &op.responses.http_codes {
- let (_, res) = deref_response(spec, res)?;
- if !code.starts_with("2") {
- continue;
- }
- out.push_str(code);
- out.push_str(" => Ok(");
- let mut handlers = Vec::new();
- let header_handler = match &res.headers {
- Some(_) => {
- if fn_ret
- .headers
- .as_ref()
- .map(|s| s.starts_with("Option<"))
- .unwrap()
- {
- Some("Some(response.headers().clone())")
- } else {
- Some("response.headers().clone()")
- }
- }
- None => {
- if fn_ret.headers.is_some() {
- Some("None")
- } else {
- None
- }
- }
- };
- handlers.extend(header_handler);
- let body_handler = match &res.schema {
- Some(schema) if schema_is_string(spec, schema)? => {
- if optional {
- Some("Some(response.text().await?)")
- } else {
- Some("response.text().await?")
- }
- }
- Some(_) => {
- if optional {
- Some("Some(response.json().await?)")
- } else {
- Some("response.json().await?")
- }
- }
- None => {
- if optional {
- Some("None")
- } else {
- None
- }
- }
- };
- handlers.extend(body_handler);
- match handlers[..] {
- [single] => out.push_str(single),
- _ => {
- out.push('(');
- for (i, item) in handlers.iter().copied().enumerate() {
- out.push_str(item);
- if i + 1 < handlers.len() {
- out.push_str(", ");
- }
- }
- out.push(')');
- }
- }
- out.push_str("),\n");
- }
- out.push_str("_ => Err(ForgejoError::UnexpectedStatusCode(response.status()))\n");
- out.push_str("}\n");
-
- Ok(out)
-}
-
fn schema_is_string(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result<bool> {
let (_, schema) = deref_definition(spec, schema)?;
let is_str = match schema._type {
@@ -649,74 +134,6 @@ fn schema_is_string(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result
Ok(is_str)
}
-fn param_is_string(spec: &OpenApiV2, param: &Parameter) -> eyre::Result<bool> {
- match param._in {
- ParameterIn::Body => {
- let schema_ref = param
- .schema
- .as_ref()
- .ok_or_else(|| eyre::eyre!("body param did not have schema"))?;
- schema_is_string(spec, schema_ref)
- }
- _ => {
- let is_str = match param._type {
- Some(ParameterType::String) => true,
- _ => false,
- };
- Ok(is_str)
- }
- }
-}
-
-fn create_get_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
- let doc = method_docs(op)?;
- let sig = fn_signature_from_op(spec, op)?;
- let body = create_method_body(spec, "get", path, op)?;
- Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
-}
-
-fn create_put_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
- let doc = method_docs(op)?;
- let sig = fn_signature_from_op(spec, op)?;
- let body = create_method_body(spec, "put", path, op)?;
- Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
-}
-
-fn create_post_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
- let doc = method_docs(op)?;
- let sig = fn_signature_from_op(spec, op)?;
- let body = create_method_body(spec, "post", path, op)?;
- Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
-}
-
-fn create_delete_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
- let doc = method_docs(op)?;
- let sig = fn_signature_from_op(spec, op)?;
- let body = create_method_body(spec, "delete", path, op)?;
- Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
-}
-
-fn create_options_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
- let doc = method_docs(op)?;
- let sig = fn_signature_from_op(spec, op)?;
- let body = create_method_body(spec, "options", path, op)?;
- Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
-}
-
-fn create_head_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
- let doc = method_docs(op)?;
- let sig = fn_signature_from_op(spec, op)?;
- let body = create_method_body(spec, "head", path, op)?;
- Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
-}
-
-fn create_patch_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
- let doc = method_docs(op)?;
- let sig = fn_signature_from_op(spec, op)?;
- let body = create_method_body(spec, "patch", path, op)?;
- Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
-}
-
fn sanitize_ident(s: &str) -> String {
let mut s = s.to_snake_case();
let keywords = [
@@ -782,312 +199,3 @@ fn sanitize_ident(s: &str) -> String {
}
s
}
-
-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());
- }
-
- let docs = create_struct_docs(schema)?;
- let mut fields = String::new();
- let required = schema.required.as_deref().unwrap_or_default();
- if let Some(properties) = &schema.properties {
- for (prop_name, prop_schema) in properties {
- let prop_ty = schema_ref_type_name(spec, prop_schema)?;
- let field_name = sanitize_ident(prop_name);
- let mut field_ty = prop_ty.clone();
- if field_name.ends_with("url") && field_name != "ssh_url" && field_ty == "String" {
- field_ty = "url::Url".into()
- }
- if field_ty == name {
- field_ty = format!("Box<{field_ty}>")
- }
- if !required.contains(prop_name) {
- field_ty = format!("Option<{field_ty}>")
- }
- if field_ty == "Option<url::Url>" {
- fields.push_str("#[serde(deserialize_with = \"crate::none_if_blank_url\")]\n");
- }
- if field_ty == "time::OffsetDateTime" {
- fields.push_str("#[serde(with = \"time::serde::rfc3339\")]\n");
- }
- if field_ty == "Option<time::OffsetDateTime>" {
- fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n");
- }
- if &field_name != prop_name {
- fields.push_str("#[serde(rename = \"");
- fields.push_str(prop_name);
- fields.push_str("\")]\n");
- }
- fields.push_str("pub ");
- fields.push_str(&field_name);
- fields.push_str(": ");
- fields.push_str(&field_ty);
- fields.push_str(",\n");
- }
- }
-
- if let Some(additonal_schema) = &schema.additional_properties {
- let prop_ty = schema_ref_type_name(spec, additonal_schema)?;
- fields.push_str("#[serde(flatten)]\n");
- fields.push_str("pub additional: std::collections::BTreeMap<String, ");
- fields.push_str(&prop_ty);
- fields.push_str(">,\n");
- }
-
- let out = format!("{docs}#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct {name} {{\n{fields}}}\n\n");
- Ok(out)
-}
-
-fn create_struct_docs(schema: &Schema) -> eyre::Result<String> {
- let doc = match &schema.description {
- Some(desc) => {
- let mut out = String::new();
- for line in desc.lines() {
- out.push_str("/// ");
- out.push_str(line);
- 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();
- 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);
- let required = param.required.unwrap_or_default();
- fields.push_str("pub ");
- 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,
- "write!(f, \"{}={{field_name}}&\", field_name = {field_name}.format(&time::format_description::well_known::Rfc3339).unwrap())?;",
- param.name)?;
- }
- _ => {
- writeln!(
- &mut handler,
- "write!(f, \"{}={{{}}}&\")?;",
- param.name,
- field_name.strip_prefix("r#").unwrap_or(&field_name)
- )?;
- }
- },
- ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => {
- writeln!(
- &mut handler,
- "write!(f, \"{}={{{}}}&\")?;",
- param.name,
- field_name.strip_prefix("r#").unwrap_or(&field_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") => {
- "write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;"
- },
- _ => {
- "write!(f, \"{item}\")?;"
- }
- }
- },
- ParameterType::Number |
- ParameterType::Integer |
- ParameterType::Boolean => {
- "write!(f, \"{item}\")?;"
- },
- 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 item in {field_name} {{")?;
- writeln!(&mut handler, "write!(f, \"{}=\")?;", param.name)?;
- handler.push_str(item_pusher);
- handler.push('\n');
- writeln!(&mut handler, "write!(f, \"&\")?;")?;
- writeln!(&mut handler, "}}")?;
- writeln!(&mut handler, "}}")?;
- }
- }
- }
- ParameterType::File => eyre::bail!("cannot send file in query"),
- }
- if !required {
- writeln!(&mut handler, "}}")?;
- }
- imp.push_str(&handler);
- }
- }
-
- let result = if fields.is_empty() {
- String::new()
- } else {
- let op_name = op
- .operation_id
- .as_ref()
- .ok_or_else(|| eyre::eyre!("no op id found"))?
- .to_pascal_case();
- format!(
- "
-pub struct {op_name}Query {{
- {fields}
-}}
-
-impl std::fmt::Display for {op_name}Query {{
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
- {imp}
- Ok(())
- }}
-}}
-"
- )
- };
-
- Ok(result)
-}
-
-fn simple_query_array(
- param: &Parameter,
- item_pusher: &str,
- name: &str,
- sep: &str,
-) -> eyre::Result<String> {
- let mut out = String::new();
-
- writeln!(
- &mut out,
- "
-if !{name}.is_empty() {{
- write!(f, \"{}=\")?;
- for (item, i) in {name}.iter().enumerate() {{
- {item_pusher}
- if i < {name}.len() - 1 {{
- write!(f, \"{sep}\")?;
- }}
- }}
- write!(f, \"&\")?;
-}}",
- param.name
- )?;
-
- Ok(out)
-}
diff --git a/generator/src/methods.rs b/generator/src/methods.rs
new file mode 100644
index 0000000..aeebf30
--- /dev/null
+++ b/generator/src/methods.rs
@@ -0,0 +1,562 @@
+use crate::openapi::*;
+use eyre::WrapErr;
+use heck::ToSnakeCase;
+use std::fmt::Write;
+
+pub fn create_methods(spec: &OpenApiV2) -> eyre::Result<String> {
+ let mut s = String::new();
+ s.push_str("use crate::ForgejoError;\n");
+ s.push_str("impl crate::Forgejo {\n");
+ for (path, item) in &spec.paths {
+ s.push_str(&create_methods_for_path(&spec, path, item).wrap_err_with(|| path.clone())?);
+ }
+ s.push_str("}\n");
+ Ok(s)
+}
+
+fn create_methods_for_path(spec: &OpenApiV2, path: &str, item: &PathItem) -> eyre::Result<String> {
+ let mut s = String::new();
+ if let Some(op) = &item.get {
+ s.push_str(&create_get_method(spec, path, op).wrap_err("GET")?);
+ }
+ if let Some(op) = &item.put {
+ s.push_str(&create_put_method(spec, path, op).wrap_err("PUT")?);
+ }
+ if let Some(op) = &item.post {
+ s.push_str(&create_post_method(spec, path, op).wrap_err("POST")?);
+ }
+ if let Some(op) = &item.delete {
+ s.push_str(&create_delete_method(spec, path, op).wrap_err("DELETE")?);
+ }
+ if let Some(op) = &item.options {
+ s.push_str(&create_options_method(spec, path, op).wrap_err("OPTIONS")?);
+ }
+ if let Some(op) = &item.head {
+ s.push_str(&create_head_method(spec, path, op).wrap_err("HEAD")?);
+ }
+ if let Some(op) = &item.patch {
+ s.push_str(&create_patch_method(spec, path, op).wrap_err("PATCH")?);
+ }
+ Ok(s)
+}
+
+fn create_get_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let doc = method_docs(op)?;
+ let sig = fn_signature_from_op(spec, op)?;
+ let body = create_method_body(spec, "get", path, op)?;
+ Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
+}
+
+fn create_put_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let doc = method_docs(op)?;
+ let sig = fn_signature_from_op(spec, op)?;
+ let body = create_method_body(spec, "put", path, op)?;
+ Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
+}
+
+fn create_post_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let doc = method_docs(op)?;
+ let sig = fn_signature_from_op(spec, op)?;
+ let body = create_method_body(spec, "post", path, op)?;
+ Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
+}
+
+fn create_delete_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let doc = method_docs(op)?;
+ let sig = fn_signature_from_op(spec, op)?;
+ let body = create_method_body(spec, "delete", path, op)?;
+ Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
+}
+
+fn create_options_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let doc = method_docs(op)?;
+ let sig = fn_signature_from_op(spec, op)?;
+ let body = create_method_body(spec, "options", path, op)?;
+ Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
+}
+
+fn create_head_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let doc = method_docs(op)?;
+ let sig = fn_signature_from_op(spec, op)?;
+ let body = create_method_body(spec, "head", path, op)?;
+ Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
+}
+
+fn create_patch_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
+ let doc = method_docs(op)?;
+ let sig = fn_signature_from_op(spec, op)?;
+ let body = create_method_body(spec, "patch", path, op)?;
+ Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n"))
+}
+
+fn method_docs(op: &Operation) -> eyre::Result<String> {
+ let mut out = String::new();
+ let mut prev = false;
+ if let Some(summary) = &op.summary {
+ write!(&mut out, "/// {summary}\n")?;
+ prev = true;
+ }
+ if let Some(params) = &op.parameters {
+ if prev {
+ out.push_str("///\n");
+ }
+ for param in params {
+ let param = match &param {
+ MaybeRef::Value { value } => value,
+ MaybeRef::Ref { _ref } => eyre::bail!("pipis"),
+ };
+ match param._in {
+ ParameterIn::Path | ParameterIn::Body | ParameterIn::FormData => {
+ write!(&mut out, "/// - `{}`", param.name)?;
+ if let Some(description) = &param.description {
+ write!(&mut out, ": {}", description)?;
+ }
+ writeln!(&mut out)?;
+ }
+ _ => (),
+ }
+ }
+ }
+ Ok(out)
+}
+
+fn fn_signature_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
+ let name = op
+ .operation_id
+ .as_deref()
+ .ok_or_else(|| eyre::eyre!("operation did not have id"))?
+ .to_snake_case()
+ .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>"
+ ))
+}
+
+fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
+ let mut args = "&self".to_string();
+ let mut has_query = false;
+ let mut has_headers = false;
+ let mut has_form = false;
+ if let Some(params) = &op.parameters {
+ for param in params {
+ let param = match &param {
+ MaybeRef::Value { value } => value,
+ MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
+ };
+ match param._in {
+ ParameterIn::Path => {
+ let type_name = param_type(&param, false)?;
+ args.push_str(", ");
+ args.push_str(&crate::sanitize_ident(&param.name));
+ args.push_str(": ");
+ args.push_str(&type_name);
+ }
+ ParameterIn::Query => has_query = true,
+ ParameterIn::Header => has_headers = true,
+ ParameterIn::Body => {
+ let schema_ref = param.schema.as_ref().unwrap();
+ let ty = crate::schema_ref_type_name(spec, &schema_ref)?;
+ args.push_str(", ");
+ args.push_str(&crate::sanitize_ident(&param.name));
+ args.push_str(": ");
+ args.push_str(&ty);
+ }
+ ParameterIn::FormData => {
+ args.push_str(", ");
+ args.push_str(&crate::sanitize_ident(&param.name));
+ args.push_str(": Vec<u8>");
+ }
+ }
+ }
+ }
+ if has_query {
+ let query_ty = crate::structs::query_struct_name(op)?;
+ args.push_str(", query: ");
+ args.push_str(&query_ty);
+ }
+ Ok(args)
+}
+
+pub 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"))?;
+ 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",
+ _ => {
+ if owned {
+ "String"
+ } else {
+ "&str"
+ }
+ }
+ }
+ .into(),
+ ParameterType::Number => match format.as_deref() {
+ Some("float") => "f32",
+ Some("double") => "f64",
+ _ => "f64",
+ }
+ .into(),
+ ParameterType::Integer => match format.as_deref() {
+ Some("int32") => "u32",
+ Some("int64") => "u64",
+ _ => "u32",
+ }
+ .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(ty_name)
+}
+
+fn fn_return_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<ResponseType> {
+ let responses = op
+ .responses
+ .http_codes
+ .iter()
+ .filter(|(k, _)| k.starts_with("2"))
+ .map(|(_, v)| response_ref_type_name(spec, v))
+ .collect::<Result<Vec<_>, _>>()?;
+ let mut iter = responses.into_iter();
+ let mut response = iter
+ .next()
+ .ok_or_else(|| eyre::eyre!("must have at least one response type"))?;
+ for next in iter {
+ response = response.merge(next)?;
+ }
+
+ Ok(response)
+}
+
+fn response_ref_type_name(
+ spec: &OpenApiV2,
+ schema: &MaybeRef<Response>,
+) -> eyre::Result<ResponseType> {
+ let (_, response) = deref_response(spec, schema)?;
+ let mut ty = ResponseType::default();
+ if response.headers.is_some() {
+ ty.headers = Some("reqwest::header::HeaderMap".into());
+ }
+ if let Some(schema) = &response.schema {
+ ty.body = Some(crate::schema_ref_type_name(spec, schema)?);
+ };
+ Ok(ty)
+}
+
+fn deref_response<'a>(
+ spec: &'a OpenApiV2,
+ r: &'a MaybeRef<Response>,
+) -> eyre::Result<(Option<&'a str>, &'a Response)> {
+ let r = match r {
+ MaybeRef::Value { value } => return Ok((None, value)),
+ MaybeRef::Ref { _ref } => _ref,
+ };
+ let name = r
+ .strip_prefix("#/responses/")
+ .ok_or_else(|| eyre::eyre!("invalid response reference"))?;
+ let global_responses = spec
+ .responses
+ .as_ref()
+ .ok_or_else(|| eyre::eyre!("no global responses"))?;
+ let response = global_responses
+ .get(name)
+ .ok_or_else(|| eyre::eyre!("referenced response does not exist"))?;
+ Ok((Some(name), response))
+}
+
+fn create_method_body(
+ spec: &OpenApiV2,
+ method: &str,
+ path: &str,
+ op: &Operation,
+) -> eyre::Result<String> {
+ let request = create_method_request(spec, method, path, op)?;
+ let response = create_method_response(spec, method, path, op)?;
+ Ok(format!("{request}\n {response}"))
+}
+
+fn create_method_request(
+ spec: &OpenApiV2,
+ method: &str,
+ path: &str,
+ op: &Operation,
+) -> eyre::Result<String> {
+ let mut has_query = false;
+ let mut has_headers = false;
+ let mut body_method = String::new();
+ if let Some(params) = &op.parameters {
+ for param in params {
+ let param = match &param {
+ MaybeRef::Value { value } => value,
+ MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
+ };
+ let name = crate::sanitize_ident(&param.name);
+ match param._in {
+ ParameterIn::Path => (/* do nothing */),
+ ParameterIn::Query => has_query = true,
+ ParameterIn::Header => has_headers = true,
+ ParameterIn::Body => {
+ if !body_method.is_empty() {
+ eyre::bail!("cannot have more than one body parameter");
+ }
+ if param_is_string(spec, param)? {
+ body_method = format!(".body({name})");
+ } else {
+ body_method = format!(".json(&{name})");
+ }
+ }
+ ParameterIn::FormData => {
+ if !body_method.is_empty() {
+ eyre::bail!("cannot have more than one body parameter");
+ }
+ body_method = format!(".multipart(reqwest::multipart::Form::new().part(\"attachment\", reqwest::multipart::Part::bytes({name}).file_name(\"file\").mime_str(\"*/*\").unwrap()))");
+ }
+ }
+ }
+ }
+ let mut fmt_str = sanitize_path_arg(path)?;
+ let mut fmt_args = String::new();
+ if has_query {
+ fmt_str.push_str("?{}");
+ fmt_args.push_str(", query.to_string()");
+ }
+ let path_arg = if fmt_str.contains("{") {
+ format!("&format!(\"{fmt_str}\"{fmt_args})")
+ } else {
+ format!("\"{fmt_str}\"")
+ };
+
+ let out = format!("let request = self.{method}({path_arg}){body_method}.build()?;");
+ Ok(out)
+}
+
+fn param_is_string(spec: &OpenApiV2, param: &Parameter) -> eyre::Result<bool> {
+ match param._in {
+ ParameterIn::Body => {
+ let schema_ref = param
+ .schema
+ .as_ref()
+ .ok_or_else(|| eyre::eyre!("body param did not have schema"))?;
+ crate::schema_is_string(spec, schema_ref)
+ }
+ _ => {
+ let is_str = match param._type {
+ Some(ParameterType::String) => true,
+ _ => false,
+ };
+ Ok(is_str)
+ }
+ }
+}
+
+fn sanitize_path_arg(mut path: &str) -> eyre::Result<String> {
+ let mut out = String::new();
+ loop {
+ let (head, tail) = match path.split_once("{") {
+ Some(i) => i,
+ None => {
+ out.push_str(path);
+ break;
+ }
+ };
+ path = tail;
+ out.push_str(head);
+ out.push('{');
+ let (head, tail) = match path.split_once("}") {
+ Some(i) => i,
+ None => {
+ eyre::bail!("unmatched bracket");
+ }
+ };
+ path = tail;
+ out.push_str(&head.to_snake_case());
+ out.push('}');
+ }
+ if out.starts_with("/") {
+ out.remove(0);
+ }
+ Ok(out)
+}
+
+fn create_method_response(
+ spec: &OpenApiV2,
+ method: &str,
+ path: &str,
+ op: &Operation,
+) -> eyre::Result<String> {
+ let mut has_empty = false;
+ let mut only_empty = true;
+ for (code, res) in &op.responses.http_codes {
+ let response = response_ref_type_name(spec, res)?;
+ if !code.starts_with("2") {
+ continue;
+ }
+ if matches!(response.body.as_deref(), Some("()") | None) {
+ has_empty = true;
+ } else {
+ only_empty = false;
+ }
+ }
+ let fn_ret = fn_return_from_op(spec, op)?;
+ let optional = has_empty && !only_empty;
+ let mut out = String::new();
+ out.push_str("let response = self.execute(request).await?;\n");
+ out.push_str("match response.status().as_u16() {\n");
+ for (code, res) in &op.responses.http_codes {
+ let (_, res) = deref_response(spec, res)?;
+ if !code.starts_with("2") {
+ continue;
+ }
+ out.push_str(code);
+ out.push_str(" => Ok(");
+ let mut handlers = Vec::new();
+ let header_handler = match &res.headers {
+ Some(_) => {
+ if fn_ret
+ .headers
+ .as_ref()
+ .map(|s| s.starts_with("Option<"))
+ .unwrap()
+ {
+ Some("Some(response.headers().clone())")
+ } else {
+ Some("response.headers().clone()")
+ }
+ }
+ None => {
+ if fn_ret.headers.is_some() {
+ Some("None")
+ } else {
+ None
+ }
+ }
+ };
+ handlers.extend(header_handler);
+ let body_handler = match &res.schema {
+ Some(schema) if crate::schema_is_string(spec, schema)? => {
+ if optional {
+ Some("Some(response.text().await?)")
+ } else {
+ Some("response.text().await?")
+ }
+ }
+ Some(_) => {
+ if optional {
+ Some("Some(response.json().await?)")
+ } else {
+ Some("response.json().await?")
+ }
+ }
+ None => {
+ if optional {
+ Some("None")
+ } else {
+ None
+ }
+ }
+ };
+ handlers.extend(body_handler);
+ match handlers[..] {
+ [single] => out.push_str(single),
+ _ => {
+ out.push('(');
+ for (i, item) in handlers.iter().copied().enumerate() {
+ out.push_str(item);
+ if i + 1 < handlers.len() {
+ out.push_str(", ");
+ }
+ }
+ out.push(')');
+ }
+ }
+ out.push_str("),\n");
+ }
+ out.push_str("_ => Err(ForgejoError::UnexpectedStatusCode(response.status()))\n");
+ out.push_str("}\n");
+
+ Ok(out)
+}
+
+#[derive(Debug, Default)]
+struct ResponseType {
+ headers: Option<String>,
+ body: Option<String>,
+}
+
+impl ResponseType {
+ fn merge(self, other: Self) -> eyre::Result<Self> {
+ let mut new = Self::default();
+ match (self.headers, other.headers) {
+ (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"),
+ (Some(a), None) => new.headers = Some(format!("Option<{a}>")),
+ (None, Some(b)) => new.headers = Some(format!("Option<{b}>")),
+ (a, b) => new.headers = a.or(b),
+ };
+ match (self.body.as_deref(), other.body.as_deref()) {
+ (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"),
+ (Some(a), Some("()") | None) => new.body = Some(format!("Option<{a}>")),
+ (Some("()") | None, Some(b)) => new.body = Some(format!("Option<{b}>")),
+ (a, b) => new.body = self.body.or(other.body),
+ };
+ Ok(new)
+ }
+}
+
+impl std::fmt::Display for ResponseType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut tys = Vec::new();
+ tys.extend(self.headers.as_deref());
+ tys.extend(self.body.as_deref());
+ match tys[..] {
+ [single] => f.write_str(single),
+ _ => {
+ write!(f, "(")?;
+ for (i, item) in tys.iter().copied().enumerate() {
+ f.write_str(item)?;
+ if i + 1 < tys.len() {
+ write!(f, ", ")?;
+ }
+ }
+ write!(f, ")")?;
+ Ok(())
+ }
+ }
+ }
+}
diff --git a/generator/src/structs.rs b/generator/src/structs.rs
new file mode 100644
index 0000000..55483b4
--- /dev/null
+++ b/generator/src/structs.rs
@@ -0,0 +1,338 @@
+use crate::openapi::*;
+use eyre::WrapErr;
+use heck::ToPascalCase;
+use std::fmt::Write;
+
+pub fn create_structs(spec: &OpenApiV2) -> eyre::Result<String> {
+ let mut s = String::new();
+ s.push_str("use structs::*;\n");
+ s.push_str("pub mod structs {\n");
+ if let Some(definitions) = &spec.definitions {
+ for (name, schema) in definitions {
+ let strukt = create_struct_for_definition(&spec, name, schema)?;
+ s.push_str(&strukt);
+ }
+ }
+ for (path, item) in &spec.paths {
+ let strukt = create_query_structs_for_path(&spec, path, item)?;
+ s.push_str(&strukt);
+ }
+ s.push_str("\n}");
+ Ok(s)
+}
+
+pub 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());
+ }
+
+ let docs = create_struct_docs(schema)?;
+ let mut fields = String::new();
+ let required = schema.required.as_deref().unwrap_or_default();
+ if let Some(properties) = &schema.properties {
+ for (prop_name, prop_schema) in properties {
+ let prop_ty = crate::schema_ref_type_name(spec, prop_schema)?;
+ let field_name = crate::sanitize_ident(prop_name);
+ let mut field_ty = prop_ty.clone();
+ if field_name.ends_with("url") && field_name != "ssh_url" && field_ty == "String" {
+ field_ty = "url::Url".into()
+ }
+ if field_ty == name {
+ field_ty = format!("Box<{field_ty}>")
+ }
+ if !required.contains(prop_name) {
+ field_ty = format!("Option<{field_ty}>")
+ }
+ if field_ty == "Option<url::Url>" {
+ fields.push_str("#[serde(deserialize_with = \"crate::none_if_blank_url\")]\n");
+ }
+ if field_ty == "time::OffsetDateTime" {
+ fields.push_str("#[serde(with = \"time::serde::rfc3339\")]\n");
+ }
+ if field_ty == "Option<time::OffsetDateTime>" {
+ fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n");
+ }
+ if &field_name != prop_name {
+ fields.push_str("#[serde(rename = \"");
+ fields.push_str(prop_name);
+ fields.push_str("\")]\n");
+ }
+ fields.push_str("pub ");
+ fields.push_str(&field_name);
+ fields.push_str(": ");
+ fields.push_str(&field_ty);
+ fields.push_str(",\n");
+ }
+ }
+
+ if let Some(additonal_schema) = &schema.additional_properties {
+ let prop_ty = crate::schema_ref_type_name(spec, additonal_schema)?;
+ fields.push_str("#[serde(flatten)]\n");
+ fields.push_str("pub additional: std::collections::BTreeMap<String, ");
+ fields.push_str(&prop_ty);
+ fields.push_str(">,\n");
+ }
+
+ let out = format!("{docs}#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct {name} {{\n{fields}}}\n\n");
+ Ok(out)
+}
+
+fn create_struct_docs(schema: &Schema) -> eyre::Result<String> {
+ let doc = match &schema.description {
+ Some(desc) => {
+ let mut out = String::new();
+ for line in desc.lines() {
+ out.push_str("/// ");
+ out.push_str(line);
+ out.push_str("\n/// \n");
+ }
+ out
+ }
+ None => String::new(),
+ };
+ Ok(doc)
+}
+
+pub fn create_query_structs_for_path(
+ spec: &OpenApiV2,
+ path: &str,
+ item: &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)
+}
+
+pub fn query_struct_name(op: &Operation) -> eyre::Result<String> {
+ let mut ty = op
+ .operation_id
+ .as_deref()
+ .ok_or_else(|| eyre::eyre!("operation did not have id"))?
+ .to_pascal_case()
+ .replace("o_auth2", "oauth2");
+ ty.push_str("Query");
+ Ok(ty)
+}
+
+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();
+ 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 = crate::methods::param_type(param, true)?;
+ let field_name = crate::sanitize_ident(&param.name);
+ let required = param.required.unwrap_or_default();
+ fields.push_str("pub ");
+ 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,
+ "write!(f, \"{}={{field_name}}&\", field_name = {field_name}.format(&time::format_description::well_known::Rfc3339).unwrap())?;",
+ param.name)?;
+ }
+ _ => {
+ writeln!(
+ &mut handler,
+ "write!(f, \"{}={{{}}}&\")?;",
+ param.name,
+ field_name.strip_prefix("r#").unwrap_or(&field_name)
+ )?;
+ }
+ },
+ ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => {
+ writeln!(
+ &mut handler,
+ "write!(f, \"{}={{{}}}&\")?;",
+ param.name,
+ field_name.strip_prefix("r#").unwrap_or(&field_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") => {
+ "write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;"
+ },
+ _ => {
+ "write!(f, \"{item}\")?;"
+ }
+ }
+ },
+ ParameterType::Number |
+ ParameterType::Integer |
+ ParameterType::Boolean => {
+ "write!(f, \"{item}\")?;"
+ },
+ 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 item in {field_name} {{")?;
+ writeln!(&mut handler, "write!(f, \"{}=\")?;", param.name)?;
+ handler.push_str(item_pusher);
+ handler.push('\n');
+ writeln!(&mut handler, "write!(f, \"&\")?;")?;
+ writeln!(&mut handler, "}}")?;
+ writeln!(&mut handler, "}}")?;
+ }
+ }
+ }
+ ParameterType::File => eyre::bail!("cannot send file in query"),
+ }
+ if !required {
+ writeln!(&mut handler, "}}")?;
+ }
+ imp.push_str(&handler);
+ }
+ }
+
+ let result = if fields.is_empty() {
+ String::new()
+ } else {
+ let op_name = query_struct_name(op)?;
+ format!(
+ "
+pub struct {op_name} {{
+ {fields}
+}}
+
+impl std::fmt::Display for {op_name} {{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
+ {imp}
+ Ok(())
+ }}
+}}
+"
+ )
+ };
+
+ Ok(result)
+}
+
+fn simple_query_array(
+ param: &Parameter,
+ item_pusher: &str,
+ name: &str,
+ sep: &str,
+) -> eyre::Result<String> {
+ let mut out = String::new();
+
+ writeln!(
+ &mut out,
+ "
+if !{name}.is_empty() {{
+ write!(f, \"{}=\")?;
+ for (item, i) in {name}.iter().enumerate() {{
+ {item_pusher}
+ if i < {name}.len() - 1 {{
+ write!(f, \"{sep}\")?;
+ }}
+ }}
+ write!(f, \"&\")?;
+}}",
+ param.name
+ )?;
+
+ Ok(out)
+}