use crate::lower::{LoweredProgram, ProgramBackendAlg, expand_program};
use crate::syntax::{Reified, lift_operation};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::visit_mut::{self, VisitMut};
use syn::{Expr, ExprMethodCall, Ident, ImplItemFn, Meta, Token, parse_quote};
struct JsonRpcBackend;
#[derive(Clone, Copy, Default)]
pub(crate) struct MethodDefaults {
fallible: bool,
}
fn split_arguments(attr: TokenStream) -> syn::Result<(MethodDefaults, TokenStream)> {
if attr.is_empty() {
return Ok((MethodDefaults::default(), attr));
}
let mut defaults = MethodDefaults::default();
let mut forwarded = Punctuated::<Meta, Token![,]>::new();
for argument in Punctuated::<Meta, Token![,]>::parse_terminated.parse2(attr)? {
if matches!(&argument, Meta::Path(path) if path.is_ident("fallible")) {
defaults.fallible = true;
} else {
forwarded.push(argument);
}
}
Ok((defaults, quote!(#forwarded)))
}
struct MethodRequirement {
reified: Reified,
fallible: bool,
}
fn is_fallible(declaration: &Expr) -> bool {
let mut current = declaration;
loop {
let Expr::MethodCall(call) = current else { return false };
if call.method == "fallible" {
return true;
}
current = &call.receiver;
}
}
struct Methods<'a> {
requirements: &'a mut Vec<MethodRequirement>,
defaults: MethodDefaults,
}
impl VisitMut for Methods<'_> {
fn visit_expr_method_call_mut(&mut self, call: &mut ExprMethodCall) {
if call.method == "method"
&& let Some(declaration) = call.args.iter_mut().nth(1)
{
let declared = is_fallible(declaration);
if let Some(reified) = lift_operation(declaration) {
if self.defaults.fallible && !declared {
let total = declaration.clone();
*declaration = parse_quote!(#total.fallible());
}
let fallible = declared || self.defaults.fallible;
self.requirements.push(MethodRequirement { reified, fallible });
}
}
visit_mut::visit_expr_method_call_mut(self, call);
}
}
impl ProgramBackendAlg for JsonRpcBackend {
type Defaults = MethodDefaults;
const NESTED_SUFFIX: &'static str = "_rpc";
const REJECTED_PARAM: &'static str = "JSON-RPC programs currently support type parameters only";
fn require_declarations(method: &mut ImplItemFn, defaults: &Self::Defaults) {
let mut operations = Vec::new();
Methods { requirements: &mut operations, defaults: *defaults }.visit_block_mut(&mut method.block);
let where_clause = method.sig.generics.make_where_clause();
let mut carriers: Vec<Ident> = Vec::new();
for requirement in &operations {
let carrier = &requirement.reified.carrier;
if !carriers.contains(carrier) {
carriers.push(carrier.clone());
}
}
for carrier in carriers {
where_clause.predicates.push(parse_quote!(This: ::alux_ext::HandlerContextAlg<#carrier>));
}
for MethodRequirement { reified: Reified { operation, carrier }, fallible } in operations {
where_clause.predicates.push(parse_quote! {
#operation: ::alux_ext::OperationAlg<Context = #carrier>
+ ::alux_ext::ApplyAlg<
<This as ::alux_ext::HandlerContextAlg<#carrier>>::Handle,
<#operation as ::alux_ext::OperationAlg>::Args,
>
+ Send + Sync + 'static
});
let registration = if fallible {
quote!(::alux_jsonrpc::JsonRpcFallibleAlg)
} else {
quote!(::alux_jsonrpc::JsonRpcMethodAlg)
};
where_clause.predicates.push(parse_quote! {
This: #registration<
<This as ::alux_ext::HandlerContextAlg<#carrier>>::Handle,
<#operation as ::alux_ext::OperationAlg>::Args,
<#operation as ::alux_ext::ApplyAlg<
<This as ::alux_ext::HandlerContextAlg<#carrier>>::Handle,
<#operation as ::alux_ext::OperationAlg>::Args,
>>::Output
>
});
}
}
fn require_subprogram(program: &TokenStream) -> TokenStream {
quote! {
#program: ::alux_jsonrpc::JsonRpcProgramAlg<This, Methods = <This as ::alux_jsonrpc::JsonRpcAlg>::Methods>
}
}
fn compile_program(lowered: &LoweredProgram) -> TokenStream {
let LoweredProgram { program_type, compiler_params, predicates, body } = lowered;
quote! {
impl<This, #compiler_params> ::alux_jsonrpc::JsonRpcProgramAlg<This> for #program_type
where
#(#predicates,)*
{
type Methods = <This as ::alux_jsonrpc::JsonRpcAlg>::Methods;
fn compile_jsonrpc(self, compiler: &This) -> Self::Methods {
let _ = self;
let builder = ::alux_jsonrpc::JsonRpcProgramBuilder;
let program = (#body).into_program();
::alux_jsonrpc::CompileJsonRpcProgram::compile_jsonrpc_program(program, compiler)
}
}
}
}
}
pub(crate) fn jsonrpc_program_defunc_internal(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
let (defaults, forwarded) = split_arguments(attr)?;
expand_program::<JsonRpcBackend>(forwarded, item, &defaults)
}
#[cfg(test)]
mod tests {
use super::jsonrpc_program_defunc_internal;
use quote::quote;
#[test]
fn generates_a_composable_jsonrpc_program() {
let output = jsonrpc_program_defunc_internal(
quote!(name = StatusRpcExt),
quote! {
pub impl<This> This
where
This: JsonRpcApiAlg,
{
fn status_rpc<Alg>(&self)
where
Alg: StatusAlg,
{
self.methods()
.method("status", self.op(Alg::status_current))
.method("status_for", self.op(Alg::status_for_path))
}
}
},
)
.unwrap()
.to_string();
assert!(output.contains("struct StatusRpcProgram"));
assert!(output.contains("alux_jsonrpc :: JsonRpcProgramAlg"));
assert!(output.contains("StatusCurrentOperation < Alg >"));
assert!(output.contains("StatusForPathOperation < Alg >"));
assert!(output.contains("CompileJsonRpcProgram :: compile_jsonrpc_program"));
}
#[test]
fn carries_the_authored_documentation_onto_the_program() {
let output = jsonrpc_program_defunc_internal(
quote!(name = StatusRpcExt),
quote! {
pub impl<This> This {
fn status_rpc<Alg>(&self) {
self.methods().method("status", self.op(Alg::status_current))
}
}
},
)
.unwrap()
.to_string();
assert!(
output
.contains(r#"# [doc = r" Declares the status methods."] # [doc (hidden)] pub struct StatusRpcProgram"#)
);
}
#[test]
fn reads_every_declaration_of_a_fallible_program_as_fallible() {
let output = jsonrpc_program_defunc_internal(
quote!(name = StatusRpcExt, fallible),
quote! {
pub impl<This> This
where
This: JsonRpcApiAlg,
{
fn status_rpc<Alg>(&self)
where
Alg: StatusAlg,
{
self.methods()
.method("status", self.op(Alg::status_current))
.method("status_for", self.op(Alg::status_for_path).named())
}
}
},
)
.unwrap()
.to_string();
assert_eq!(output.matches("JsonRpcFallibleAlg").count(), 2);
assert!(!output.contains("JsonRpcMethodAlg"));
assert_eq!(output.matches(". fallible ()").count(), 2);
assert!(!output.contains("fallible)"), "the flag leaked into a forwarded attribute");
}
#[test]
fn keeps_a_declaration_that_states_its_own_mode() {
let output = jsonrpc_program_defunc_internal(
quote!(name = StatusRpcExt, fallible),
quote! {
pub impl<This> This {
fn status_rpc<Alg>(&self) {
self.methods().method("status", self.op(Alg::status_current).fallible())
}
}
},
)
.unwrap()
.to_string();
assert_eq!(output.matches(". fallible ()").count(), 1);
assert_eq!(output.matches("JsonRpcFallibleAlg").count(), 1);
}
#[test]
fn leaves_a_silent_program_on_the_value_path() {
let output = jsonrpc_program_defunc_internal(
quote!(name = StatusRpcExt),
quote! {
pub impl<This> This {
fn status_rpc<Alg>(&self) {
self.methods().method("status", self.op(Alg::status_current))
}
}
},
)
.unwrap()
.to_string();
assert!(output.contains("JsonRpcMethodAlg"));
assert!(!output.contains("fallible"));
}
#[test]
fn composes_named_jsonrpc_programs() {
let output = jsonrpc_program_defunc_internal(
quote!(name = ExampleRpcExt),
quote! {
pub impl<This> This
where
This: JsonRpcApiAlg,
{
fn example_rpc<Alg>(&self)
where
Alg: StatusAlg + ItemsAlg,
{
self.methods().merge(self.status_rpc::<Alg>()).merge(self.items_rpc::<Alg>())
}
}
},
)
.unwrap()
.to_string();
assert!(output.contains("StatusRpcProgram < Alg >"));
assert!(output.contains("ItemsRpcProgram < Alg >"));
assert!(output.contains("builder . program"));
}
}