Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/breach-macros/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,18 @@ impl<'a> ToTokens for HttpError<'a> {

if let Some(attribute) = self.data.attribute() {
if attribute.axum {
let hook = attribute.axum_hook.as_ref().map(|hook| {
quote! {
#hook(&self);
}
});

tokens.append_all(quote! {
#[automatically_derived]
impl #impl_generics ::axum::response::IntoResponse for #ident #type_generics #where_clause {
fn into_response(self) -> ::axum::response::Response {
#hook;

(self.status(), ::axum::Json(self)).into_response()
}
}
Expand Down
136 changes: 101 additions & 35 deletions packages/breach-macros/src/http/attribute.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,69 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Error, Result, Type, spanned::Spanned};
use syn::{Attribute, Error, Expr, Result, Type, spanned::Spanned};

use crate::status::Status;

pub struct HttpErrorAttribute {
pub status: Option<Status>,
}

impl<'a> HttpErrorAttribute {
pub fn parse_slice(input: &'a [Attribute]) -> Result<Option<Self>> {
let mut result = None;

for attribute in input {
if !attribute.meta.path().is_ident("http") {
continue;
}

if result.is_some() {
return Err(Error::new(
attribute.span(),
"only a single `http` attribute is allowed",
));
}

result = Some(Self::parse(attribute)?);
}

Ok(result)
}

pub fn parse(attribute: &'a Attribute) -> Result<Self> {
let mut status = None;

attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("status") {
status = Some(meta.value()?.parse()?);

Ok(())
} else {
Err(meta.error("unknown parameter"))
}
})?;

Ok(Self { status })
}

pub fn status(&self) -> TokenStream {
status(self.status.as_ref())
}

pub fn responses(&self, r#type: Option<TokenStream>) -> TokenStream {
responses(self.status.as_ref(), r#type)
}
}

pub struct HttpErrorDataAttribute {
pub status: Option<Status>,
pub base: Option<Type>,
pub axum: bool,
pub axum_hook: Option<Expr>,
pub utoipa: bool,
}

impl<'a> HttpErrorAttribute {
impl<'a> HttpErrorDataAttribute {
pub fn parse_slice(input: &'a [Attribute]) -> Result<Option<Self>> {
let mut result = None;

Expand All @@ -37,6 +89,7 @@ impl<'a> HttpErrorAttribute {
let mut status = None;
let mut base = None;
let mut axum = false;
let mut axum_hook = None;
let mut utoipa = false;

attribute.parse_nested_meta(|meta| {
Expand All @@ -51,6 +104,10 @@ impl<'a> HttpErrorAttribute {
} else if meta.path.is_ident("axum") {
axum = true;

Ok(())
} else if meta.path.is_ident("axum_hook") {
axum_hook = Some(meta.value()?.parse()?);

Ok(())
} else if meta.path.is_ident("utoipa") {
utoipa = true;
Expand All @@ -65,50 +122,59 @@ impl<'a> HttpErrorAttribute {
status,
base,
axum,
axum_hook,
utoipa,
})
}

pub fn status(&self) -> TokenStream {
if let Some(status) = &self.status {
let status = status.as_ident();

quote!(::breach::http::StatusCode::#status)
} else {
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
}
status(self.status.as_ref())
}

pub fn responses(&self, r#type: Option<TokenStream>) -> TokenStream {
if let Some(status) = &self.status {
let code = status.code.as_str();

let content = r#type.map(|r#type| {
// TODO: Attempt to infer content type from schema?
quote! {
.content(
"application/json",
::utoipa::openapi::content::ContentBuilder::new()
.schema(Some(<#r#type as ::utoipa::PartialSchema>::schema()))
.build()
)
}
});
responses(self.status.as_ref(), r#type)
}
}

fn status(status: Option<&Status>) -> TokenStream {
if let Some(status) = status {
let status = status.as_ident();

quote!(::breach::http::StatusCode::#status)
} else {
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
}
}

fn responses(status: Option<&Status>, r#type: Option<TokenStream>) -> TokenStream {
if let Some(status) = status {
let code = status.code.as_str();

let content = r#type.map(|r#type| {
// TODO: Attempt to infer content type from schema?
quote! {
::std::collections::BTreeMap::from_iter([
(
#code.to_owned(),
::utoipa::openapi::RefOr::T(
::utoipa::openapi::response::ResponseBuilder::new()
#content
.build()
),
),
])
.content(
"application/json",
::utoipa::openapi::content::ContentBuilder::new()
.schema(Some(<#r#type as ::utoipa::PartialSchema>::schema()))
.build()
)
}
} else {
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
});

quote! {
::std::collections::BTreeMap::from_iter([
(
#code.to_owned(),
::utoipa::openapi::RefOr::T(
::utoipa::openapi::response::ResponseBuilder::new()
#content
.build()
),
),
])
}
} else {
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
}
}
4 changes: 2 additions & 2 deletions packages/breach-macros/src/http/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
use syn::{Data, DeriveInput, Result};

use crate::http::{
attribute::HttpErrorAttribute, r#enum::HttpErrorEnum, r#struct::HttpErrorStruct,
attribute::HttpErrorDataAttribute, r#enum::HttpErrorEnum, r#struct::HttpErrorStruct,
union::HttpErrorUnion,
};

Expand All @@ -21,7 +21,7 @@ impl<'a> HttpErrorData<'a> {
})
}

pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
match self {
HttpErrorData::Struct(r#struct) => r#struct.attribute(),
HttpErrorData::Enum(r#enum) => r#enum.attribute(),
Expand Down
8 changes: 4 additions & 4 deletions packages/breach-macros/src/http/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{DataEnum, DeriveInput, Error, Field, Fields, Ident, Result, Variant, spanned::Spanned};

use crate::http::attribute::HttpErrorAttribute;
use crate::http::attribute::{HttpErrorAttribute, HttpErrorDataAttribute};

pub struct HttpErrorEnum<'a> {
ident: &'a Ident,
variants: Vec<HttpErrorEnumVariant<'a>>,
attribute: Option<HttpErrorAttribute>,
attribute: Option<HttpErrorDataAttribute>,
}

impl<'a> HttpErrorEnum<'a> {
pub fn parse(input: &'a DeriveInput, data: &'a DataEnum) -> Result<Self> {
let mut result = HttpErrorEnum {
ident: &input.ident,
variants: Vec::with_capacity(data.variants.len()),
attribute: HttpErrorAttribute::parse_slice(&input.attrs)?,
attribute: HttpErrorDataAttribute::parse_slice(&input.attrs)?,
};

for variant in &data.variants {
Expand All @@ -27,7 +27,7 @@ impl<'a> HttpErrorEnum<'a> {
Ok(result)
}

pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
self.attribute.as_ref()
}

Expand Down
8 changes: 4 additions & 4 deletions packages/breach-macros/src/http/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ use proc_macro2::TokenStream;
use quote::quote;
use syn::{DataStruct, DeriveInput, Error, Result, spanned::Spanned};

use crate::http::attribute::HttpErrorAttribute;
use crate::http::attribute::HttpErrorDataAttribute;

pub struct HttpErrorStruct {
attribute: HttpErrorAttribute,
attribute: HttpErrorDataAttribute,
}

impl<'a> HttpErrorStruct {
pub fn parse(input: &'a DeriveInput, _data: &'a DataStruct) -> Result<Self> {
let Some(attribute) = HttpErrorAttribute::parse_slice(&input.attrs)? else {
let Some(attribute) = HttpErrorDataAttribute::parse_slice(&input.attrs)? else {
return Err(Error::new(input.span(), "missing http attribute"));
};

Ok(HttpErrorStruct { attribute })
}

pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
Some(&self.attribute)
}

Expand Down
4 changes: 2 additions & 2 deletions packages/breach-macros/src/http/union.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use proc_macro2::TokenStream;
use syn::{DataUnion, DeriveInput, Result};

use crate::http::attribute::HttpErrorAttribute;
use crate::http::attribute::HttpErrorDataAttribute;

pub struct HttpErrorUnion {}

Expand All @@ -10,7 +10,7 @@ impl HttpErrorUnion {
Err(syn::Error::new_spanned(input, "union is not supported"))
}

pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
None
}

Expand Down