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:
Peter Huene 2019-07-17 23:34:16 -07:00 committed by Peter Huene
parent 1ef51523d2
commit 50d45593fd
No known key found for this signature in database
GPG Key ID: E1D265D820213D6A
31 changed files with 1067 additions and 88 deletions

9
Cargo.lock generated
View File

@ -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"

View File

@ -19,4 +19,5 @@ members = [
"examples/twilio",
"examples/sendgrid",
"examples/generic",
"examples/durable-functions",
]

View File

@ -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.

View File

@ -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(

View File

@ -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(

View File

@ -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) {

View File

@ -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() {

View File

@ -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 = {

View File

@ -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")),}"#);
}
}

View File

@ -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")),}"#);
}
}

View File

@ -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")),}"#);
}
}

View File

@ -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!(

View File

@ -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::*;

View File

@ -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
}
}

View File

@ -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"
);
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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;

View File

@ -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
};
}

View File

@ -0,0 +1,4 @@
target/
Cargo.lock
.vscode/
.git/

View File

@ -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"]

View File

@ -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" ]

View File

@ -0,0 +1,8 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"default": "Information"
}
}
}

View File

@ -0,0 +1,8 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "Rust",
"languageWorkers:workersDirectory": "workers"
},
"ConnectionStrings": {}
}

View File

@ -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,
// );
}

View File

@ -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! {}

View File

@ -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!()
}

View File

@ -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!()
}

View File

@ -0,0 +1,7 @@
#![cfg_attr(feature = "unstable", feature(async_await))]
mod functions;
fn main() {
azure_functions::worker_main(std::env::args(), functions::EXPORTS);
}