Implement initial Durable Function Bindings.
This commit implements initial support for the Durable Functions bindings. This is not yet complete as there is still a lot to do for implementing the orchestration client, the orchestration context, and the required codegen changes for activity bindings.
This commit is contained in:
parent
1ef51523d2
commit
50d45593fd
|
@ -353,6 +353,15 @@ dependencies = [
|
|||
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "durable-functions-example"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"azure-functions 0.10.0",
|
||||
"futures-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.5.3"
|
||||
|
|
|
@ -19,4 +19,5 @@ members = [
|
|||
"examples/twilio",
|
||||
"examples/sendgrid",
|
||||
"examples/generic",
|
||||
"examples/durable-functions",
|
||||
]
|
||||
|
|
53
README.md
53
README.md
|
@ -268,31 +268,34 @@ The `#[func]` attribute is used to turn an ordinary Rust function into an Azure
|
|||
|
||||
The current list of supported bindings:
|
||||
|
||||
| Rust Type | Azure Functions Binding | Direction | Vec\<T> |
|
||||
|----------------------------------------------------------------------------------------------------------------------------|-------------------------------------|----------------|---------|
|
||||
| [Blob](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.Blob.html) | Input and Ouput Blob | in, inout, out | No |
|
||||
| [BlobTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.BlobTrigger.html) | Blob Trigger | in, inout | No |
|
||||
| [CosmosDbDocument](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.CosmosDbDocument.html) | Input and Output Cosmos DB Document | in, out | Yes |
|
||||
| [CosmosDbTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.CosmosDbTrigger.html) | Cosmos DB Trigger | in | No |
|
||||
| [EventGridEvent](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.EventGridEvent.html) | Event Grid Trigger | in | No |
|
||||
| [EventHubMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.EventHubMessage.html) | Event Hub Output Message | out | Yes |
|
||||
| [EventHubTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.EventHubTrigger.html) | Event Hub Trigger | in | No |
|
||||
| [GenericInput](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.GenericInput.html) | Generic Input | in | No |
|
||||
| [GenericOutput](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.GenericOutput.html) | Generic Output | out | No |
|
||||
| [GenericTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.GenericTrigger.html) | Generic Trigger | in | No |
|
||||
| [HttpRequest](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.HttpRequest.html) | HTTP Trigger | in | No |
|
||||
| [HttpResponse](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.HttpResponse.html) | Output HTTP Response | out | No |
|
||||
| [QueueMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.QueueMessage.html) | Output Queue Message | out | Yes |
|
||||
| [QueueTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.QueueTrigger.html) | Queue Trigger | in | No |
|
||||
| [SendGridMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SendGridMessage.html) | SendGrid Email Message | out | Yes |
|
||||
| [ServiceBusMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.ServiceBusMessage.html) | Service Bus Output Message | out | Yes |
|
||||
| [ServiceBusTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.ServiceBusTrigger.html) | Service Bus Trigger | in | No |
|
||||
| [SignalRConnectionInfo](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SignalRConnectionInfo.html) | SignalR Connection Info | in | No |
|
||||
| [SignalRGroupAction](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SignalRGroupAction.html) | SignalR Group Action | out | Yes |
|
||||
| [SignalRMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SignalRMessage.html) | SignalR Message | out | Yes |
|
||||
| [Table](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.Table.html) | Input and Ouput Table | in, out | No |
|
||||
| [TimerInfo](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TimerInfo.html) | Timer Trigger | in | No |
|
||||
| [TwilioSmsMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TwilioSmsMessage.html) | Twilio SMS Message Output | out | Yes | Yes |
|
||||
| Rust Type | Azure Functions Binding | Direction | Vec\<T> |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|----------------|---------|
|
||||
| [Blob](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.Blob.html) | Input and Ouput Blob | in, inout, out | No |
|
||||
| [BlobTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.BlobTrigger.html) | Blob Trigger | in, inout | No |
|
||||
| [CosmosDbDocument](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.CosmosDbDocument.html) | Input and Output Cosmos DB Document | in, out | Yes |
|
||||
| [CosmosDbTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.CosmosDbTrigger.html) | Cosmos DB Trigger | in | No |
|
||||
| [DurableActivityContext](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.DurableActivityContext.html) | Durable Activity Trigger | in | No |
|
||||
| [DurableOrchestrationClient](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.DurableOrchestrationClient.html) | Durable Orchestration Client | in | No |
|
||||
| [DurableOrchestrationContext](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.DurableOrchestrationContext.html) | Durable Orchestration Trigger | in | No |
|
||||
| [EventGridEvent](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.EventGridEvent.html) | Event Grid Trigger | in | No |
|
||||
| [EventHubMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.EventHubMessage.html) | Event Hub Output Message | out | Yes |
|
||||
| [EventHubTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.EventHubTrigger.html) | Event Hub Trigger | in | No |
|
||||
| [GenericInput](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.GenericInput.html) | Generic Input | in | No |
|
||||
| [GenericOutput](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.GenericOutput.html) | Generic Output | out | No |
|
||||
| [GenericTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.GenericTrigger.html) | Generic Trigger | in | No |
|
||||
| [HttpRequest](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.HttpRequest.html) | HTTP Trigger | in | No |
|
||||
| [HttpResponse](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.HttpResponse.html) | Output HTTP Response | out | No |
|
||||
| [QueueMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.QueueMessage.html) | Output Queue Message | out | Yes |
|
||||
| [QueueTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.QueueTrigger.html) | Queue Trigger | in | No |
|
||||
| [SendGridMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SendGridMessage.html) | SendGrid Email Message | out | Yes |
|
||||
| [ServiceBusMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.ServiceBusMessage.html) | Service Bus Output Message | out | Yes |
|
||||
| [ServiceBusTrigger](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.ServiceBusTrigger.html) | Service Bus Trigger | in | No |
|
||||
| [SignalRConnectionInfo](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SignalRConnectionInfo.html) | SignalR Connection Info | in | No |
|
||||
| [SignalRGroupAction](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SignalRGroupAction.html) | SignalR Group Action | out | Yes |
|
||||
| [SignalRMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.SignalRMessage.html) | SignalR Message | out | Yes |
|
||||
| [Table](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.Table.html) | Input and Ouput Table | in, out | No |
|
||||
| [TimerInfo](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TimerInfo.html) | Timer Trigger | in | No |
|
||||
| [TwilioSmsMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TwilioSmsMessage.html) | Twilio SMS Message Output | out | Yes | Yes |
|
||||
|
||||
More bindings will be implemented in the future, including support for retreiving data from custom bindings.
|
||||
|
||||
|
|
|
@ -23,6 +23,65 @@ use syn::{
|
|||
|
||||
pub const OUTPUT_BINDING_PREFIX: &str = "output";
|
||||
const RETURN_BINDING_NAME: &str = "$return";
|
||||
const ORCHESTRATION_CONTEXT_TYPE: &str = "DurableOrchestrationContext";
|
||||
|
||||
fn has_parameter_of_type(func: &ItemFn, type_name: &str) -> bool {
|
||||
func.sig.inputs.iter().any(|arg| {
|
||||
if let FnArg::Typed(arg) = arg {
|
||||
match &*arg.ty {
|
||||
Type::Reference(tr) => {
|
||||
if let Type::Path(tp) = &*tr.elem {
|
||||
return last_segment_in_path(&tp.path).ident == type_name;
|
||||
}
|
||||
}
|
||||
Type::Path(tp) => {
|
||||
return last_segment_in_path(&tp.path).ident == type_name;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_orchestration_function(func: &ItemFn) {
|
||||
if func.sig.asyncness.is_none() {
|
||||
macro_panic(func.sig.ident.span(), "orchestration functions must be async");
|
||||
}
|
||||
|
||||
if func.sig.inputs.len() != 1 {
|
||||
macro_panic(
|
||||
func.sig.ident.span(),
|
||||
format!(
|
||||
"orchestration functions must have exactly one parameter of type `{}`",
|
||||
ORCHESTRATION_CONTEXT_TYPE
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if !match func.sig.inputs.iter().nth(0).unwrap() {
|
||||
FnArg::Typed(arg) => match &*arg.ty {
|
||||
Type::Path(tp) => last_segment_in_path(&tp.path).ident == ORCHESTRATION_CONTEXT_TYPE,
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
} {
|
||||
macro_panic(
|
||||
func.sig.ident.span(),
|
||||
format!(
|
||||
"orchestration functions must have exactly one parameter of type `{}`",
|
||||
ORCHESTRATION_CONTEXT_TYPE
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if let ReturnType::Type(_, ty) = &func.sig.output {
|
||||
macro_panic(
|
||||
ty.span(),
|
||||
"orchestration functions cannot have return types",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_function(func: &ItemFn) {
|
||||
match func.vis {
|
||||
|
@ -67,6 +126,13 @@ fn validate_function(func: &ItemFn) {
|
|||
"the 'func' attribute cannot be used on variadic functions",
|
||||
);
|
||||
}
|
||||
|
||||
if func.sig.asyncness.is_some() && cfg!(not(feature = "unstable")) {
|
||||
macro_panic(
|
||||
func.sig.asyncness.span(),
|
||||
"async Azure Functions require a nightly compiler with the 'unstable' feature enabled",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_generic_argument_type<'a>(
|
||||
|
@ -397,6 +463,12 @@ pub fn func_impl(
|
|||
|
||||
validate_function(&target);
|
||||
|
||||
let is_orchestration = has_parameter_of_type(&target, ORCHESTRATION_CONTEXT_TYPE);
|
||||
|
||||
if is_orchestration {
|
||||
validate_orchestration_function(&target);
|
||||
}
|
||||
|
||||
let mut func = Function::from(match syn::parse_macro_input::parse::<AttributeArgs>(args) {
|
||||
Ok(f) => f,
|
||||
Err(e) => macro_panic(
|
||||
|
@ -428,17 +500,19 @@ pub fn func_impl(
|
|||
);
|
||||
}
|
||||
|
||||
for binding in bind_return_type(&target.sig.output, &mut binding_args).into_iter() {
|
||||
if let Some(name) = binding.name() {
|
||||
if !names.insert(name.to_string()) {
|
||||
if let ReturnType::Type(_, ty) = &target.sig.output {
|
||||
macro_panic(ty.span(), format!("output binding has a name of '{}' that conflicts with a parameter's binding name; the corresponding parameter must be renamed.", name));
|
||||
if !is_orchestration {
|
||||
for binding in bind_return_type(&target.sig.output, &mut binding_args).into_iter() {
|
||||
if let Some(name) = binding.name() {
|
||||
if !names.insert(name.to_string()) {
|
||||
if let ReturnType::Type(_, ty) = &target.sig.output {
|
||||
macro_panic(ty.span(), format!("output binding has a name of '{}' that conflicts with a parameter's binding name; the corresponding parameter must be renamed.", name));
|
||||
}
|
||||
macro_panic(target.sig.output.span(), format!("output binding has a name of '{}' that conflicts with a parameter's binding name; the corresponding parameter must be renamed.", name));
|
||||
}
|
||||
macro_panic(target.sig.output.span(), format!("output binding has a name of '{}' that conflicts with a parameter's binding name; the corresponding parameter must be renamed.", name));
|
||||
}
|
||||
}
|
||||
|
||||
func.bindings.to_mut().push(binding);
|
||||
func.bindings.to_mut().push(binding);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, args)) = binding_args.iter().nth(0) {
|
||||
|
@ -449,10 +523,17 @@ pub fn func_impl(
|
|||
|
||||
if let Lit::Str(s) = v {
|
||||
match s.value().as_ref() {
|
||||
RETURN_BINDING_NAME => macro_panic(
|
||||
v.span(),
|
||||
"cannot bind to a function without a return value",
|
||||
),
|
||||
RETURN_BINDING_NAME => if is_orchestration {
|
||||
macro_panic(
|
||||
v.span(),
|
||||
"cannot bind to the return value of an orchestration function",
|
||||
)
|
||||
} else {
|
||||
macro_panic(
|
||||
v.span(),
|
||||
"cannot bind to a function without a return value",
|
||||
)
|
||||
},
|
||||
v => macro_panic(
|
||||
v.span(),
|
||||
format!(
|
||||
|
@ -470,33 +551,28 @@ pub fn func_impl(
|
|||
});
|
||||
}
|
||||
|
||||
let invoker = Invoker(&target);
|
||||
let invoker = Invoker {
|
||||
func: &target,
|
||||
is_orchestration,
|
||||
};
|
||||
|
||||
let target_name = target.sig.ident.to_string();
|
||||
if func.name.is_empty() {
|
||||
func.name = Cow::Owned(target_name.clone());
|
||||
}
|
||||
|
||||
match target.sig.asyncness {
|
||||
Some(asyncness) => {
|
||||
if cfg!(feature = "unstable") {
|
||||
func.invoker = Some(azure_functions_shared::codegen::Invoker {
|
||||
name: Cow::Owned(invoker.name()),
|
||||
invoker_fn: InvokerFn::Async(None),
|
||||
});
|
||||
} else {
|
||||
macro_panic(
|
||||
asyncness.span(),
|
||||
"async Azure Functions require a nightly compiler with the 'unstable' feature enabled",
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !is_orchestration && target.sig.asyncness.is_some() {
|
||||
if cfg!(feature = "unstable") {
|
||||
func.invoker = Some(azure_functions_shared::codegen::Invoker {
|
||||
name: Cow::Owned(invoker.name()),
|
||||
invoker_fn: InvokerFn::Sync(None),
|
||||
invoker_fn: InvokerFn::Async(None),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
func.invoker = Some(azure_functions_shared::codegen::Invoker {
|
||||
name: Cow::Owned(invoker.name()),
|
||||
invoker_fn: InvokerFn::Sync(None),
|
||||
});
|
||||
}
|
||||
|
||||
let const_name = Ident::new(
|
||||
|
|
|
@ -7,11 +7,14 @@ use syn::{FnArg, Ident, ItemFn, Pat, Type};
|
|||
|
||||
const INVOKER_PREFIX: &str = "__invoke_";
|
||||
|
||||
pub struct Invoker<'a>(pub &'a ItemFn);
|
||||
pub struct Invoker<'a> {
|
||||
pub func: &'a ItemFn,
|
||||
pub is_orchestration: bool,
|
||||
}
|
||||
|
||||
impl<'a> Invoker<'a> {
|
||||
pub fn name(&self) -> String {
|
||||
format!("{}{}", INVOKER_PREFIX, self.0.sig.ident)
|
||||
format!("{}{}", INVOKER_PREFIX, self.func.sig.ident)
|
||||
}
|
||||
|
||||
fn deref_arg_type(ty: &Type) -> &Type {
|
||||
|
@ -32,7 +35,10 @@ impl<'a> Invoker<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
struct CommonInvokerTokens<'a>(pub &'a ItemFn);
|
||||
struct CommonInvokerTokens<'a> {
|
||||
pub func: &'a ItemFn,
|
||||
pub is_orchestration: bool,
|
||||
}
|
||||
|
||||
impl<'a> CommonInvokerTokens<'a> {
|
||||
fn get_input_args(&self) -> (Vec<&'a Ident>, Vec<&'a Type>) {
|
||||
|
@ -77,7 +83,15 @@ impl<'a> CommonInvokerTokens<'a> {
|
|||
.map(|(name, arg_type)| (name, Invoker::deref_arg_type(arg_type)))
|
||||
}
|
||||
|
||||
fn get_args_for_call(&self) -> Vec<::proc_macro2::TokenStream> {
|
||||
fn get_orchestration_state_arg(&self, trigger: &Ident) -> TokenStream {
|
||||
if self.is_orchestration {
|
||||
quote!(let __state = #trigger.as_ref().unwrap().get_state();)
|
||||
} else {
|
||||
TokenStream::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_args_for_call(&self) -> Vec<TokenStream> {
|
||||
self.iter_args()
|
||||
.map(|(name, arg_type)| {
|
||||
let name_str = name.to_string();
|
||||
|
@ -95,7 +109,7 @@ impl<'a> CommonInvokerTokens<'a> {
|
|||
}
|
||||
|
||||
fn iter_args(&self) -> impl Iterator<Item = (&'a Ident, &'a Type)> {
|
||||
self.0.sig.inputs.iter().map(|x| match x {
|
||||
self.func.sig.inputs.iter().map(|x| match x {
|
||||
FnArg::Typed(arg) => (
|
||||
match &*arg.pat {
|
||||
Pat::Ident(name) => &name.ident,
|
||||
|
@ -109,8 +123,8 @@ impl<'a> CommonInvokerTokens<'a> {
|
|||
}
|
||||
|
||||
impl ToTokens for CommonInvokerTokens<'_> {
|
||||
fn to_tokens(&self, tokens: &mut ::proc_macro2::TokenStream) {
|
||||
let target = &self.0.sig.ident;
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let target = &self.func.sig.ident;
|
||||
|
||||
let (args, types) = self.get_input_args();
|
||||
let args_for_match = args.clone();
|
||||
|
@ -124,6 +138,8 @@ impl ToTokens for CommonInvokerTokens<'_> {
|
|||
|
||||
let args_for_call = self.get_args_for_call();
|
||||
|
||||
let orchestration_state = self.get_orchestration_state_arg(trigger_arg);
|
||||
|
||||
quote!(
|
||||
use azure_functions::{IntoVec, FromVec};
|
||||
|
||||
|
@ -137,7 +153,7 @@ impl ToTokens for CommonInvokerTokens<'_> {
|
|||
#trigger_name => #trigger_arg = Some(
|
||||
#trigger_type::new(
|
||||
__param.data.expect("expected parameter binding data"),
|
||||
__metadata.take().expect("expected only one trigger")
|
||||
__metadata.take().expect("expected only one trigger"),
|
||||
)
|
||||
),
|
||||
#(#arg_names => #args_for_match = Some(#arg_assignments),)*
|
||||
|
@ -145,6 +161,8 @@ impl ToTokens for CommonInvokerTokens<'_> {
|
|||
};
|
||||
}
|
||||
|
||||
#orchestration_state
|
||||
|
||||
let __ret = #target(#(#args_for_call,)*);
|
||||
)
|
||||
.to_tokens(tokens);
|
||||
|
@ -152,17 +170,39 @@ impl ToTokens for CommonInvokerTokens<'_> {
|
|||
}
|
||||
|
||||
impl ToTokens for Invoker<'_> {
|
||||
fn to_tokens(&self, tokens: &mut ::proc_macro2::TokenStream) {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let ident = Ident::new(
|
||||
&format!("{}{}", INVOKER_PREFIX, self.0.sig.ident.to_string()),
|
||||
self.0.sig.ident.span(),
|
||||
&format!("{}{}", INVOKER_PREFIX, self.func.sig.ident.to_string()),
|
||||
self.func.sig.ident.span(),
|
||||
);
|
||||
|
||||
let common_tokens = CommonInvokerTokens(&self.0);
|
||||
let common_tokens = CommonInvokerTokens {
|
||||
func: &self.func,
|
||||
is_orchestration: self.is_orchestration,
|
||||
};
|
||||
|
||||
let output_bindings = OutputBindings(self.0);
|
||||
let output_bindings = OutputBindings {
|
||||
func: self.func,
|
||||
is_orchestration: self.is_orchestration,
|
||||
};
|
||||
|
||||
if self.0.sig.asyncness.is_some() {
|
||||
if self.is_orchestration {
|
||||
quote!(
|
||||
#[allow(dead_code)]
|
||||
fn #ident(
|
||||
__req: ::azure_functions::rpc::InvocationRequest,
|
||||
) -> ::azure_functions::rpc::InvocationResponse {
|
||||
#common_tokens
|
||||
|
||||
::azure_functions::durable::orchestrate(
|
||||
__req.invocation_id,
|
||||
__ret,
|
||||
__state,
|
||||
)
|
||||
}
|
||||
)
|
||||
.to_tokens(tokens);
|
||||
} else if self.func.sig.asyncness.is_some() {
|
||||
quote!(
|
||||
#[allow(dead_code)]
|
||||
fn #ident(
|
||||
|
|
|
@ -4,7 +4,10 @@ use proc_macro2::TokenStream;
|
|||
use quote::{quote, ToTokens};
|
||||
use syn::{FnArg, Ident, Index, ItemFn, Pat, ReturnType, Type};
|
||||
|
||||
pub struct OutputBindings<'a>(pub &'a ItemFn);
|
||||
pub struct OutputBindings<'a> {
|
||||
pub func: &'a ItemFn,
|
||||
pub is_orchestration: bool,
|
||||
}
|
||||
|
||||
impl<'a> OutputBindings<'a> {
|
||||
fn get_output_argument_bindings(&self) -> Vec<TokenStream> {
|
||||
|
@ -66,7 +69,7 @@ impl<'a> OutputBindings<'a> {
|
|||
}
|
||||
|
||||
fn iter_output_return_bindings(&self) -> Vec<TokenStream> {
|
||||
match &self.0.sig.output {
|
||||
match &self.func.sig.output {
|
||||
ReturnType::Default => vec![],
|
||||
ReturnType::Type(_, ty) => match &**ty {
|
||||
Type::Tuple(tuple) => tuple
|
||||
|
@ -82,7 +85,7 @@ impl<'a> OutputBindings<'a> {
|
|||
}
|
||||
|
||||
fn iter_mut_args(&self) -> impl Iterator<Item = (&'a Ident, &'a Type)> {
|
||||
self.0.sig.inputs.iter().filter_map(|x| match x {
|
||||
self.func.sig.inputs.iter().filter_map(|x| match x {
|
||||
FnArg::Typed(arg) => {
|
||||
if let Type::Reference(tr) = &*arg.ty {
|
||||
tr.mutability?;
|
||||
|
@ -167,6 +170,11 @@ impl<'a> OutputBindings<'a> {
|
|||
|
||||
impl ToTokens for OutputBindings<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
// Orchestration functions have no output bindings
|
||||
if self.is_orchestration {
|
||||
return;
|
||||
}
|
||||
|
||||
for binding in self.get_output_argument_bindings() {
|
||||
binding.to_tokens(tokens);
|
||||
}
|
||||
|
@ -175,7 +183,7 @@ impl ToTokens for OutputBindings<'_> {
|
|||
binding.to_tokens(tokens);
|
||||
}
|
||||
|
||||
match &self.0.sig.output {
|
||||
match &self.func.sig.output {
|
||||
ReturnType::Default => {}
|
||||
ReturnType::Type(_, ty) => {
|
||||
if let Some(binding) = OutputBindings::get_return_binding(ty, false) {
|
||||
|
|
|
@ -47,7 +47,7 @@ impl BindingArguments {
|
|||
}
|
||||
)
|
||||
} else {
|
||||
quote!()
|
||||
TokenStream::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,7 +311,8 @@ impl Field {
|
|||
}
|
||||
}
|
||||
}
|
||||
quote!()
|
||||
|
||||
TokenStream::new()
|
||||
}
|
||||
|
||||
pub fn get_field_assignment(&self) -> TokenStream {
|
||||
|
@ -392,7 +393,7 @@ fn get_default_direction_serialization(
|
|||
FieldType::Direction => true,
|
||||
_ => false,
|
||||
}) {
|
||||
return quote!();
|
||||
return TokenStream::new();
|
||||
}
|
||||
|
||||
if let Some(direction) = binding_args.direction.as_ref() {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod activity_trigger;
|
||||
mod blob;
|
||||
mod blob_trigger;
|
||||
mod cosmos_db;
|
||||
|
@ -8,6 +9,8 @@ mod event_hub_trigger;
|
|||
mod generic;
|
||||
mod http;
|
||||
mod http_trigger;
|
||||
mod orchestration_client;
|
||||
mod orchestration_trigger;
|
||||
mod queue;
|
||||
mod queue_trigger;
|
||||
mod send_grid;
|
||||
|
@ -19,6 +22,7 @@ mod table;
|
|||
mod timer_trigger;
|
||||
mod twilio_sms;
|
||||
|
||||
pub use self::activity_trigger::*;
|
||||
pub use self::blob::*;
|
||||
pub use self::blob_trigger::*;
|
||||
pub use self::cosmos_db::*;
|
||||
|
@ -29,6 +33,8 @@ pub use self::event_hub_trigger::*;
|
|||
pub use self::generic::*;
|
||||
pub use self::http::*;
|
||||
pub use self::http_trigger::*;
|
||||
pub use self::orchestration_client::*;
|
||||
pub use self::orchestration_trigger::*;
|
||||
pub use self::queue::*;
|
||||
pub use self::queue_trigger::*;
|
||||
pub use self::send_grid::*;
|
||||
|
@ -65,7 +71,6 @@ impl Default for Direction {
|
|||
#[serde(untagged, rename_all = "camelCase")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Binding {
|
||||
Context,
|
||||
HttpTrigger(HttpTrigger),
|
||||
Http(Http),
|
||||
TimerTrigger(TimerTrigger),
|
||||
|
@ -87,12 +92,14 @@ pub enum Binding {
|
|||
SendGrid(SendGrid),
|
||||
GenericTrigger(Generic),
|
||||
Generic(Generic),
|
||||
OrchestrationClient(OrchestrationClient),
|
||||
OrchestrationTrigger(OrchestrationTrigger),
|
||||
ActivityTrigger(ActivityTrigger),
|
||||
}
|
||||
|
||||
impl Binding {
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Binding::Context => None,
|
||||
Binding::HttpTrigger(b) => Some(&b.name),
|
||||
Binding::Http(b) => Some(&b.name),
|
||||
Binding::TimerTrigger(b) => Some(&b.name),
|
||||
|
@ -114,12 +121,14 @@ impl Binding {
|
|||
Binding::SendGrid(b) => Some(&b.name),
|
||||
Binding::GenericTrigger(b) => Some(&b.name),
|
||||
Binding::Generic(b) => Some(&b.name),
|
||||
Binding::OrchestrationClient(b) => Some(&b.name),
|
||||
Binding::OrchestrationTrigger(b) => Some(&b.name),
|
||||
Binding::ActivityTrigger(b) => Some(&b.name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding_type(&self) -> Option<&str> {
|
||||
match self {
|
||||
Binding::Context => None,
|
||||
Binding::HttpTrigger(_) => Some(HttpTrigger::binding_type()),
|
||||
Binding::Http(_) => Some(HttpTrigger::binding_type()),
|
||||
Binding::TimerTrigger(_) => Some(TimerTrigger::binding_type()),
|
||||
|
@ -141,13 +150,9 @@ impl Binding {
|
|||
Binding::SendGrid(_) => Some(SendGrid::binding_type()),
|
||||
Binding::GenericTrigger(b) => Some(b.binding_type()),
|
||||
Binding::Generic(b) => Some(b.binding_type()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_context(&self) -> bool {
|
||||
match self {
|
||||
Binding::Context => true,
|
||||
_ => false,
|
||||
Binding::OrchestrationClient(_) => Some(OrchestrationClient::binding_type()),
|
||||
Binding::OrchestrationTrigger(_) => Some(OrchestrationTrigger::binding_type()),
|
||||
Binding::ActivityTrigger(_) => Some(ActivityTrigger::binding_type()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,7 +166,9 @@ impl Binding {
|
|||
| Binding::EventHubTrigger(_)
|
||||
| Binding::CosmosDbTrigger(_)
|
||||
| Binding::ServiceBusTrigger(_)
|
||||
| Binding::GenericTrigger(_) => true,
|
||||
| Binding::GenericTrigger(_)
|
||||
| Binding::OrchestrationTrigger(_)
|
||||
| Binding::ActivityTrigger(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +177,6 @@ impl Binding {
|
|||
impl ToTokens for Binding {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
Binding::Context => panic!("context bindings cannot be tokenized"),
|
||||
Binding::HttpTrigger(b) => {
|
||||
quote!(::azure_functions::codegen::bindings::Binding::HttpTrigger(#b))
|
||||
}
|
||||
|
@ -226,6 +232,15 @@ impl ToTokens for Binding {
|
|||
Binding::Generic(b) => {
|
||||
quote!(::azure_functions::codegen::bindings::Binding::Generic(#b))
|
||||
}
|
||||
Binding::OrchestrationClient(b) => {
|
||||
quote!(::azure_functions::codegen::bindings::Binding::OrchestrationClient(#b))
|
||||
}
|
||||
Binding::OrchestrationTrigger(b) => {
|
||||
quote!(::azure_functions::codegen::bindings::Binding::OrchestrationTrigger(#b))
|
||||
}
|
||||
Binding::ActivityTrigger(b) => {
|
||||
quote!(::azure_functions::codegen::bindings::Binding::ActivityTrigger(#b))
|
||||
}
|
||||
}
|
||||
.to_tokens(tokens);
|
||||
}
|
||||
|
@ -264,6 +279,12 @@ lazy_static! {
|
|||
map.insert("GenericTrigger", |args, span| {
|
||||
Binding::GenericTrigger(Generic::from((args, span)))
|
||||
});
|
||||
map.insert("DurableOrchestrationContext", |args, span| {
|
||||
Binding::OrchestrationTrigger(OrchestrationTrigger::from((args, span)))
|
||||
});
|
||||
map.insert("DurableActivityContext", |args, span| {
|
||||
Binding::ActivityTrigger(ActivityTrigger::from((args, span)))
|
||||
});
|
||||
map
|
||||
};
|
||||
pub static ref INPUT_BINDINGS: BindingMap = {
|
||||
|
@ -281,6 +302,9 @@ lazy_static! {
|
|||
map.insert("GenericInput", |args, span| {
|
||||
Binding::Generic(Generic::from((args, span)))
|
||||
});
|
||||
map.insert("DurableOrchestrationClient", |args, span| {
|
||||
Binding::OrchestrationClient(OrchestrationClient::from((args, span)))
|
||||
});
|
||||
map
|
||||
};
|
||||
pub static ref INPUT_OUTPUT_BINDINGS: BindingMap = {
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
use azure_functions_shared_codegen::binding;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[binding(name = "activityTrigger", direction = "in")]
|
||||
pub struct ActivityTrigger {
|
||||
#[field(camel_case_value = true)]
|
||||
pub name: Cow<'static, str>,
|
||||
pub activity: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::codegen::tests::should_panic;
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::ToTokens;
|
||||
use serde_json::to_string;
|
||||
use syn::{parse_str, NestedMeta};
|
||||
|
||||
#[test]
|
||||
fn it_serializes_to_json() {
|
||||
let binding = ActivityTrigger {
|
||||
name: Cow::from("foo"),
|
||||
activity: Some(Cow::from("bar")),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
to_string(&binding).unwrap(),
|
||||
r#"{"type":"activityTrigger","direction":"in","name":"foo","activity":"bar"}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_attribute_arguments() {
|
||||
let binding: ActivityTrigger = (
|
||||
vec![
|
||||
parse_str::<NestedMeta>(r#"name = "foo""#).unwrap(),
|
||||
parse_str::<NestedMeta>(r#"activity = "bar""#).unwrap(),
|
||||
],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
|
||||
assert_eq!(binding.name.as_ref(), "foo");
|
||||
assert_eq!(binding.activity.as_ref().unwrap(), "bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_name_attribute_argument() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: ActivityTrigger = (vec![], Span::call_site()).into();
|
||||
},
|
||||
"the 'name' argument is required for this binding",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_name_attribute_be_a_string() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: ActivityTrigger = (
|
||||
vec![parse_str::<NestedMeta>(r#"name = false"#).unwrap()],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
},
|
||||
"expected a literal string value for the 'name' argument",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_activity_attribute_be_a_string() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: ActivityTrigger = (
|
||||
vec![parse_str::<NestedMeta>(r#"activity = false"#).unwrap()],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
},
|
||||
"expected a literal string value for the 'activity' argument",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_converts_to_tokens() {
|
||||
let binding = ActivityTrigger {
|
||||
name: Cow::from("foo"),
|
||||
activity: Some(Cow::from("bar")),
|
||||
};
|
||||
|
||||
let mut stream = TokenStream::new();
|
||||
binding.to_tokens(&mut stream);
|
||||
let mut tokens = stream.to_string();
|
||||
tokens.retain(|c| c != ' ');
|
||||
|
||||
assert_eq!(tokens, r#"::azure_functions::codegen::bindings::ActivityTrigger{name:::std::borrow::Cow::Borrowed("foo"),activity:Some(::std::borrow::Cow::Borrowed("bar")),}"#);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
use azure_functions_shared_codegen::binding;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[binding(name = "orchestrationClient", direction = "in")]
|
||||
pub struct OrchestrationClient {
|
||||
#[field(camel_case_value = true)]
|
||||
pub name: Cow<'static, str>,
|
||||
#[field(name = "taskHub")]
|
||||
pub task_hub: Option<Cow<'static, str>>,
|
||||
#[field(name = "connectionName")]
|
||||
pub connection: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::codegen::tests::should_panic;
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::ToTokens;
|
||||
use serde_json::to_string;
|
||||
use syn::{parse_str, NestedMeta};
|
||||
|
||||
#[test]
|
||||
fn it_serializes_to_json() {
|
||||
let binding = OrchestrationClient {
|
||||
name: Cow::from("foo"),
|
||||
task_hub: Some(Cow::from("bar")),
|
||||
connection: Some(Cow::from("baz")),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
to_string(&binding).unwrap(),
|
||||
r#"{"type":"orchestrationClient","direction":"in","name":"foo","taskHub":"bar","connectionName":"baz"}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_attribute_arguments() {
|
||||
let binding: OrchestrationClient = (
|
||||
vec![
|
||||
parse_str::<NestedMeta>(r#"name = "foo""#).unwrap(),
|
||||
parse_str::<NestedMeta>(r#"task_hub = "bar""#).unwrap(),
|
||||
parse_str::<NestedMeta>(r#"connection = "baz""#).unwrap(),
|
||||
],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
|
||||
assert_eq!(binding.name.as_ref(), "foo");
|
||||
assert_eq!(binding.task_hub.as_ref().unwrap(), "bar");
|
||||
assert_eq!(binding.connection.as_ref().unwrap(), "baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_name_attribute_argument() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: OrchestrationClient = (vec![], Span::call_site()).into();
|
||||
},
|
||||
"the 'name' argument is required for this binding",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_name_attribute_be_a_string() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: OrchestrationClient = (
|
||||
vec![parse_str::<NestedMeta>(r#"name = false"#).unwrap()],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
},
|
||||
"expected a literal string value for the 'name' argument",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_task_hub_attribute_be_a_string() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: OrchestrationClient = (
|
||||
vec![parse_str::<NestedMeta>(r#"task_hub = false"#).unwrap()],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
},
|
||||
"expected a literal string value for the 'task_hub' argument",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_connection_attribute_be_a_string() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: OrchestrationClient = (
|
||||
vec![parse_str::<NestedMeta>(r#"connection = false"#).unwrap()],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
},
|
||||
"expected a literal string value for the 'connection' argument",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_converts_to_tokens() {
|
||||
let binding = OrchestrationClient {
|
||||
name: Cow::from("foo"),
|
||||
task_hub: Some(Cow::from("bar")),
|
||||
connection: Some(Cow::from("baz")),
|
||||
};
|
||||
|
||||
let mut stream = TokenStream::new();
|
||||
binding.to_tokens(&mut stream);
|
||||
let mut tokens = stream.to_string();
|
||||
tokens.retain(|c| c != ' ');
|
||||
|
||||
assert_eq!(tokens, r#"::azure_functions::codegen::bindings::OrchestrationClient{name:::std::borrow::Cow::Borrowed("foo"),task_hub:Some(::std::borrow::Cow::Borrowed("bar")),connection:Some(::std::borrow::Cow::Borrowed("baz")),}"#);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
use azure_functions_shared_codegen::binding;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[binding(name = "orchestrationTrigger", direction = "in")]
|
||||
pub struct OrchestrationTrigger {
|
||||
#[field(camel_case_value = true)]
|
||||
pub name: Cow<'static, str>,
|
||||
pub orchestration: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::codegen::tests::should_panic;
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::ToTokens;
|
||||
use serde_json::to_string;
|
||||
use syn::{parse_str, NestedMeta};
|
||||
|
||||
#[test]
|
||||
fn it_serializes_to_json() {
|
||||
let binding = OrchestrationTrigger {
|
||||
name: Cow::from("foo"),
|
||||
orchestration: Some(Cow::from("bar")),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
to_string(&binding).unwrap(),
|
||||
r#"{"type":"orchestrationTrigger","direction":"in","name":"foo","orchestration":"bar"}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_attribute_arguments() {
|
||||
let binding: OrchestrationTrigger = (
|
||||
vec![
|
||||
parse_str::<NestedMeta>(r#"name = "foo""#).unwrap(),
|
||||
parse_str::<NestedMeta>(r#"orchestration = "bar""#).unwrap(),
|
||||
],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
|
||||
assert_eq!(binding.name.as_ref(), "foo");
|
||||
assert_eq!(binding.orchestration.as_ref().unwrap(), "bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_name_attribute_argument() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: OrchestrationTrigger = (vec![], Span::call_site()).into();
|
||||
},
|
||||
"the 'name' argument is required for this binding",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_name_attribute_be_a_string() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: OrchestrationTrigger = (
|
||||
vec![parse_str::<NestedMeta>(r#"name = false"#).unwrap()],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
},
|
||||
"expected a literal string value for the 'name' argument",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_requires_the_orchestration_attribute_be_a_string() {
|
||||
should_panic(
|
||||
|| {
|
||||
let _: OrchestrationTrigger = (
|
||||
vec![parse_str::<NestedMeta>(r#"orchestration = false"#).unwrap()],
|
||||
Span::call_site(),
|
||||
)
|
||||
.into();
|
||||
},
|
||||
"expected a literal string value for the 'orchestration' argument",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_converts_to_tokens() {
|
||||
let binding = OrchestrationTrigger {
|
||||
name: Cow::from("foo"),
|
||||
orchestration: Some(Cow::from("bar")),
|
||||
};
|
||||
|
||||
let mut stream = TokenStream::new();
|
||||
binding.to_tokens(&mut stream);
|
||||
let mut tokens = stream.to_string();
|
||||
tokens.retain(|c| c != ' ');
|
||||
|
||||
assert_eq!(tokens, r#"::azure_functions::codegen::bindings::OrchestrationTrigger{name:::std::borrow::Cow::Borrowed("foo"),orchestration:Some(::std::borrow::Cow::Borrowed("bar")),}"#);
|
||||
}
|
||||
}
|
|
@ -143,7 +143,7 @@ impl ToTokens for Function {
|
|||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let name = QuotableBorrowedStr(&self.name);
|
||||
let disabled = self.disabled;
|
||||
let bindings = self.bindings.iter().filter(|x| !x.is_context());
|
||||
let bindings = self.bindings.iter();
|
||||
let invoker = QuotableOption(self.invoker.as_ref());
|
||||
|
||||
quote!(
|
||||
|
|
|
@ -3,6 +3,9 @@ mod blob;
|
|||
mod blob_trigger;
|
||||
mod cosmos_db_document;
|
||||
mod cosmos_db_trigger;
|
||||
mod durable_activity_context;
|
||||
mod durable_orchestration_client;
|
||||
mod durable_orchestration_context;
|
||||
mod event_grid_event;
|
||||
mod event_hub_message;
|
||||
mod event_hub_trigger;
|
||||
|
@ -27,6 +30,9 @@ pub use self::blob::*;
|
|||
pub use self::blob_trigger::*;
|
||||
pub use self::cosmos_db_document::*;
|
||||
pub use self::cosmos_db_trigger::*;
|
||||
pub use self::durable_activity_context::*;
|
||||
pub use self::durable_orchestration_client::*;
|
||||
pub use self::durable_orchestration_context::*;
|
||||
pub use self::event_grid_event::*;
|
||||
pub use self::event_hub_message::*;
|
||||
pub use self::event_hub_trigger::*;
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
use crate::rpc::TypedData;
|
||||
use serde_derive::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents the Durable Functions activity context binding.
|
||||
///
|
||||
/// The following binding attributes are supported:
|
||||
///
|
||||
/// | Name | Description |
|
||||
/// |------------|------------------------------------------------------------------|
|
||||
/// | `name` | The name of the parameter being bound. |
|
||||
/// | `activity` | The name of the activity. Defaults to the name of the function. |
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// TODO: IMPLEMENT
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DurableActivityContext {}
|
||||
|
||||
impl DurableActivityContext {
|
||||
#[doc(hidden)]
|
||||
pub fn new(data: TypedData, metadata: HashMap<String, TypedData>) -> Self {
|
||||
println!("{:#?}", data);
|
||||
println!("{:#?}", metadata);
|
||||
DurableActivityContext {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::rpc::typed_data::Data;
|
||||
|
||||
#[test]
|
||||
fn it_constructs() {
|
||||
let data = TypedData {
|
||||
data: Some(Data::String(r#"{ }"#.to_owned())),
|
||||
};
|
||||
|
||||
let _ = DurableActivityContext::new(data, HashMap::new());
|
||||
|
||||
// TODO: implement
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
use crate::{
|
||||
durable::{CreationUrls, ManagementUrls},
|
||||
rpc::{typed_data::Data, TypedData},
|
||||
};
|
||||
use serde_derive::Deserialize;
|
||||
use serde_json::from_str;
|
||||
|
||||
/// Represents the Durable Functions orchestration client input binding.
|
||||
///
|
||||
/// The following binding attributes are supported:
|
||||
///
|
||||
/// | Name | Description |
|
||||
/// |--------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
/// | `name` | The name of the parameter being bound. |
|
||||
/// | `task_hub` | The name of the task hub to use. Defaults to the value from host.json |
|
||||
/// | `connection` | The name of an app setting that contains a storage account connection string. Defaults to the storage account for the function application. |
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// TODO: IMPLEMENT
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DurableOrchestrationClient {
|
||||
#[serde(rename = "taskHubName")]
|
||||
task_hub: String,
|
||||
creation_urls: CreationUrls,
|
||||
management_urls: ManagementUrls,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl From<TypedData> for DurableOrchestrationClient {
|
||||
fn from(data: TypedData) -> Self {
|
||||
match &data.data {
|
||||
Some(Data::String(s)) => {
|
||||
from_str(s).expect("failed to parse durable orchestration client data")
|
||||
}
|
||||
_ => panic!("expected string data for durable orchestration client"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_converts_from_typed_data() {
|
||||
let data = TypedData {
|
||||
data: Some(Data::String(r#"{"taskHubName":"test","creationUrls":{"createNewInstancePostUri":"http://localhost:8080/runtime/webhooks/durabletask/orchestrators/{functionName}[/{instanceId}]?code=foo","createAndWaitOnNewInstancePostUri":"http://localhost:8080/runtime/webhooks/durabletask/orchestrators/{functionName}[/{instanceId}]?timeout={timeoutInSeconds}&pollingInterval={intervalInSeconds}&code=foo"},"managementUrls":{"id":"INSTANCEID","statusQueryGetUri":"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID?taskHub=DurableFunctionsHub&connection=Storage&code=foo","sendEventPostUri":"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=foo","terminatePostUri":"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=foo","rewindPostUri":"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID/rewind?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=foo","purgeHistoryDeleteUri":"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID?taskHub=DurableFunctionsHub&connection=Storage&code=foo"}}"#.to_owned())),
|
||||
};
|
||||
|
||||
let client: DurableOrchestrationClient = data.into();
|
||||
assert_eq!(client.task_hub, "test");
|
||||
assert_eq!(
|
||||
client.creation_urls.create_new_instance_url,
|
||||
"http://localhost:8080/runtime/webhooks/durabletask/orchestrators/{functionName}[/{instanceId}]?code=foo"
|
||||
);
|
||||
assert_eq!(
|
||||
client.creation_urls.create_new_instance_and_wait_url,
|
||||
"http://localhost:8080/runtime/webhooks/durabletask/orchestrators/{functionName}[/{instanceId}]?timeout={timeoutInSeconds}&pollingInterval={intervalInSeconds}&code=foo"
|
||||
);
|
||||
assert_eq!(client.management_urls.id, "INSTANCEID");
|
||||
assert_eq!(
|
||||
client.management_urls.status_query_url,
|
||||
"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID?taskHub=DurableFunctionsHub&connection=Storage&code=foo"
|
||||
);
|
||||
assert_eq!(
|
||||
client.management_urls.raise_event_url,
|
||||
"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=foo"
|
||||
);
|
||||
assert_eq!(
|
||||
client.management_urls.terminate_url,
|
||||
"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=foo"
|
||||
);
|
||||
assert_eq!(
|
||||
client.management_urls.rewind_url,
|
||||
"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID/rewind?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=foo"
|
||||
);
|
||||
assert_eq!(
|
||||
client.management_urls.purge_history_url,
|
||||
"http://localhost:8080/runtime/webhooks/durabletask/instances/INSTANCEID?taskHub=DurableFunctionsHub&connection=Storage&code=foo"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
use crate::{
|
||||
durable::OrchestrationState,
|
||||
rpc::{typed_data::Data, TypedData},
|
||||
};
|
||||
use serde_json::from_str;
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
/// Represents the Durable Functions orchestration context binding.
|
||||
///
|
||||
/// The following binding attributes are supported:
|
||||
///
|
||||
/// | Name | Description |
|
||||
/// |-----------------|-----------------------------------------------------------------------|
|
||||
/// | `name` | The name of the parameter being bound. |
|
||||
/// | `orchestration` | The name of the orchestration. Defaults to the name of the function. |
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// TODO: IMPLEMENT
|
||||
#[derive(Debug)]
|
||||
pub struct DurableOrchestrationContext {
|
||||
state: Rc<RefCell<OrchestrationState>>,
|
||||
}
|
||||
|
||||
impl DurableOrchestrationContext {
|
||||
#[doc(hidden)]
|
||||
pub fn new(data: TypedData, _metadata: HashMap<String, TypedData>) -> Self {
|
||||
DurableOrchestrationContext {
|
||||
state: Rc::new(RefCell::new(match &data.data {
|
||||
Some(Data::String(s)) => {
|
||||
from_str(s).expect("failed to parse orchestration context data")
|
||||
}
|
||||
_ => panic!("expected JSON data for orchestration context data"),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn get_state(&self) -> Rc<RefCell<OrchestrationState>> {
|
||||
self.state.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"history":[
|
||||
{
|
||||
"EventType":12,
|
||||
"EventId":-1,
|
||||
"IsPlayed":false,
|
||||
"Timestamp":"2019-07-18T06:22:27.016757Z"
|
||||
},
|
||||
{
|
||||
"OrchestrationInstance":{
|
||||
"InstanceId":"49497890673e4a75ab380e7a956c607b",
|
||||
"ExecutionId":"5d2025984bef476bbaacefaa499a4f5f"
|
||||
},
|
||||
"EventType":0,
|
||||
"ParentInstance":null,
|
||||
"Name":"HelloWorld",
|
||||
"Version":"",
|
||||
"Input":"{}",
|
||||
"Tags":null,
|
||||
"EventId":-1,
|
||||
"IsPlayed":false,
|
||||
"Timestamp":"2019-07-18T06:22:26.626966Z"
|
||||
}
|
||||
],
|
||||
"input":{
|
||||
|
||||
},
|
||||
"instanceId":"49497890673e4a75ab380e7a956c607b",
|
||||
"isReplaying":false,
|
||||
"parentInstanceId":null
|
||||
}
|
||||
*/
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::rpc::typed_data::Data;
|
||||
|
||||
#[test]
|
||||
fn it_constructs() {
|
||||
let data = TypedData {
|
||||
data: Some(Data::String(r#"{ }"#.to_owned())),
|
||||
};
|
||||
|
||||
let _ = DurableOrchestrationContext::new(data, HashMap::new());
|
||||
|
||||
// TODO: implement
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
//! Module for Durable Functions types.
|
||||
use crate::rpc::{
|
||||
status_result::Status, typed_data::Data, InvocationResponse, StatusResult, TypedData,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::to_string;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
future::Future,
|
||||
ptr::null,
|
||||
rc::Rc,
|
||||
task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
|
||||
};
|
||||
|
||||
mod creation_urls;
|
||||
mod management_urls;
|
||||
|
||||
pub use self::creation_urls::*;
|
||||
pub use self::management_urls::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OrchestrationState {
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl OrchestrationState {
|
||||
fn mark_done(&mut self) {
|
||||
self.done = true;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn waker_clone(_: *const ()) -> RawWaker {
|
||||
panic!("orchestration functions cannot perform asynchronous operations");
|
||||
}
|
||||
|
||||
unsafe fn waker_wake(_: *const ()) {
|
||||
panic!("orchestration functions cannot perform asynchronous operations");
|
||||
}
|
||||
|
||||
unsafe fn waker_wake_by_ref(_: *const ()) {
|
||||
panic!("orchestration functions cannot perform asynchronous operations");
|
||||
}
|
||||
|
||||
unsafe fn waker_drop(_: *const ()) {}
|
||||
|
||||
/// The entrypoint for orchestration functions.
|
||||
///
|
||||
/// The given future is the user function.
|
||||
#[doc(hidden)]
|
||||
pub fn orchestrate(
|
||||
id: String,
|
||||
func: impl Future<Output = ()>,
|
||||
state: Rc<RefCell<OrchestrationState>>,
|
||||
) -> InvocationResponse {
|
||||
let waker = unsafe {
|
||||
Waker::from_raw(RawWaker::new(
|
||||
null(),
|
||||
&RawWakerVTable::new(waker_clone, waker_wake, waker_wake_by_ref, waker_drop),
|
||||
))
|
||||
};
|
||||
|
||||
match Future::poll(Box::pin(func).as_mut(), &mut Context::from_waker(&waker)) {
|
||||
Poll::Ready(_) => {
|
||||
// Orchestration has completed and the result is ready, return done with output
|
||||
state.borrow_mut().mark_done();
|
||||
}
|
||||
Poll::Pending => {
|
||||
// Orchestration has not yet completed
|
||||
}
|
||||
};
|
||||
|
||||
InvocationResponse {
|
||||
invocation_id: id,
|
||||
return_value: Some(TypedData {
|
||||
data: Some(Data::Json(to_string(&*state.borrow()).unwrap())),
|
||||
}),
|
||||
result: Some(StatusResult {
|
||||
status: Status::Success as i32,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
use serde_derive::Deserialize;
|
||||
|
||||
/// Represents the Durable Funtions client creation URLs.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationUrls {
|
||||
/// The URL for creating a new orchestration instance.
|
||||
#[serde(rename = "createNewInstancePostUri")]
|
||||
pub create_new_instance_url: String,
|
||||
/// The URL for creating and waiting on a new orchestration instance.
|
||||
#[serde(rename = "createAndWaitOnNewInstancePostUri")]
|
||||
pub create_new_instance_and_wait_url: String,
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
use serde_derive::Deserialize;
|
||||
|
||||
/// Represents the Durable Funtions client management URLs.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ManagementUrls {
|
||||
/// The ID of the orchestration instance.
|
||||
pub id: String,
|
||||
/// The status URL of the orchestration instance.
|
||||
#[serde(rename = "statusQueryGetUri")]
|
||||
pub status_query_url: String,
|
||||
/// The "raise event" URL of the orchestration instance.
|
||||
#[serde(rename = "sendEventPostUri")]
|
||||
pub raise_event_url: String,
|
||||
/// The "terminate" URL of the orchestration instance.
|
||||
#[serde(rename = "terminatePostUri")]
|
||||
pub terminate_url: String,
|
||||
/// The "rewind" URL of the orchestration instance.
|
||||
#[serde(rename = "rewindPostUri")]
|
||||
pub rewind_url: String,
|
||||
/// The "purge history" URL of the orchestration instance.
|
||||
#[serde(rename = "purgeHistoryDeleteUri")]
|
||||
pub purge_history_url: String,
|
||||
}
|
|
@ -106,6 +106,7 @@ mod worker;
|
|||
pub mod bindings;
|
||||
pub mod blob;
|
||||
pub mod context;
|
||||
pub mod durable;
|
||||
pub mod event_hub;
|
||||
pub mod generic;
|
||||
pub mod http;
|
||||
|
|
|
@ -20,6 +20,8 @@ const TWILIO_PACKAGE_NAME: &str = "microsoft.azure.webjobs.extensions.twilio";
|
|||
const TWILIO_PACKAGE_VERSION: &str = "3.0.0";
|
||||
const SEND_GRID_PACKAGE_NAME: &str = "microsoft.azure.webjobs.extensions.sendgrid";
|
||||
const SEND_GRID_PACKAGE_VERSION: &str = "3.0.0";
|
||||
const DURABLE_TASK_PACKAGE_NAME: &str = "microsoft.azure.webjobs.extensions.durabletask";
|
||||
const DURABLE_TASK_PACKAGE_VERSION: &str = "1.8.3";
|
||||
|
||||
lazy_static! {
|
||||
// This comes from https://github.com/Azure/azure-functions-core-tools/blob/master/src/Azure.Functions.Cli/Common/Constants.cs#L63
|
||||
|
@ -89,6 +91,18 @@ lazy_static! {
|
|||
bindings::SendGrid::binding_type(),
|
||||
(SEND_GRID_PACKAGE_NAME, SEND_GRID_PACKAGE_VERSION),
|
||||
);
|
||||
map.insert(
|
||||
bindings::OrchestrationClient::binding_type(),
|
||||
(DURABLE_TASK_PACKAGE_NAME, DURABLE_TASK_PACKAGE_VERSION),
|
||||
);
|
||||
map.insert(
|
||||
bindings::OrchestrationTrigger::binding_type(),
|
||||
(DURABLE_TASK_PACKAGE_NAME, DURABLE_TASK_PACKAGE_VERSION),
|
||||
);
|
||||
map.insert(
|
||||
bindings::ActivityTrigger::binding_type(),
|
||||
(DURABLE_TASK_PACKAGE_NAME, DURABLE_TASK_PACKAGE_VERSION),
|
||||
);
|
||||
map
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
target/
|
||||
Cargo.lock
|
||||
.vscode/
|
||||
.git/
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "durable-functions-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Peter Huene <peterhuene@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
azure-functions = { path = "../../azure-functions" }
|
||||
log = "0.4.6"
|
||||
futures-preview = { version = "0.3.0-alpha.17", optional = true }
|
||||
|
||||
[features]
|
||||
unstable = ["azure-functions/unstable", "futures-preview"]
|
|
@ -0,0 +1,30 @@
|
|||
# syntax=docker/dockerfile-upstream:experimental
|
||||
|
||||
FROM peterhuene/azure-functions-rs-build:0.10.0 AS build-image
|
||||
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
|
||||
# Run with mounted cache
|
||||
RUN --mount=type=cache,target=/src/target \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
["cargo", "run", "--release", "--", "init", "--script-root", "/home/site/wwwroot", "--sync-extensions"]
|
||||
|
||||
FROM mcr.microsoft.com/azure-functions/base:2.0 as runtime-image
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/runtime-deps:2.2
|
||||
|
||||
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
|
||||
HOME=/home \
|
||||
FUNCTIONS_WORKER_RUNTIME=Rust \
|
||||
languageWorkers__workersDirectory=/home/site/wwwroot/workers
|
||||
|
||||
# Copy the Azure Functions host from the runtime image
|
||||
COPY --from=runtime-image [ "/azure-functions-host", "/azure-functions-host" ]
|
||||
|
||||
# Copy the script root contents from the build image
|
||||
COPY --from=build-image ["/home/site/wwwroot", "/home/site/wwwroot"]
|
||||
|
||||
WORKDIR /home/site/wwwroot
|
||||
CMD [ "/azure-functions-host/Microsoft.Azure.WebJobs.Script.WebHost" ]
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"default": "Information"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "Rust",
|
||||
"languageWorkers:workersDirectory": "workers"
|
||||
},
|
||||
"ConnectionStrings": {}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
use azure_functions::{bindings::DurableOrchestrationContext, func};
|
||||
use futures::future::join_all;
|
||||
|
||||
#[func(name = "HelloWorld")]
|
||||
pub async fn hello_world(context: DurableOrchestrationContext) {
|
||||
// context.set_output(
|
||||
// join_all(
|
||||
// [
|
||||
// context.call_activity("SayHello", "Tokyo"),
|
||||
// context.call_activity("SayHello", "London"),
|
||||
// context.call_activity("SayHello", "Seattle"),
|
||||
// ]
|
||||
// .into_iter(),
|
||||
// )
|
||||
// .await,
|
||||
// );
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// WARNING: This file is regenerated by the `cargo func new` command.
|
||||
|
||||
#[cfg(feature = "unstable")]
|
||||
mod hello_world;
|
||||
#[cfg(feature = "unstable")]
|
||||
mod say_hello;
|
||||
#[cfg(feature = "unstable")]
|
||||
mod start;
|
||||
|
||||
// Export the Azure Functions here.
|
||||
#[cfg(feature = "unstable")]
|
||||
azure_functions::export! {
|
||||
hello_world,
|
||||
say_hello,
|
||||
start,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unstable"))]
|
||||
azure_functions::export! {}
|
|
@ -0,0 +1,13 @@
|
|||
use azure_functions::{bindings::DurableActivityContext, func};
|
||||
|
||||
#[func(name = "SayHello")]
|
||||
pub async fn say_hello(_context: DurableActivityContext) {
|
||||
// context.set_output(format!(
|
||||
// "Hello {}!",
|
||||
// context
|
||||
// .get_input()
|
||||
// .as_str()
|
||||
// .expect("expected a string input")
|
||||
// ));
|
||||
unimplemented!()
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
use azure_functions::{
|
||||
bindings::{DurableOrchestrationClient, HttpRequest, HttpResponse},
|
||||
func,
|
||||
};
|
||||
|
||||
#[func]
|
||||
pub async fn start(_req: HttpRequest, _client: DurableOrchestrationClient) -> HttpResponse {
|
||||
// match client.start_new("HelloWorld").await {
|
||||
// Ok(_) => "Orchestration started.".into(),
|
||||
// Err(e) => format!("Failed to start orchestration: {}", e).into()
|
||||
// }
|
||||
unimplemented!()
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
#![cfg_attr(feature = "unstable", feature(async_await))]
|
||||
|
||||
mod functions;
|
||||
|
||||
fn main() {
|
||||
azure_functions::worker_main(std::env::args(), functions::EXPORTS);
|
||||
}
|
Loading…
Reference in New Issue