use std::collections::HashSet;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::{ImplItem, ItemImpl, parse_macro_input, parse_quote};
use nest_rs_codegen::{Paginate, impl_self_ident, parse_crud_args};
pub(crate) fn entry(args: TokenStream, input: TokenStream) -> TokenStream {
let item = parse_macro_input!(input as ItemImpl);
match crud(args.into(), item) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
pub(crate) fn crud(args: TokenStream2, mut item: ItemImpl) -> syn::Result<TokenStream2> {
let cfg = parse_crud_args(args)?;
let self_ty = item.self_ty.clone();
let base = impl_self_ident(&self_ty, "#[crud]")?;
let existing: HashSet<String> = item
.items
.iter()
.filter_map(|it| match it {
ImplItem::Fn(f) => Some(f.sig.ident.to_string()),
_ => None,
})
.collect();
let service = &cfg.service;
let entity = &cfg.entity;
let output = &cfg.output;
let tag = output
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_else(|| "Resource".to_owned());
let internal = format_ident!("__nestrs_crud_internal_{}", base);
let id_v7_check: TokenStream2 = quote! {
if __id.0.get_version_num() != 7 {
return ::core::result::Result::Err(::poem::Error::from_string(
"path id must be a UUID v7",
::poem::http::StatusCode::BAD_REQUEST,
));
}
};
let mut generated: Vec<ImplItem> = Vec::new();
if !existing.contains("list") {
let summary = format!("List {tag}");
let list_method: ImplItem = match cfg.paginate {
None => parse_quote! {
#[get("/")]
#[api(summary = #summary, tags(#tag))]
async fn list(
&self,
_authz: ::nest_rs_authz::http::Authorize<::nest_rs_authz::Read, #entity>,
) -> ::poem::Result<::poem::web::Json<::std::vec::Vec<#output>>> {
let __rows = ::nest_rs_seaorm::CrudService::list(&*self.#service)
.await
.map_err(#internal)?;
::core::result::Result::Ok(::poem::web::Json(
__rows.iter().map(#output::from).collect(),
))
}
},
Some(Paginate::Cursor) => parse_quote! {
#[get("/")]
#[api(summary = #summary, tags(#tag))]
async fn list(
&self,
_authz: ::nest_rs_authz::http::Authorize<::nest_rs_authz::Read, #entity>,
__page: ::poem::web::Query<::nest_rs_seaorm::PageParams>,
) -> ::poem::Result<::poem::Response> {
let __p = ::nest_rs_seaorm::CrudService::page(
&*self.#service,
__page.0.limit(),
__page.0.after_uuid(),
)
.await
.map_err(#internal)?;
let __items: ::std::vec::Vec<#output> =
__p.items.iter().map(#output::from).collect();
let mut __resp = ::poem::IntoResponse::into_response(::poem::web::Json(__items));
if let ::core::option::Option::Some(__cursor) = __p.next_cursor {
__resp.headers_mut().insert(
::poem::http::HeaderName::from_static("x-next-cursor"),
::poem::http::HeaderValue::from_str(
&::std::string::ToString::to_string(&__cursor),
)
.expect("a UUID renders as a valid header value"),
);
}
::core::result::Result::Ok(__resp)
}
},
Some(Paginate::Page) => {
return Err(syn::Error::new(
Span::call_site(),
"#[crud] REST list does not yet support `paginate = page` (offset); \
use `paginate = cursor`",
));
}
};
generated.push(list_method);
}
if !existing.contains("get") {
let summary = format!("Fetch {tag} by id");
generated.push(parse_quote! {
#[get("/:id")]
#[api(summary = #summary, tags(#tag))]
async fn get(
&self,
_authz: ::nest_rs_authz::http::Authorize<::nest_rs_authz::Read, #entity>,
__id: ::poem::web::Path<::uuid::Uuid>,
) -> ::poem::Result<::poem::web::Json<#output>> {
#id_v7_check
match ::nest_rs_seaorm::CrudService::access(
&*self.#service,
::nest_rs_authz::Action::Read,
__id.0,
)
.await
.map_err(#internal)?
{
::nest_rs_seaorm::Access::Found(__m) => {
::core::result::Result::Ok(::poem::web::Json(#output::from(&__m)))
}
::nest_rs_seaorm::Access::Denied => ::core::result::Result::Err(
::poem::Error::from_status(::poem::http::StatusCode::FORBIDDEN),
),
::nest_rs_seaorm::Access::Missing => ::core::result::Result::Err(
::poem::Error::from_status(::poem::http::StatusCode::NOT_FOUND),
),
}
}
});
}
if !cfg.readonly {
if let Some(create) = &cfg.create
&& !existing.contains("create")
{
let summary = format!("Create {tag}");
generated.push(parse_quote! {
#[post("/")]
#[api(summary = #summary, tags(#tag))]
async fn create(
&self,
_authz: ::nest_rs_authz::http::Authorize<::nest_rs_authz::Create, #entity>,
__body: ::nest_rs_http::Valid<::poem::web::Json<#create>>,
) -> ::poem::Result<::poem::web::Json<#output>> {
let __row = ::nest_rs_seaorm::CrudService::create(
&*self.#service,
__body.into_inner(),
)
.await
.map_err(#internal)?;
::core::result::Result::Ok(::poem::web::Json(#output::from(&__row)))
}
});
}
if let Some(update) = &cfg.update
&& !existing.contains("update")
{
let summary = format!("Update {tag} by id");
generated.push(parse_quote! {
#[patch("/:id")]
#[api(summary = #summary, tags(#tag))]
async fn update(
&self,
_authz: ::nest_rs_authz::http::Authorize<::nest_rs_authz::Update, #entity>,
__id: ::poem::web::Path<::uuid::Uuid>,
__body: ::nest_rs_http::Valid<::poem::web::Json<#update>>,
) -> ::poem::Result<::poem::web::Json<#output>> {
#id_v7_check
match ::nest_rs_seaorm::CrudService::access(
&*self.#service,
::nest_rs_authz::Action::Update,
__id.0,
)
.await
.map_err(#internal)?
{
::nest_rs_seaorm::Access::Found(__m) => {
let __row = ::nest_rs_seaorm::CrudService::update(
&*self.#service,
__m,
__body.into_inner(),
)
.await
.map_err(#internal)?;
::core::result::Result::Ok(::poem::web::Json(#output::from(&__row)))
}
::nest_rs_seaorm::Access::Denied => ::core::result::Result::Err(
::poem::Error::from_status(::poem::http::StatusCode::FORBIDDEN),
),
::nest_rs_seaorm::Access::Missing => ::core::result::Result::Err(
::poem::Error::from_status(::poem::http::StatusCode::NOT_FOUND),
),
}
}
});
}
if !existing.contains("delete") {
let summary = format!("Delete {tag} by id");
generated.push(parse_quote! {
#[delete("/:id")]
#[api(summary = #summary, tags(#tag))]
async fn delete(
&self,
_authz: ::nest_rs_authz::http::Authorize<::nest_rs_authz::Delete, #entity>,
__id: ::poem::web::Path<::uuid::Uuid>,
) -> ::poem::Result<::poem::http::StatusCode> {
#id_v7_check
match ::nest_rs_seaorm::CrudService::access(
&*self.#service,
::nest_rs_authz::Action::Delete,
__id.0,
)
.await
.map_err(#internal)?
{
::nest_rs_seaorm::Access::Found(__m) => {
::nest_rs_seaorm::CrudService::delete(&*self.#service, __m)
.await
.map_err(#internal)?;
::core::result::Result::Ok(::poem::http::StatusCode::NO_CONTENT)
}
::nest_rs_seaorm::Access::Denied => ::core::result::Result::Err(
::poem::Error::from_status(::poem::http::StatusCode::FORBIDDEN),
),
::nest_rs_seaorm::Access::Missing => ::core::result::Result::Err(
::poem::Error::from_status(::poem::http::StatusCode::NOT_FOUND),
),
}
}
});
}
}
generated.append(&mut item.items);
item.items = generated;
Ok(quote! {
#[::nest_rs_http::routes]
#item
#[doc(hidden)]
#[allow(non_snake_case)]
fn #internal<E: ::std::string::ToString>(__e: E) -> ::poem::Error {
::poem::Error::from_string(
::std::string::ToString::to_string(&__e),
::poem::http::StatusCode::INTERNAL_SERVER_ERROR,
)
}
})
}