1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! Gateway extends the [crowbar](https://crates.io/crates/crowbar) crate making
//! it possible to write type safe AWS Lambda functions in Rust that are invoked
//! by [API gateway](https://aws.amazon.com/api-gateway/) events.
//!
//! It exports native Rust functions as CPython modules making it possible to embed
//! handlers within aws's python3.6 runtime.
//!
//! # Usage
//!
//! Add both `gateway` and `cpython `to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! gateway = "0.1"
//! cpython = "0.1"
//! ```
//!
//! Use macros from both crates:
//!
//! ```rust,ignore
//! #[macro_use(gateway)]
//! extern crate gateway;
//! // the following imports macros needed by the gateway macro
//! #[macro_use]
//! extern crate cpython;
//! ```
//!
//! And write your function using the [gateway!](macro.gateway.html) macro:
//!
//! ```rust
//! # #[macro_use(gateway)] extern crate gateway;
//! # #[macro_use] extern crate cpython;
//! # fn main() {
//! gateway!(|_request, context| {
//!     println!("hi cloudwatch logs, this is {}", context.function_name());
//!     // return a basic 200 response
//!     Ok(gateway::Response::default())
//! });
//! # }
//! ```
//!
//! # Packaging functions
//!
//! For your code to be usable in AWS Lambda's Python3.6 execution environment,
//! you need to compile to
//! a dynamic library with the necessary functions for CPython to run. The
//! `gateway!` macro does
//! most of this for you, but cargo still needs to know what to do.
//!
//! You can configure cargo to build a dynamic library with the following.
//! If you're using the
//! `gateway!` macro as above, you need to use `lambda` for the library name
//! (see the documentation
//! for [gateway!](macro.gateway.html) if you want to use something else).
//!
//! ```toml
//! [lib]
//! name = "lambda"
//! crate-type = ["cdylib"]
//! ```
//!
//! > Note: cdylib exports C interface from a Rust dynamic library.
//!
//! > Link formats are described [here](https://doc.rust-lang.org/reference/linkage.html)
//!
//! `cargo build` will now build a `liblambda.so`. Put this in a zip file and
//! upload it to an AWS Lambda function. Use the Python 3.6 execution environment with the handler
//! configured as `liblambda.handler`.
//!
//! Because you're building a dynamic library, other libraries that you're dynamically linking
//! against need to also be in the Lambda execution environment. The easiest way to do this is
//! building in an environment similar to Lambda's, such as Amazon Linux. You can use an [EC2
//! instance](https://aws.amazon.com/amazon-linux-ami/) or a [Docker
//! container](https://hub.docker.com/r/lambci/lambda).
//!

extern crate crowbar;
extern crate cpython;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

#[doc(hidden)]
pub use cpython::{PyObject, PyResult};
use cpython::Python;
pub use crowbar::LambdaContext;

pub mod response;
pub mod request;

pub use response::Response;
pub use request::Request;

/// Result type for API Gateway requests
pub type GatewayResult = Result<Response, Box<std::error::Error>>;

// wrap crowbar handler in gateway handler
#[doc(hidden)]
pub fn handler<F>(py: Python, f: F, py_event: PyObject, py_context: PyObject) -> PyResult<PyObject>
where
    F: FnOnce(Request, LambdaContext) -> GatewayResult,
{
    crowbar::handler(
        py,
        |event, ctx| f(serde_json::from_value::<Request>(event)?, ctx),
        py_event,
        py_context,
    )
}

#[macro_export]
/// Macro to wrap a Lambda function handler for api gateway events.
///
/// Lambda functions accept two arguments (the event, a `gateway::Request`, and the context, a
/// `LambdaContext`) and returns a value (a serde_json `Value`). The function signature should look
/// like:
///
/// ```rust,ignore
/// fn handler(event: Request, context: LambdaContext) -> GatewayResult
/// ```
///
/// To use this macro, you need to `macro_use` both crowbar *and* cpython, because crowbar
/// references multiple cpython macros.
///
/// ```rust,ignore
/// #[macro_use(gateway)]
/// extern crate gateway;
/// #[macro_use]
/// extern crate cpython;
/// ```
///
/// # Examples
///
/// You can wrap a closure with `gateway!`:
///
/// ```rust
/// # #[macro_use(gateway)] extern crate gateway;
/// # #[macro_use] extern crate cpython;
/// # fn main() {
/// gateway!(|request, context| {
///     println!("{:?}", request);
///     Ok(gateway::Response::default())
/// });
/// # }
/// ```
///
/// You can also define a named function:
///
/// ```rust
/// # #[macro_use(gateway)] extern crate gateway;
/// # #[macro_use] extern crate cpython;
/// # fn main() {
/// use gateway::{Request, Response, LambdaContext, GatewayResult};
///
/// fn my_handler(request: Request, context: LambdaContext) -> GatewayResult {
///     println!("{:?}", request);
///     Ok(Response::builder().body(":thumbsup:").build())
/// }
///
/// gateway!(my_handler);
/// # }
/// ```
///
/// # Multiple handlers
///
/// You can define multiple handlers in the same module in a way similar to `match`:
///
/// ```rust
/// # #[macro_use(gateway)] extern crate gateway;
/// # #[macro_use] extern crate cpython;
/// # fn main() {
/// gateway! {
///     "one" => |request, context| { Ok(gateway::Response::builder().body("1").build()) },
///     "two" => |request, context| { Ok(gateway::Response::builder().body("2").build()) },
/// };
/// # }
/// ```
///
/// # Changing the dynamic library name
///
/// If you need to change the name of the built dynamic library, you first need to change the
/// `[lib]` section in Cargo.toml:
///
/// ```toml
/// [lib]
/// name = "kappa"
/// crate-type = ["cdylib"]
/// ```
///
/// You then also need to change the names of the library symbols, which you can do by extending
/// upon the multiple handler version of `gateway!`:
///
/// ```rust
/// # #[macro_use(gateway)] extern crate gateway;
/// # #[macro_use] extern crate cpython;
/// # fn main() {
/// gateway! {
///     crate (libkappa, initlibkappa, PyInit_libkappa) {
///         "handler" => |request, context| {
///            Ok(gateway::Response::builder().body(
///               "hi from libkappa"
///            ).build())
///         }
///     }
/// };
/// # }
/// ```
macro_rules! gateway {
    (@module ($module:ident, $py2:ident, $py3:ident)
     @handlers ($($handler:expr => $target:expr),*)) => {
        py_module_initializer!($module, $py2, $py3, |py, m| {
            $(
                m.add(py, $handler, py_fn!(
                    py,
                    x(
                        event: $crate::PyObject,
                        context: $crate::PyObject
                    ) -> $crate::PyResult<$crate::PyObject> {
                        $crate::handler(py, $target, event, context)
                    }
                ))?;
            )*
            Ok(())
        });
    };

    (crate $module:tt { $($handler:expr => $target:expr),* }) => {
        gateway! { @module $module @handlers ($($handler => $target),*) }
    };

    (crate $module:tt { $($handler:expr => $target:expr,)* }) => {
        gateway! { @module $module @handlers ($($handler => $target),*) }
    };

    ($($handler:expr => $target:expr),*) => {
        gateway! { @module (liblambda, initliblambda, PyInit_liblambda)
                  @handlers ($($handler => $target),*) }
    };

    ($($handler:expr => $target:expr,)*) => {
        gateway! { $($handler => $target),* }
    };

    ($f:expr) => {
        gateway! { "handler" => $f, }
    };
}