diff --git a/.gitignore b/.gitignore index 911cf87c..94f3d847 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target /.cargo lambda-runtime/libtest.rmeta +Cargo.lock # Built AWS Lambda zipfile lambda.zip diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 7a444286..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,733 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "async-stream" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3670df70cbc01729f901f94c887814b3c68db038aad1329a418bae178bc5295c" -dependencies = [ - "async-stream-impl", - "futures-core", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3548b8efc9f8e8a5a0a2808c5bd8451a9031b9e5b879a79590304ae928b0a70" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - -[[package]] -name = "bytes" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "dtoa" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" - -[[package]] -name = "futures-executor" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" - -[[package]] -name = "futures-macro" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" - -[[package]] -name = "futures-task" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] - -[[package]] -name = "futures-util" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "proc-macro-hack", - "proc-macro-nested", - "slab", -] - -[[package]] -name = "hermit-abi" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" -dependencies = [ - "libc", -] - -[[package]] -name = "http" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "httparse" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" - -[[package]] -name = "httpdate" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" - -[[package]] -name = "hyper" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12219dc884514cb4a6a03737f4413c0e01c23a1b059b0156004b23f1e19dccbe" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project 1.0.4", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "idna" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "itoa" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" - -[[package]] -name = "lambda" -version = "0.1.0" -dependencies = [ - "async-stream", - "bytes", - "futures", - "http", - "hyper", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-error", - "tracing-futures", -] - -[[package]] -name = "lambda_http" -version = "0.2.0-beta.1" -dependencies = [ - "base64", - "http", - "lambda", - "log", - "maplit", - "serde", - "serde_derive", - "serde_json", - "serde_urlencoded", - "tokio", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff" - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "matches" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" - -[[package]] -name = "memchr" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" - -[[package]] -name = "mio" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" -dependencies = [ - "socket2", - "winapi", -] - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - -[[package]] -name = "num_cpus" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "once_cell" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pin-project" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" -dependencies = [ - "pin-project-internal 0.4.27", -] - -[[package]] -name = "pin-project" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b70b68509f17aa2857863b6fa00bf21fc93674c7a8893de2f469f6aa7ca2f2" -dependencies = [ - "pin-project-internal 1.0.4", -] - -[[package]] -name = "pin-project-internal" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa25a6393f22ce819b0f50e0be89287292fda8d425be38ee0ca14c4931d9e71" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - -[[package]] -name = "proc-macro2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" - -[[package]] -name = "serde" -version = "1.0.123" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.123" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" -dependencies = [ - "dtoa", - "itoa", - "serde", - "url", -] - -[[package]] -name = "sharded-slab" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "slab" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" - -[[package]] -name = "socket2" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - -[[package]] -name = "syn" -version = "1.0.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "thread_local" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8208a331e1cb318dd5bd76951d2b8fc48ca38a69f5f4e4af1b6a9f8c6236915" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tinyvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "tokio" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8efab2086f17abcddb8f756117665c958feee6b2e39974c2f1600592ab3a4195" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42517d2975ca3114b22a16192634e8241dc5cc1f130be194645970cc1c371494" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tower-service" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" - -[[package]] -name = "tracing" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "tracing-error" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-futures" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" -dependencies = [ - "pin-project 0.4.27", - "tracing", -] - -[[package]] -name = "tracing-subscriber" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8f0c8f4c594e4fc9debc1990deab13238077271ba84dd853d54902ee3401" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] - -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "unicode-bidi" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -dependencies = [ - "matches", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-xid" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" - -[[package]] -name = "url" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" -dependencies = [ - "form_urlencoded", - "idna", - "matches", - "percent-encoding", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index 4abbc02c..575c2d3e 100644 --- a/lambda-http/Cargo.toml +++ b/lambda-http/Cargo.toml @@ -19,8 +19,7 @@ maintenance = { status = "actively-developed" } base64 = "0.12" http = "0.2" lambda = { path = "../lambda", version = "0.1" } -serde = "^1" -serde_derive = "^1" +serde = { version = "^1", features = ["derive"] } serde_json = "^1" serde_urlencoded = "0.6" diff --git a/lambda-http/src/ext.rs b/lambda-http/src/ext.rs index 81db5ed7..13740f49 100644 --- a/lambda-http/src/ext.rs +++ b/lambda-http/src/ext.rs @@ -1,10 +1,9 @@ //! Extension methods for `http::Request` types +use crate::{request::RequestContext, strmap::StrMap, Body}; use serde::{de::value::Error as SerdeError, Deserialize}; use std::{error::Error, fmt}; -use crate::{request::RequestContext, strmap::StrMap, Body}; - /// ALB/API gateway pre-parsed http query string parameters pub(crate) struct QueryStringParameters(pub(crate) StrMap); @@ -68,7 +67,7 @@ impl Error for PayloadError { /// /// ```rust,no_run /// use lambda_http::{handler, lambda::{self, Context}, Body, IntoResponse, Request, Response, RequestExt}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// /// type Error = Box; /// @@ -258,7 +257,7 @@ impl RequestExt for http::Request { #[cfg(test)] mod tests { use crate::{Body, Request, RequestExt}; - use serde_derive::Deserialize; + use serde::Deserialize; #[test] fn requests_can_mock_query_string_parameters_ext() { diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index 80796e10..7ee4927e 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -97,9 +97,9 @@ pub trait Handler: Sized { /// The type of Response this Handler will return type Response: IntoResponse; /// The type of Future this Handler will return - type Fut: Future> + 'static; + type Fut: Future> + Send + Sync + 'static; /// Function used to execute handler behavior - fn call(&mut self, event: Request, context: Context) -> Self::Fut; + fn call(&self, event: Request, context: Context) -> Self::Fut; } /// Adapts a [`Handler`](trait.Handler.html) to the `lambda::run` interface @@ -110,22 +110,22 @@ pub fn handler(handler: H) -> Adapter { /// An implementation of `Handler` for a given closure return a `Future` representing the computed response impl Handler for F where - F: FnMut(Request, Context) -> Fut, + F: Fn(Request, Context) -> Fut, R: IntoResponse, - Fut: Future> + Send + 'static, + Fut: Future> + Send + Sync + 'static, { type Response = R; type Error = Error; type Fut = Fut; - fn call(&mut self, event: Request, context: Context) -> Self::Fut { - (*self)(event, context) + fn call(&self, event: Request, context: Context) -> Self::Fut { + (self)(event, context) } } #[doc(hidden)] pub struct TransformResponse { is_alb: bool, - fut: Pin>>>, + fut: Pin> + Send + Sync>>, } impl Future for TransformResponse @@ -158,7 +158,7 @@ impl Handler for Adapter { type Response = H::Response; type Error = H::Error; type Fut = H::Fut; - fn call(&mut self, event: Request, context: Context) -> Self::Fut { + fn call(&self, event: Request, context: Context) -> Self::Fut { self.handler.call(event, context) } } @@ -166,7 +166,7 @@ impl Handler for Adapter { impl LambdaHandler, LambdaResponse> for Adapter { type Error = H::Error; type Fut = TransformResponse; - fn call(&mut self, event: LambdaRequest<'_>, context: Context) -> Self::Fut { + fn call(&self, event: LambdaRequest<'_>, context: Context) -> Self::Fut { let is_alb = event.is_alb(); let fut = Box::pin(self.handler.call(event.into(), context)); TransformResponse { is_alb, fut } diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs index 347cb29e..3444209f 100644 --- a/lambda-http/src/request.rs +++ b/lambda-http/src/request.rs @@ -3,16 +3,17 @@ //! Typically these are exposed via the `request_context` //! request extension method provided by [lambda_http::RequestExt](../trait.RequestExt.html) //! -use serde::de::{Deserialize, Deserializer, Error as DeError, MapAccess, Visitor}; -use serde_derive::Deserialize; -use serde_json::{error::Error as JsonError, Value}; -use std::{borrow::Cow, collections::HashMap, fmt, io::Read, mem}; - use crate::{ body::Body, ext::{PathParameters, QueryStringParameters, StageVariables}, strmap::StrMap, }; +use serde::{ + de::{Deserializer, Error as DeError, MapAccess, Visitor}, + Deserialize, +}; +use serde_json::{error::Error as JsonError, Value}; +use std::{borrow::Cow, collections::HashMap, fmt, io::Read, mem}; /// Internal representation of an Lambda http event from /// ALB, API Gateway REST and HTTP API proxy event perspectives @@ -101,42 +102,65 @@ impl LambdaRequest<'_> { } } +/// See [context-variable-reference](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html) for more detail. #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2RequestContext { + /// The API owner's AWS account ID. pub account_id: String, + /// The identifier API Gateway assigns to your API. pub api_id: String, + /// The stringified value of the specified key-value pair of the context map returned from an API Gateway Lambda authorizer function. #[serde(default)] pub authorizer: HashMap, + /// The full domain name used to invoke the API. This should be the same as the incoming Host header. pub domain_name: String, + /// The first label of the $context.domainName. This is often used as a caller/customer identifier. pub domain_prefix: String, + /// The HTTP method used. pub http: Http, + /// The ID that API Gateway assigns to the API request. pub request_id: String, + /// Undocumented, could be resourcePath pub route_key: String, + /// The deployment stage of the API request (for example, Beta or Prod). pub stage: String, + /// Undocumented, could be requestTime pub time: String, + /// Undocumented, could be requestTimeEpoch pub time_epoch: usize, } +/// See [context-variable-reference](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html) for more detail. #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayRequestContext { - //pub path: String, + /// The API owner's AWS account ID. pub account_id: String, + /// The identifier that API Gateway assigns to your resource. pub resource_id: String, + /// The deployment stage of the API request (for example, Beta or Prod). pub stage: String, + /// The ID that API Gateway assigns to the API request. pub request_id: String, + /// The path to your resource. For example, for the non-proxy request URI of `https://{rest-api-id.execute-api.{region}.amazonaws.com/{stage}/root/child`, The $context.resourcePath value is /root/child. pub resource_path: String, + /// The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. pub http_method: String, + /// The stringified value of the specified key-value pair of the context map returned from an API Gateway Lambda authorizer function. #[serde(default)] pub authorizer: HashMap, + /// The identifier API Gateway assigns to your API. pub api_id: String, + /// Cofnito identity information pub identity: Identity, } +/// Elastic load balancer context information #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct AlbRequestContext { + /// Elastic load balancer context information pub elb: Elb, } @@ -166,10 +190,17 @@ pub struct Elb { #[serde(rename_all = "camelCase")] pub struct Http { #[serde(deserialize_with = "deserialize_method")] + /// The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. pub method: http::Method, + /// The request path. For example, for a non-proxy request URL of + /// `https://{rest-api-id.execute-api.{region}.amazonaws.com/{stage}/root/child`, + /// the $context.path value is `/{stage}/root/child`. pub path: String, + /// The request protocol, for example, HTTP/1.1. pub protocol: String, + /// The source IP address of the TCP connection making the request to API Gateway. pub source_ip: String, + /// The User-Agent header of the API caller. pub user_agent: String, } @@ -177,17 +208,35 @@ pub struct Http { #[derive(Deserialize, Debug, Default, Clone)] #[serde(rename_all = "camelCase")] pub struct Identity { + /// The source IP address of the TCP connection making the request to API Gateway. pub source_ip: String, + /// The Amazon Cognito identity ID of the caller making the request. + /// Available only if the request was signed with Amazon Cognito credentials. pub cognito_identity_id: Option, + /// The Amazon Cognito identity pool ID of the caller making the request. + /// Available only if the request was signed with Amazon Cognito credentials. pub cognito_identity_pool_id: Option, + /// A comma-separated list of the Amazon Cognito authentication providers used by the caller making the request. + /// Available only if the request was signed with Amazon Cognito credentials. pub cognito_authentication_provider: Option, + /// The Amazon Cognito authentication type of the caller making the request. + /// Available only if the request was signed with Amazon Cognito credentials. pub cognito_authentication_type: Option, + /// The AWS account ID associated with the request. pub account_id: Option, + /// The principal identifier of the caller making the request. pub caller: Option, + /// For API methods that require an API key, this variable is the API key associated with the method request. + /// For methods that don't require an API key, this variable is null. pub api_key: Option, + /// Undocumented. Can be the API key ID associated with an API request that requires an API key. + /// The description of `api_key` and `access_key` may actually be reversed. pub access_key: Option, + /// The principal identifier of the user making the request. Used in Lambda authorizers. pub user: Option, + /// The User-Agent header of the API caller. pub user_agent: Option, + /// The Amazon Resource Name (ARN) of the effective user identified after authentication. pub user_arn: Option, } @@ -358,7 +407,7 @@ impl<'a> From> for http::Request { .expect("failed to build request"); // no builder method that sets headers in batch - mem::replace(req.headers_mut(), headers); + let _ = mem::replace(req.headers_mut(), headers); req } @@ -422,7 +471,7 @@ impl<'a> From> for http::Request { } // no builder method that sets headers in batch - mem::replace(req.headers_mut(), multi_value_headers); + let _ = mem::replace(req.headers_mut(), multi_value_headers); req } @@ -483,7 +532,7 @@ impl<'a> From> for http::Request { } // no builder method that sets headers in batch - mem::replace(req.headers_mut(), multi_value_headers); + let _ = mem::replace(req.headers_mut(), multi_value_headers); req } diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 61406426..bdeee5c1 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -1,16 +1,14 @@ //! Response types +use crate::body::Body; use http::{ header::{HeaderMap, HeaderValue, CONTENT_TYPE}, Response, }; use serde::{ ser::{Error as SerError, SerializeMap}, - Serializer, + Serialize, Serializer, }; -use serde_derive::Serialize; - -use crate::body::Body; /// Representation of API Gateway response #[doc(hidden)] diff --git a/lambda-http/src/strmap.rs b/lambda-http/src/strmap.rs index 10801733..281a1101 100644 --- a/lambda-http/src/strmap.rs +++ b/lambda-http/src/strmap.rs @@ -1,14 +1,13 @@ +use serde::{ + de::{MapAccess, Visitor}, + Deserialize, Deserializer, +}; use std::{ collections::{hash_map::Keys, HashMap}, fmt, sync::Arc, }; -use serde::{ - de::{MapAccess, Visitor}, - Deserialize, Deserializer, -}; - /// A read-only view into a map of string data which may contain multiple values /// /// Internally data is always represented as many valued @@ -76,7 +75,7 @@ impl<'a> Iterator for StrMapIter<'a> { /// internal type used when deserializing StrMaps from /// potentially one or many valued maps -#[derive(serde_derive::Deserialize)] +#[derive(Deserialize)] #[serde(untagged)] enum OneOrMany { One(String), diff --git a/lambda/Cargo.toml b/lambda/Cargo.toml index b4d24894..37a30f26 100644 --- a/lambda/Cargo.toml +++ b/lambda/Cargo.toml @@ -2,9 +2,11 @@ name = "lambda" version = "0.1.0" authors = ["David Barsky "] -description = "AWS Lambda Runtime." +description = "AWS Lambda Runtime" edition = "2018" -license = "Apache-2.0" +license = "Apache License 2.0" +repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +readme = "../README.md" [features] default = ["simulated"] @@ -19,6 +21,15 @@ bytes = "1.0" http = "0.2" async-stream = "0.3" futures = "0.3" -tracing = "0.1.13" -tracing-futures = "0.2.3" tracing-error = "0.1.2" +tracing = { version = "0.1", features = ["log"] } +tracing-futures = "0.2" +tower-service = "0.3" +tokio-stream = "0.1.2" + +[dev-dependencies] +tracing-subscriber = "0.2" +once_cell = "1.4.0" +simple_logger = "1.6.0" +log = "0.4" +simple-error = "0.2" diff --git a/lambda/examples/README.md b/lambda/examples/README.md new file mode 100644 index 00000000..51a5a10b --- /dev/null +++ b/lambda/examples/README.md @@ -0,0 +1,254 @@ + +## How to compile and run the examples + +1. Create a Lambda function called _RuntimeTest_ in AWS with a custom runtime and no code. + +2. Compile all examples + +``` +cargo build --release --target x86_64-unknown-linux-musl --examples +``` +3. Prepare the package for the example you want to test, e.g. +``` +cp ./target/x86_64-unknown-linux-musl/release/examples/hello ./bootstrap && zip lambda.zip bootstrap && rm bootstrap +``` +4. Upload the package to AWS Lambda +``` +aws lambda update-function-code --region us-east-1 --function-name RuntimeTest --zip-file fileb://lambda.zip +``` +_Feel free to replace the names and IDs with your own values._ + +## basic.rs + +**Deployment**: +```bash +cp ./target/x86_64-unknown-linux-musl/release/examples/basic ./bootstrap && zip lambda.zip bootstrap && rm bootstrap +aws lambda update-function-code --region us-east-1 --function-name RuntimeTest --zip-file fileb://lambda.zip +``` + +**Test event JSON (success)**: +```json +{ "command": "do something" } +``` + +Sample response: +```json +{ + "msg": "Command do something executed.", + "req_id": "67a038e4-dc19-4adf-aa32-5ba09312c6ca" +} +``` + +**Test event JSON (error)**: +```json +{ "foo": "do something" } +``` + +Sample response: +```json +{ + "errorType": "Runtime.ExitError", + "errorMessage": "RequestId: 586366df-f271-4e6e-9c30-b3dcab30f4e8 Error: Runtime exited with error: exit status 1" +} +``` +The runtime could not deserialize our invalid input, but it did not give a detailed explanation why the error occurred in the response. More details appear in the CloudWatch log: +``` +START RequestId: 6e667f61-c5d4-4b07-a60f-cd1ab339c35f Version: $LATEST +Error: Error("missing field `command`", line: 1, column: 22) +END RequestId: 6e667f61-c5d4-4b07-a60f-cd1ab339c35f +REPORT RequestId: 6e667f61-c5d4-4b07-a60f-cd1ab339c35f Duration: 36.34 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 10 MB +RequestId: 6e667f61-c5d4-4b07-a60f-cd1ab339c35f Error: Runtime exited with error: exit status 1 +Runtime.ExitError +``` + + See _error-handling.rs_ example for more error handling options. + +## macro.rs + +The most basic example using `#[lambda]` macro to reduce the amount of boilerplate code. + +**Deployment**: +```bash +cp ./target/x86_64-unknown-linux-musl/release/examples/macro ./bootstrap && zip lambda.zip bootstrap && rm bootstrap +aws lambda update-function-code --region us-east-1 --function-name RuntimeTest --zip-file fileb://lambda.zip +``` + +**Test event JSON**: +```json +{ "foo": "bar" } +``` + +Sample response: +```json +{ + "foo": "bar" +} +``` + +## error-handling.rs + +Errors are logged by the runtime only if `log` is initialized by the handler. +These examples use `simple_logger`, but you can use any other provider that works with `log`. +``` +simple_logger::init_with_level(log::Level::Debug)?; +``` + +**Deployment**: +```bash +cp ./target/x86_64-unknown-linux-musl/release/examples/error-handling ./bootstrap && zip lambda.zip bootstrap && rm bootstrap +aws lambda update-function-code --region us-east-1 --function-name RuntimeTest --zip-file fileb://lambda.zip +``` + +The following input/output examples correspond to different `match` arms in the handler of `error-handling.rs`. + +#### Invalid event JSON + +Test input: +```json +{ + "event_type": "WrongType" +} +``` + +Lambda output: +``` +{ + "errorType": "alloc::boxed::Box", + "errorMessage": "unknown variant `WrongType`, expected one of `Response`, `ExternalError`, `SimpleError`, `CustomError`" +} +``` + +CloudWatch records: +``` +START RequestId: b98e07c6-e2ba-4ca6-9968-d0b94729ddba Version: $LATEST +2020-07-21 04:28:52,630 ERROR [lambda] unknown variant `WrongType`, expected one of `Response`, `ExternalError`, `SimpleError`, `CustomError` +END RequestId: b98e07c6-e2ba-4ca6-9968-d0b94729ddba +REPORT RequestId: b98e07c6-e2ba-4ca6-9968-d0b94729ddba Duration: 2.06 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 28 MB Init Duration: 33.67 ms +``` + +#### A simple text-only error + +Test event JSON: +```json +{ + "event_type": "SimpleError" +} +``` + +Lambda output: +``` +{ + "errorType": "alloc::boxed::Box", + "errorMessage": "A simple error as requested!" +} +``` + +CloudWatch records: +``` +START RequestId: 77c66dbf-bd60-4f77-8453-682d0bceba91 Version: $LATEST +2020-07-21 04:35:28,044 ERROR [lambda] A simple error as requested! +END RequestId: 77c66dbf-bd60-4f77-8453-682d0bceba91 +REPORT RequestId: 77c66dbf-bd60-4f77-8453-682d0bceba91 Duration: 0.98 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 28 MB +``` + +#### A custom error with JSON output for Display trait + +Test event JSON: +```json +{ + "event_type": "CustomError" +} +``` + +Lambda output: +``` +{ + "errorType": "alloc::boxed::Box", + "errorMessage": "{\"is_authenticated\":false,\"msg\":\"A custom error as requested!\",\"req_id\":\"b46b0588-1383-4224-bc7a-42b0d61930c1\"}" +} +``` + +CloudWatch records: +``` +START RequestId: b46b0588-1383-4224-bc7a-42b0d61930c1 Version: $LATEST +2020-07-21 04:39:00,133 ERROR [lambda] {"is_authenticated":false,"msg":"A custom error as requested!","req_id":"b46b0588-1383-4224-bc7a-42b0d61930c1"} +END RequestId: b46b0588-1383-4224-bc7a-42b0d61930c1 +REPORT RequestId: b46b0588-1383-4224-bc7a-42b0d61930c1 Duration: 0.91 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 29 MB +``` + +#### A 3rd party error from _std::fs::File::open_ + +Test event JSON: +```json +{ + "event_type": "ExternalError" +} +``` + +Lambda output: +``` +{ + "errorType": "alloc::boxed::Box", + "errorMessage": "No such file or directory (os error 2)" +} +``` + +CloudWatch records: +``` +START RequestId: 893d24e5-cb79-4f6f-bae0-36304c62e9da Version: $LATEST +2020-07-21 04:43:56,254 ERROR [lambda] No such file or directory (os error 2) +END RequestId: 893d24e5-cb79-4f6f-bae0-36304c62e9da +REPORT RequestId: 893d24e5-cb79-4f6f-bae0-36304c62e9da Duration: 1.15 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 29 MB +``` + +#### Handler panic + +Test event JSON: +```json +{ + "event_type": "Panic" +} +``` + +Lambda output: +``` +{ + "errorType": "Runtime.ExitError", + "errorMessage": "RequestId: 2d579019-07f7-409a-a6e6-af7725253307 Error: Runtime exited with error: exit status 101" +} +``` + +CloudWatch records: +``` +START RequestId: 2d579019-07f7-409a-a6e6-af7725253307 Version: $LATEST +thread 'main' panicked at 'explicit panic', lambda/examples/error-handling.rs:87:13 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +END RequestId: 2d579019-07f7-409a-a6e6-af7725253307 +REPORT RequestId: 2d579019-07f7-409a-a6e6-af7725253307 Duration: 43.40 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 27 MB Init Duration: 23.15 ms +RequestId: 2d579019-07f7-409a-a6e6-af7725253307 Error: Runtime exited with error: exit status 101 +Runtime.ExitError +``` + +#### A response to a successful Lambda execution + +Test event JSON: +```json +{ + "event_type": "Response" +} +``` + +Lambda output: +``` +{ + "msg": "OK", + "req_id": "9752a3ad-6566-44e4-aafd-74db1fd4f361" +} +``` + +CloudWatch records: +``` +START RequestId: 9752a3ad-6566-44e4-aafd-74db1fd4f361 Version: $LATEST +END RequestId: 9752a3ad-6566-44e4-aafd-74db1fd4f361 +REPORT RequestId: 9752a3ad-6566-44e4-aafd-74db1fd4f361 Duration: 0.89 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 29 MB +``` \ No newline at end of file diff --git a/lambda/examples/basic.rs b/lambda/examples/basic.rs new file mode 100644 index 00000000..7b492a44 --- /dev/null +++ b/lambda/examples/basic.rs @@ -0,0 +1,54 @@ +// This example requires the following input to succeed: +// { "command": "do something" } + +use lambda::{handler_fn, Context}; +use log::LevelFilter; +use serde::{Deserialize, Serialize}; +use simple_logger::SimpleLogger; + +/// A shorthand for `Box` +/// type required by aws-lambda-rust-runtime. +pub type Error = Box; + +/// This is also a made-up example. Requests come into the runtime as unicode +/// strings in json format, which can map to any structure that implements `serde::Deserialize` +/// The runtime pays no attention to the contents of the request payload. +#[derive(Deserialize)] +struct Request { + command: String, +} + +/// This is a made-up example of what a response structure may look like. +/// There is no restriction on what it can be. The runtime requires responses +/// to be serialized into json. The runtime pays no attention +/// to the contents of the response payload. +#[derive(Serialize)] +struct Response { + req_id: String, + msg: String, +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + // can be replaced with any other method of initializing `log` + SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); + + let func = handler_fn(my_handler); + lambda::run(func).await?; + Ok(()) +} + +pub(crate) async fn my_handler(event: Request, ctx: Context) -> Result { + // extract some useful info from the request + let command = event.command; + + // prepare the response + let resp = Response { + req_id: ctx.request_id, + msg: format!("Command {} executed.", command), + }; + + // return `Response` (it will be serialized to JSON automatically by the runtime) + Ok(resp) +} diff --git a/lambda/examples/error-handling.rs b/lambda/examples/error-handling.rs new file mode 100644 index 00000000..1b061254 --- /dev/null +++ b/lambda/examples/error-handling.rs @@ -0,0 +1,117 @@ +/// See https://github.com/awslabs/aws-lambda-rust-runtime for more info on Rust runtime for AWS Lambda +use lambda::handler_fn; +use log::LevelFilter; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use simple_logger::SimpleLogger; +use std::fs::File; + +/// A shorthand for `Box` type required by aws-lambda-rust-runtime. +pub type Error = Box; + +/// A simple Lambda request structure with just one field +/// that tells the Lambda what is expected of it. +#[derive(Deserialize)] +struct Request { + event_type: EventType, +} + +/// Event types that tell our Lambda what to do do. +#[derive(Deserialize, PartialEq)] +enum EventType { + Response, + ExternalError, + SimpleError, + CustomError, + Panic, +} + +/// A simple Lambda response structure. +#[derive(Serialize)] +struct Response { + req_id: String, + msg: String, +} + +#[derive(Debug, Serialize)] +struct CustomError { + is_authenticated: bool, + req_id: String, + msg: String, +} + +impl std::error::Error for CustomError { + // this implementation required `Debug` and `Display` traits +} + +impl std::fmt::Display for CustomError { + /// Display the error struct as a JSON string + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let err_as_json = json!(self).to_string(); + write!(f, "{}", err_as_json) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // The runtime logging can be enabled here by initializing `log` with `simple_logger` + // or another compatible crate. The runtime is using `tracing` internally. + // You can comment out the `simple_logger` init line and uncomment the following block to + // use `tracing` in the handler function. + // + SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); + /* + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + // this needs to be set to false, otherwise ANSI color codes will + // show up in a confusing manner in CloudWatch logs. + .with_ansi(false) + // disabling time is handy because CloudWatch will add the ingestion time. + .without_time() + .init(); + */ + + // call the actual handler of the request + let func = handler_fn(func); + lambda::run(func).await?; + Ok(()) +} + +/// The actual handler of the Lambda request. +pub(crate) async fn func(event: Value, ctx: lambda::Context) -> Result { + // check what action was requested + match serde_json::from_value::(event)?.event_type { + EventType::SimpleError => { + // generate a simple text message error using `simple_error` crate + return Err(Box::new(simple_error::SimpleError::new("A simple error as requested!"))); + } + EventType::CustomError => { + // generate a custom error using our own structure + let cust_err = CustomError { + is_authenticated: ctx.identity.is_some(), + req_id: ctx.request_id, + msg: "A custom error as requested!".into(), + }; + return Err(Box::new(cust_err)); + } + EventType::ExternalError => { + // try to open a non-existent file to get an error and propagate it with `?` + let _file = File::open("non-existent-file.txt")?; + + // it should never execute past the above line + unreachable!(); + } + EventType::Panic => { + panic!(); + } + EventType::Response => { + // generate and return an OK response in JSON format + let resp = Response { + req_id: ctx.request_id, + msg: "OK".into(), + }; + + return Ok(json!(resp)); + } + } +} diff --git a/lambda/examples/hello.rs b/lambda/examples/hello.rs deleted file mode 100644 index d94ee322..00000000 --- a/lambda/examples/hello.rs +++ /dev/null @@ -1,17 +0,0 @@ -use lambda::{handler_fn, Context}; -use serde_json::{json, Value}; - -type Error = Box; - -#[tokio::main] -async fn main() -> Result<(), Error> { - let func = handler_fn(func); - lambda::run(func).await?; - Ok(()) -} - -async fn func(event: Value, _: Context) -> Result { - let first_name = event["firstName"].as_str().unwrap_or("world"); - - Ok(json!({ "message": format!("Hello, {}!", first_name) })) -} diff --git a/lambda/src/client.rs b/lambda/src/client.rs index e1f667dd..b24b2e78 100644 --- a/lambda/src/client.rs +++ b/lambda/src/client.rs @@ -1,77 +1,20 @@ -use crate::{ - requests::{IntoResponse, NextEventResponse}, - Error, -}; -use http::{ - uri::{PathAndQuery, Scheme}, - HeaderValue, Method, Request, Response, StatusCode, Uri, -}; -use hyper::{client::HttpConnector, server::conn::Http, service::service_fn, Body}; -use serde_json::json; -use std::convert::TryFrom; -use tokio::{ - io::{AsyncRead, AsyncWrite}, - select, - sync::oneshot, -}; -use tracing::{error, info, instrument}; - -#[instrument] -async fn hello(req: Request) -> Result, Error> { - Ok(Response::new(Body::from("hello"))) -} - -async fn handle_incoming(req: Request) -> Result, Error> { - let path: Vec<&str> = req - .uri() - .path_and_query() - .unwrap() - .as_str() - .split("/") - .collect::>(); - match &path[1..] { - ["2018-06-01", "runtime", "invocation", "next"] => next_event(&req).await, - ["2018-06-01", "runtime", "invocation", id, "response"] => complete_event(&req, id).await, - ["2018-06-01", "runtime", "invocation", id, "error"] => event_err(&req, id).await, - ["2018-06-01", "runtime", "init", "error"] => unimplemented!(), - _ => unimplemented!(), - } -} - -#[instrument(skip(io, rx))] -async fn handle(io: I, rx: oneshot::Receiver<()>) -> Result<(), hyper::Error> -where - I: AsyncRead + AsyncWrite + Unpin + 'static, -{ - let conn = Http::new().serve_connection(io, service_fn(handle_incoming)); - select! { - _ = rx => { - info!("Received cancelation signal"); - return Ok(()) - } - res = conn => { - match res { - Ok(()) => return Ok(()), - Err(e) => { - error!(message = "Got error serving connection", e = %e); - return Err(e); - } - } - } - } -} +use crate::Error; +use http::{uri::Scheme, Request, Response, Uri}; +use hyper::{client::HttpConnector, Body}; +use std::fmt::Debug; #[derive(Debug)] pub(crate) struct Client { - base: Uri, - client: hyper::Client, + pub(crate) base: Uri, + pub(crate) client: hyper::Client, } impl Client where C: hyper::client::connect::Connect + Sync + Send + Clone + 'static, { - pub fn with(base: Uri, client: hyper::Client) -> Self { + pub fn with(base: Uri, connector: C) -> Self { + let client = hyper::Client::builder().build(connector); Self { base, client } } @@ -98,104 +41,134 @@ where pub(crate) async fn call(&self, req: Request) -> Result, Error> { let req = self.set_origin(req)?; let (parts, body) = req.into_parts(); - let body = Body::from(body); let req = Request::from_parts(parts, body); let response = self.client.request(req).await?; Ok(response) } } -async fn next_event(req: &Request) -> Result, Error> { - let path = "/2018-06-01/runtime/invocation/next"; - assert_eq!(req.method(), Method::GET); - assert_eq!(req.uri().path_and_query().unwrap(), &PathAndQuery::from_static(path)); - let body = json!({"message": "hello"}); - - let rsp = NextEventResponse { - request_id: "8476a536-e9f4-11e8-9739-2dfe598c3fcd", - deadline: 1_542_409_706_888, - arn: "arn:aws:lambda:us-east-2:123456789012:function:custom-runtime", - trace_id: "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419", - body: serde_json::to_vec(&body)?, +#[cfg(test)] +mod endpoint_tests { + use crate::{ + client::Client, + incoming, + requests::{ + EventCompletionRequest, EventErrorRequest, IntoRequest, IntoResponse, NextEventRequest, NextEventResponse, + }, + simulated, + types::Diagnostic, + Error, Runtime, }; - rsp.into_rsp().map_err(|e| e.into()) -} - -async fn complete_event(req: &Request, id: &str) -> Result, Error> { - assert_eq!(Method::POST, req.method()); - let rsp = Response::builder() - .status(StatusCode::ACCEPTED) - .body(Body::empty()) - .expect("Unable to construct response"); + use http::{uri::PathAndQuery, HeaderValue, Method, Request, Response, StatusCode, Uri}; + use hyper::{server::conn::Http, service::service_fn, Body}; + use serde_json::json; + use std::convert::TryFrom; + use tokio::{ + io::{AsyncRead, AsyncWrite}, + select, + sync::{self, oneshot}, + }; + use tokio_stream::StreamExt; + + #[cfg(test)] + async fn next_event(req: &Request) -> Result, Error> { + let path = "/2018-06-01/runtime/invocation/next"; + assert_eq!(req.method(), Method::GET); + assert_eq!(req.uri().path_and_query().unwrap(), &PathAndQuery::from_static(path)); + let body = json!({"message": "hello"}); + + let rsp = NextEventResponse { + request_id: "8476a536-e9f4-11e8-9739-2dfe598c3fcd", + deadline: 1_542_409_706_888, + arn: "arn:aws:lambda:us-east-2:123456789012:function:custom-runtime", + trace_id: "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419", + body: serde_json::to_vec(&body)?, + }; + rsp.into_rsp() + } - let expected = format!("/2018-06-01/runtime/invocation/{}/response", id); - assert_eq!(expected, req.uri().path()); + #[cfg(test)] + async fn complete_event(req: &Request, id: &str) -> Result, Error> { + assert_eq!(Method::POST, req.method()); + let rsp = Response::builder() + .status(StatusCode::ACCEPTED) + .body(Body::empty()) + .expect("Unable to construct response"); - Ok(rsp) -} + let expected = format!("/2018-06-01/runtime/invocation/{}/response", id); + assert_eq!(expected, req.uri().path()); -async fn event_err(req: &Request, id: &str) -> Result, Error> { - let expected = format!("/2018-06-01/runtime/invocation/{}/error", id); - assert_eq!(expected, req.uri().path()); + Ok(rsp) + } - assert_eq!(req.method(), Method::POST); - let header = "lambda-runtime-function-error-type"; - let expected = "unhandled"; - assert_eq!(req.headers()[header], HeaderValue::try_from(expected)?); + #[cfg(test)] + async fn event_err(req: &Request, id: &str) -> Result, Error> { + let expected = format!("/2018-06-01/runtime/invocation/{}/error", id); + assert_eq!(expected, req.uri().path()); - let rsp = Response::builder().status(StatusCode::ACCEPTED).body(Body::empty())?; - Ok(rsp) -} + assert_eq!(req.method(), Method::POST); + let header = "lambda-runtime-function-error-type"; + let expected = "unhandled"; + assert_eq!(req.headers()[header], HeaderValue::try_from(expected)?); -fn set_origin(base: Uri, req: Request) -> Result, Error> { - let (mut parts, body) = req.into_parts(); - let (scheme, authority) = { - let scheme = base.scheme().unwrap_or(&Scheme::HTTP); - let authority = base.authority().expect("Authority not found"); - (scheme, authority) - }; - let path = parts.uri.path_and_query().expect("PathAndQuery not found"); - - let uri = Uri::builder() - .scheme(scheme.clone()) - .authority(authority.clone()) - .path_and_query(path.clone()) - .build() - .expect("Unable to build URI"); + let rsp = Response::builder().status(StatusCode::ACCEPTED).body(Body::empty())?; + Ok(rsp) + } - parts.uri = uri; - Ok(Request::from_parts(parts, body)) -} + #[cfg(test)] + async fn handle_incoming(req: Request) -> Result, Error> { + let path: Vec<&str> = req + .uri() + .path_and_query() + .expect("PathAndQuery not found") + .as_str() + .split('/') + .collect::>(); + match path[1..] { + ["2018-06-01", "runtime", "invocation", "next"] => next_event(&req).await, + ["2018-06-01", "runtime", "invocation", id, "response"] => complete_event(&req, id).await, + ["2018-06-01", "runtime", "invocation", id, "error"] => event_err(&req, id).await, + ["2018-06-01", "runtime", "init", "error"] => unimplemented!(), + _ => unimplemented!(), + } + } -#[cfg(test)] -mod endpoint_tests { - use super::{handle, set_origin}; - use crate::{ - requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest}, - simulated::SimulatedConnector, - types::Diagnostic, - Error, - }; - use http::{HeaderValue, StatusCode, Uri}; - use std::convert::TryFrom; - use tokio::sync; + #[cfg(test)] + async fn handle(io: I, rx: oneshot::Receiver<()>) -> Result<(), hyper::Error> + where + I: AsyncRead + AsyncWrite + Unpin + 'static, + { + let conn = Http::new().serve_connection(io, service_fn(handle_incoming)); + select! { + _ = rx => { + Ok(()) + } + res = conn => { + match res { + Ok(()) => Ok(()), + Err(e) => { + Err(e) + } + } + } + } + } #[tokio::test] - async fn next_event() -> Result<(), Error> { - let (client, server) = crate::simulated::chan(); + async fn test_next_event() -> Result<(), Error> { let base = Uri::from_static("http://localhost:9001"); + let (client, server) = crate::simulated::chan(); let (tx, rx) = sync::oneshot::channel(); let server = tokio::spawn(async { handle(server, rx).await.expect("Unable to handle request"); }); - let conn = SimulatedConnector { inner: client }; - let client = hyper::Client::builder().build(conn); + let conn = simulated::Connector { inner: client }; + let client = Client::with(base, conn); let req = NextEventRequest.into_req()?; - let req = set_origin(base, req)?; - let rsp = client.request(req).await.expect("Unable to send request"); + let rsp = client.call(req).await.expect("Unable to send request"); assert_eq!(rsp.status(), StatusCode::OK); let header = "lambda-runtime-deadline-ms"; @@ -211,7 +184,7 @@ mod endpoint_tests { } #[tokio::test] - async fn ok_response() -> Result<(), Error> { + async fn test_ok_response() -> Result<(), Error> { let (client, server) = crate::simulated::chan(); let (tx, rx) = sync::oneshot::channel(); let base = Uri::from_static("http://localhost:9001"); @@ -220,17 +193,16 @@ mod endpoint_tests { handle(server, rx).await.expect("Unable to handle request"); }); - let conn = SimulatedConnector { inner: client }; - let client = hyper::Client::builder().build(conn); + let conn = simulated::Connector { inner: client }; + let client = Client::with(base, conn); let req = EventCompletionRequest { request_id: "156cb537-e2d4-11e8-9b34-d36013741fb9", body: "done", }; let req = req.into_req()?; - let req = set_origin(base, req)?; - let rsp = client.request(req).await?; + let rsp = client.call(req).await?; assert_eq!(rsp.status(), StatusCode::ACCEPTED); // shutdown server @@ -243,7 +215,7 @@ mod endpoint_tests { } #[tokio::test] - async fn error_response() -> Result<(), Error> { + async fn test_error_response() -> Result<(), Error> { let (client, server) = crate::simulated::chan(); let (tx, rx) = sync::oneshot::channel(); let base = Uri::from_static("http://localhost:9001"); @@ -252,8 +224,8 @@ mod endpoint_tests { handle(server, rx).await.expect("Unable to handle request"); }); - let conn = SimulatedConnector { inner: client }; - let client = hyper::Client::builder().build(conn); + let conn = simulated::Connector { inner: client }; + let client = Client::with(base, conn); let req = EventErrorRequest { request_id: "156cb537-e2d4-11e8-9b34-d36013741fb9", @@ -263,8 +235,7 @@ mod endpoint_tests { }, }; let req = req.into_req()?; - let req = set_origin(base, req)?; - let rsp = client.request(req).await?; + let rsp = client.call(req).await?; assert_eq!(rsp.status(), StatusCode::ACCEPTED); // shutdown server @@ -276,54 +247,38 @@ mod endpoint_tests { } } - // #[tokio::test] - // async fn run_end_to_end() -> Result<(), Error> { - // use serde_json::Value; - - // let (client, server) = crate::simulated::chan(); - - // let (tx, rx) = sync::oneshot::channel(); - // let server = tokio::spawn(async move { handle(server, rx) }); - - // async fn handler(s: Value) -> Result { - // INVOCATION_CTX.with(|_ctx| {}); - // Ok(s) - // } - // let handler = handler_fn(handler); - // let client = tokio::spawn(async move { - // run_simulated(handler, &url).await?; - // Ok::<(), Error>(()) - // }); - // race!(client, server); - // Ok(()) - // } - - // #[tokio::test] - // async fn test_stream_handler() -> Result<(), Error> { - // let (client, server) = crate::simulated::chan(); - // let req = Request::builder() - // .method(Method::GET) - // .uri("http://httpbin.org") - // .body(Body::empty()) - // .expect("Can't build request"); - - // let conn = SimulatedConnector { inner: client }; - // let client = hyper::Client::builder().build(conn); - - // let (tx, rx) = sync::oneshot::channel(); - // let server = tokio::spawn(async { - // handle(server, rx).await.expect("Unable to handle request"); - // }); - - // let rsp = client.request(req).await.expect("Unable to send request"); - // assert_eq!(rsp.status(), http::StatusCode::OK); - - // // shutdown server - // tx.send(()).expect("Receiver has been dropped"); - // match server.await { - // Ok(_) => Ok(()), - // Err(e) if e.is_panic() => return Err::<(), anyhow::Error>(e.into()), - // Err(_) => unreachable!("This branch shouldn't be reachable"), - // } - // } + #[tokio::test] + async fn successful_end_to_end_run() -> Result<(), Error> { + let (client, server) = crate::simulated::chan(); + let (tx, rx) = sync::oneshot::channel(); + let base = Uri::from_static("http://localhost:9001"); + + let server = tokio::spawn(async { + handle(server, rx).await.expect("Unable to handle request"); + }); + let conn = simulated::Connector { inner: client }; + + let runtime = Runtime::builder() + .with_endpoint(base) + .with_connector(conn) + .build() + .expect("Unable to build runtime"); + + async fn func(event: serde_json::Value, _: crate::Context) -> Result { + Ok(event) + } + let f = crate::handler_fn(func); + + let client = &runtime.client; + let incoming = incoming(client).take(1); + runtime.run(incoming, f).await?; + + // shutdown server + tx.send(()).expect("Receiver has been dropped"); + match server.await { + Ok(_) => Ok(()), + Err(e) if e.is_panic() => return Err::<(), Error>(e.into()), + Err(_) => unreachable!("This branch shouldn't be reachable"), + } + } } diff --git a/lambda/src/lib.rs b/lambda/src/lib.rs index 6f420f56..44077f0e 100644 --- a/lambda/src/lib.rs +++ b/lambda/src/lib.rs @@ -1,24 +1,28 @@ -#![deny(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![deny(clippy::all, clippy::cargo)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] -//! The official Rust runtime for AWS Lambda. -//! //! The mechanism available for defining a Lambda function is as follows: //! //! Create a type that conforms to the [`Handler`] trait. This type can then be passed //! to the the `lambda::run` function, which launches and runs the Lambda runtime. pub use crate::types::Context; use client::Client; -use futures::stream::{Stream, StreamExt}; +use hyper::client::{connect::Connection, HttpConnector}; use serde::{Deserialize, Serialize}; use std::{ convert::{TryFrom, TryInto}, env, fmt, future::Future, + sync::Arc, }; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_stream::{Stream, StreamExt}; +use tower_service::Service; +use tracing::{error, trace}; mod client; mod requests; +#[cfg(test)] mod simulated; /// Types available to a Lambda function. mod types; @@ -61,22 +65,26 @@ impl Config { } } -/// A trait describing an asynchronous function `A` to `B. +/// A trait describing an asynchronous function `A` to `B`. pub trait Handler { /// Errors returned by this handler. type Error; - /// The future response value of this handler. + /// Response of this handler. type Fut: Future>; - /// Process the incoming event and `Context` then return the response asynchronously. - fn call(&mut self, event: A, context: Context) -> Self::Fut; + /// Handle the incoming event. + fn call(&self, event: A, context: Context) -> Self::Fut; } -/// Returns a new `HandlerFn` with the given closure. +/// Returns a new [`HandlerFn`] with the given closure. +/// +/// [`HandlerFn`]: struct.HandlerFn.html pub fn handler_fn(f: F) -> HandlerFn { HandlerFn { f } } -/// A `Handler` implemented by a closure. +/// A [`Handler`] implemented by a closure. +/// +/// [`Handler`]: trait.Handler.html #[derive(Clone, Debug)] pub struct HandlerFn { f: F, @@ -86,15 +94,179 @@ impl Handler for HandlerFn where F: Fn(A, Context) -> Fut, Fut: Future> + Send, - Error: Into + fmt::Debug, + Error: Into> + fmt::Display, { type Error = Error; type Fut = Fut; - fn call(&mut self, req: A, ctx: Context) -> Self::Fut { + fn call(&self, req: A, ctx: Context) -> Self::Fut { (self.f)(req, ctx) } } +#[non_exhaustive] +#[derive(Debug, PartialEq)] +enum BuilderError { + UnsetUri, +} + +struct Runtime = HttpConnector> { + client: Client, +} + +impl Runtime { + pub fn builder() -> RuntimeBuilder { + RuntimeBuilder { + connector: HttpConnector::new(), + uri: None, + } + } +} + +impl Runtime +where + C: Service + Clone + Send + Sync + Unpin + 'static, + >::Future: Unpin + Send, + >::Error: Into>, + >::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, +{ + pub async fn run( + &self, + incoming: impl Stream, Error>> + Send, + handler: F, + ) -> Result<(), Error> + where + F: Handler + Send + Sync + 'static, + >::Fut: Future>::Error>> + Send + 'static, + >::Error: fmt::Display + Send + Sync + 'static, + A: for<'de> Deserialize<'de> + Send + Sync + 'static, + B: Serialize + Send + Sync + 'static, + { + let client = &self.client; + let handler = Arc::new(handler); + tokio::pin!(incoming); + while let Some(event) = incoming.next().await { + trace!("New event arrived (run loop)"); + let event = event?; + let (parts, body) = event.into_parts(); + + let ctx: Context = Context::try_from(parts.headers)?; + let body = hyper::body::to_bytes(body).await?; + trace!("{}", std::str::from_utf8(&body)?); // this may be very verbose + let body = serde_json::from_slice(&body)?; + + let handler = Arc::clone(&handler); + let request_id = &ctx.request_id.clone(); + let task = tokio::spawn(async move { handler.call(body, ctx) }); + + let req = match task.await { + Ok(response) => match response.await { + Ok(response) => { + trace!("Ok response from handler (run loop)"); + EventCompletionRequest { + request_id, + body: response, + } + .into_req() + } + Err(err) => { + error!("{}", err); // logs the error in CloudWatch + EventErrorRequest { + request_id, + diagnostic: Diagnostic { + error_type: type_name_of_val(&err).to_owned(), + error_message: format!("{}", err), // returns the error to the caller via Lambda API + }, + } + .into_req() + } + }, + Err(err) if err.is_panic() => { + error!("{:?}", err); // inconsistent with other log record formats - to be reviewed + EventErrorRequest { + request_id, + diagnostic: Diagnostic { + error_type: type_name_of_val(&err).to_owned(), + error_message: format!("Lambda panicked: {}", err), + }, + } + .into_req() + } + Err(_) => unreachable!("tokio::task should not be canceled"), + }; + let req = req?; + client.call(req).await.expect("Unable to send response to Runtime APIs"); + } + Ok(()) + } +} + +struct RuntimeBuilder = hyper::client::HttpConnector> { + connector: C, + uri: Option, +} + +impl RuntimeBuilder +where + C: Service + Clone + Send + Sync + Unpin + 'static, + >::Future: Unpin + Send, + >::Error: Into>, + >::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, +{ + pub fn with_connector(self, connector: C2) -> RuntimeBuilder + where + C2: Service + Clone + Send + Sync + Unpin + 'static, + >::Future: Unpin + Send, + >::Error: Into>, + >::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, + { + RuntimeBuilder { + connector, + uri: self.uri, + } + } + + pub fn with_endpoint(self, uri: http::Uri) -> Self { + Self { uri: Some(uri), ..self } + } + + pub fn build(self) -> Result, BuilderError> { + let uri = match self.uri { + Some(uri) => uri, + None => return Err(BuilderError::UnsetUri), + }; + let client = Client::with(uri, self.connector); + + Ok(Runtime { client }) + } +} + +#[test] +fn test_builder() { + let runtime = Runtime::builder() + .with_connector(HttpConnector::new()) + .with_endpoint(http::Uri::from_static("http://nomatter.com")) + .build(); + + runtime.unwrap(); +} + +fn incoming(client: &Client) -> impl Stream, Error>> + Send + '_ +where + C: Service + Clone + Send + Sync + Unpin + 'static, + >::Future: Unpin + Send, + >::Error: Into>, + >::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, +{ + async_stream::stream! { + loop { + trace!("Waiting for next event (incoming loop)"); + let req = NextEventRequest.into_req().expect("Unable to construct request"); + let res = client.call(req).await; + yield res; + } + } +} + /// Starts the Lambda Rust runtime and begins polling for events on the [Lambda /// Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). /// @@ -118,88 +290,24 @@ where /// ``` pub async fn run(handler: F) -> Result<(), Error> where - F: Handler, - >::Error: fmt::Debug, - A: for<'de> Deserialize<'de>, - B: Serialize, + F: Handler + Send + Sync + 'static, + >::Fut: Future>::Error>> + Send + 'static, + >::Error: fmt::Display + Send + Sync + 'static, + A: for<'de> Deserialize<'de> + Send + Sync + 'static, + B: Serialize + Send + Sync + 'static, { - let mut handler = handler; + trace!("Loading config from env"); let config = Config::from_env()?; let uri = config.endpoint.try_into().expect("Unable to convert to URL"); - let client = Client::with(uri, hyper::Client::new()); - let incoming = incoming(&client); - run_inner(&client, incoming, &mut handler).await?; - - Ok(()) -} - -/// Runs the lambda function almost entirely in-memory. This is meant for testing. -pub async fn run_simulated(handler: F, url: &str) -> Result<(), Error> -where - F: Handler, - >::Error: fmt::Debug, - A: for<'de> Deserialize<'de>, - B: Serialize, -{ - let mut handler = handler; - let uri = url.try_into().expect("Unable to convert to URL"); - let client = Client::with(uri, hyper::Client::new()); - let incoming = incoming(&client).take(1); - run_inner(&client, incoming, &mut handler).await?; - - Ok(()) -} - -fn incoming(client: &Client) -> impl Stream, Error>> + '_ { - async_stream::stream! { - loop { - let req = NextEventRequest.into_req().expect("Unable to construct request"); - let res = client.call(req).await; - yield res; - } - } -} - -async fn run_inner( - client: &Client, - incoming: impl Stream, Error>>, - handler: &mut F, -) -> Result<(), Error> -where - F: Handler, - >::Error: fmt::Debug, - A: for<'de> Deserialize<'de>, - B: Serialize, -{ - tokio::pin!(incoming); - - while let Some(event) = incoming.next().await { - let event = event?; - let (parts, body) = event.into_parts(); - - let mut ctx: Context = Context::try_from(parts.headers)?; - ctx.env_config = Config::from_env()?; - let body = hyper::body::to_bytes(body).await?; - let body = serde_json::from_slice(&body)?; - - let request_id = &ctx.request_id.clone(); - let f = handler.call(body, ctx); - - let req = match f.await { - Ok(res) => EventCompletionRequest { request_id, body: res }.into_req()?, - Err(e) => EventErrorRequest { - request_id, - diagnostic: Diagnostic { - error_message: format!("{:?}", e), - error_type: type_name_of_val(e).to_owned(), - }, - } - .into_req()?, - }; - client.call(req).await?; - } + let runtime = Runtime::builder() + .with_connector(HttpConnector::new()) + .with_endpoint(uri) + .build() + .expect("Unable create runtime"); - Ok(()) + let client = &runtime.client; + let incoming = incoming(client); + runtime.run(incoming, handler).await } fn type_name_of_val(_: T) -> &'static str { diff --git a/lambda/src/requests.rs b/lambda/src/requests.rs index 2d691b94..a89f1689 100644 --- a/lambda/src/requests.rs +++ b/lambda/src/requests.rs @@ -134,7 +134,7 @@ struct InitErrorRequest; impl IntoRequest for InitErrorRequest { fn into_req(self) -> Result, Error> { - let uri = format!("/2018-06-01/runtime/init/error"); + let uri = "/2018-06-01/runtime/init/error".to_string(); let uri = Uri::from_str(&uri)?; let req = Request::builder() diff --git a/lambda/src/simulated.rs b/lambda/src/simulated.rs index 843a2842..7d1931c3 100644 --- a/lambda/src/simulated.rs +++ b/lambda/src/simulated.rs @@ -11,7 +11,7 @@ use std::{ }; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -/// Creates a pair of AsyncReadWrite data streams, where the write end of each member of the pair +/// Creates a pair of `AsyncRead`/`AsyncWrite` data streams, where the write end of each member of the pair /// is the read end of the other member of the pair. This allows us to emulate the behavior of a TcpStream /// but in-memory, deterministically, and with full control over failure injection. pub(crate) fn chan() -> (SimStream, SimStream) { @@ -36,11 +36,11 @@ pub(crate) fn chan() -> (SimStream, SimStream) { } #[derive(Clone)] -pub struct SimulatedConnector { +pub struct Connector { pub inner: SimStream, } -impl hyper::service::Service for SimulatedConnector { +impl hyper::service::Service for Connector { type Response = SimStream; type Error = std::io::Error; type Future = Pin> + Send>>; @@ -103,6 +103,7 @@ pub struct BufferState { } impl BufferState { + /// Creates a new `BufferState`. fn new() -> Self { BufferState { buffer: VecDeque::new(), @@ -207,7 +208,7 @@ mod tests { client.write_all(b"Ping").await.expect("Write should succeed"); // Verify we can read it from side 2 - let mut read_on_server = [0u8; 4]; + let mut read_on_server = [0_u8; 4]; server .read_exact(&mut read_on_server) .await @@ -218,7 +219,7 @@ mod tests { server.write_all(b"Pong").await.expect("Write should succeed"); // Verify we can read it from side 1 - let mut read_on_client = [0u8; 4]; + let mut read_on_client = [0_u8; 4]; client .read_exact(&mut read_on_client) .await