├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── .gitignore ├── error_handling │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── simple │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── src │ └── lib.rs │ └── worker │ ├── metadata_wasm.json │ └── worker.js ├── src ├── assets.rs ├── context.rs ├── error.rs ├── httpdate.rs ├── js_values.rs ├── lib.rs ├── media_type.rs ├── method.rs ├── request.rs └── response.rs └── tests ├── context.rs ├── method.rs └── request.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /target 3 | pkg/ 4 | wasm-pack.log 5 | worker/generated/ 6 | wrangler.toml 7 | config.toml 8 | src/config.rs 9 | tmp/ 10 | env 11 | .idea 12 | 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.1 2 | 3 | - fix: RunContext needs to be decelared Sync (even though Workers are 4 | single-threaded) 5 | 6 | ## 0.5.0 7 | 8 | - feature: support for oauth - see wasm-service-oauth crate 9 | - feature: static asset handler, for serving static files 10 | - feature: add media_type() for looking up media type (aka MIME) based on file extension. 11 | - feature: added internal_error_handler in ServiceConfig. 12 | If you don't want to write one, a default implementation is included in ServiceConfig::default() 13 | - added two example projects: simple/ and error_handling/ 14 | - removed context.default_content_type 15 | - added Request.is_empty() to test whether POST/PUT body has zero bytes 16 | - made ctx.take_logs() public 17 | - removed Mutex for deferred task logging - not needed because 18 | workers are single-threaded 19 | - response.body() takes Into(Body) so can take a wider range of parameters 20 | - additional dependencies: mime,bincode,chrono,kv_assets 21 | - upgrade dependencies: reqwest 0.11 22 | 23 | ## v0.4.0 2021-01-12 24 | 25 | - response.body takes Vec instead of &[u8] to avoid an extra copy 26 | - added impl Default for ServiceConfig 27 | 28 | ## v0.3 29 | 30 | - __Breaking changes__ to support add-ons 31 | - for `run()`, if you did logging with `log!(lq,...`, this changes to 32 | `log(ctx,...` 33 | - `handle()` changes from 34 | ```async fn handle(&self, ctx: &mut Context) -> Result<(), E>;``` 35 | to 36 | ```async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn>``` 37 | - Request is available as a separate parameter to `handle`, so instead 38 | of `ctx.request()` you can just use `req`. 39 | This avoids conflicting borrows from immutable `req`, 40 | and mutable `ctx` (needed for logging and updating Response). 41 | - The error return type is `HandlerReturn` instead of a generic 42 | error trait `E`. HandlerReturn can be used for 302 redirects, or 43 | can contain the body of a response page. As the Err() variant 44 | of Result, it makes it easy to return a response with the '?' 45 | operator. This also forces the developer to think about how internal 46 | errors should translate into user-visible http responses. 47 | - `service_request` takes an array of Handlers instead of one, 48 | so Handlers may be chained, and used like "middleware" 49 | or plugins. An Oauth2 plugin is under development. 50 | 51 | - New features: 52 | - Support for add-ons, implemented as a chain of Handlers. 53 | - methods: [`Request.get_cookie_value`], [`Request.get_query_value`], 54 | [`Response.is_unset`] 55 | - enum: [`Method::OPTIONS`] 56 | 57 | - Fixes 58 | - moved wasm-bindgen-test to dev-dependencies 59 | 60 | - Other 61 | - More unit tests. To run all tests, run both: 62 | - cargo test 63 | - wasm-pack test --firefox --headless 64 | 65 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-service" 3 | version = "0.5.1" 4 | authors = ["stevelr "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["wasm","cloudflare","workers","worker","http"] 8 | description = "Simplify implementation of serverless WASM on Cloudflare Workers" 9 | repository = "https://github.com/stevelr/wasm-service" 10 | homepage = "https://github.com/stevelr/wasm-service" 11 | documentation = "https://docs.rs/wasm-service" 12 | readme = "README.md" 13 | categories = ["web-programming::http-server","wasm","asynchronous"] 14 | 15 | [features] 16 | # "std": use the std allocator; "alloc": you provide an allocator 17 | default=["alloc"] 18 | std = ["serde_json/std", "serde/std", "service-logging/std"] 19 | alloc = ["serde_json/alloc", "serde/alloc", "service-logging/alloc"] 20 | 21 | [dependencies] 22 | async-trait = "0.1" 23 | bincode = "1.3" 24 | bytes = "1.0" 25 | chrono = "0.4" 26 | js-sys = "0.3" 27 | kv-assets = "0.2" 28 | mime = "0.3" 29 | reqwest = { version="0.11", features=["json"] } 30 | url = "2.2" 31 | wasm-bindgen = "0.2" 32 | wasm-bindgen-futures = "0.4" 33 | 34 | # optional 35 | serde_json = { version="1.0", default-features=false, optional=true } 36 | serde = { version="1.0", optional=true, features=["derive"] } 37 | service-logging = { version = "0.4", default-features=false, optional=true } 38 | 39 | [dependencies.web-sys] 40 | version = "0.3.4" 41 | features = [ 42 | 'console', 43 | ] 44 | 45 | [dev-dependencies] 46 | wasm-bindgen-test = "0.3" 47 | cfg-if = "1.0" 48 | 49 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Steve Schoettler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lightweight library for building Rust-WASM services on Cloudflare Workers. 2 | 3 | 4 | The goal of this library is to make it easy to build fast and 5 | lightweight HTTP-based services in WASM, hosted on Cloudflare Workers. 6 | To keep things fast and lightweight, there is a strong preference for 7 | significant new capabilities to added as compile-time features or separate 8 | libraries. 9 | 10 | ## Features 11 | 12 | - Fully async 13 | - Request & response bodies can be text, json, or binary 14 | - Non-blocking structured logging via [`service-logging`](https://github.com/stevelr/service-logging) 15 | - Deferred tasks that run after response is returned to client 16 | - Static file handling 17 | 18 | ## Add-ons 19 | 20 | - CORS and OAuth via [`wasm-service-oauth`](https://github.com/stevelr/wasm-service-oauth) 21 | 22 | ## Getting started 23 | 24 | To start a new project, 25 | 26 | wrangler generate -t rust PROJECT \ 27 | https://github.com/stevelr/rustwasm-service-template 28 | 29 | where PROJECT is your project name. 30 | 31 | [rustwasm-service-template](https://github.com/stevelr/rustwasm-service-template/blob/master/README.md) 32 | contains some relevant sample code, as well as 33 | instructions for setting up of Cloudflare and (optionally) Coralogix logging 34 | service. 35 | 36 | ## License 37 | 38 | Licensed under either of 39 | 40 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://apache.org/licenses/LICENSE-2.0) 41 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 42 | 43 | ## Updates 44 | 45 | See [CHANGELOG](./CHANGELOG.md) for recent changes 46 | 47 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /examples/error_handling/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /target 3 | pkg/ 4 | wasm-pack.log 5 | worker/generated/ 6 | env 7 | 8 | -------------------------------------------------------------------------------- /examples/error_handling/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-service-error-handling" 3 | version = "0.1.0" 4 | authors = [""] 5 | edition = "2018" 6 | description = "Sample error handling code" 7 | 8 | [features] 9 | default = ["wee_alloc"] 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [dependencies] 15 | wasm-bindgen = { version="0.2", features=["serde-serialize"] } 16 | wasm-bindgen-futures = "0.4" 17 | async-trait = "0.1" 18 | js-sys = "0.3" 19 | 20 | wasm-service = { version="0.5", features=["alloc"] } 21 | service-logging = { version="0.4", features=["alloc"] } 22 | serde = { version="1.0", default-features=false, features=["alloc","derive"]} 23 | serde_json = {version = "1.0", default-features = false, features = ["alloc"]} 24 | wee_alloc = { version = "0.4", optional=true } 25 | web-sys = { version = "0.3", features=[ "Headers", "console" ] } 26 | 27 | [dev-dependencies] 28 | wasm-bindgen-test = "0.3" 29 | 30 | [profile.release] 31 | # Tell `rustc` to optimize for small code size. 32 | opt-level = "s" 33 | -------------------------------------------------------------------------------- /examples/error_handling/src/lib.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | #[cfg(any(doc, target_arch = "wasm32"))] 3 | use service_logging::ConsoleLogger; 4 | use wasm_bindgen::JsValue; 5 | use wasm_service::{Context, Handler, HandlerReturn, Request, ServiceConfig}; 6 | 7 | /// Catch-all error handler that generates error page 8 | fn internal_error(e: impl std::error::Error) -> HandlerReturn { 9 | HandlerReturn { 10 | status: 200, 11 | text: format!( 12 | r#" 13 | 14 | Server error 15 | 16 |

Error

17 |

Internal error has occurred: {:?}

18 | 19 | "#, 20 | e 21 | ), 22 | } 23 | } 24 | 25 | /// The '/err' url has a bug (intentional) that will result in generation of the internal_error page. 26 | struct MyHandler {} 27 | #[async_trait(?Send)] 28 | impl Handler for MyHandler { 29 | /// Process incoming Request 30 | async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn> { 31 | use wasm_service::Method::GET; 32 | 33 | match (req.method(), req.url().path()) { 34 | (GET, "/") => { 35 | ctx.response().text("OK"); 36 | } 37 | (GET, "/err") => { 38 | // demonstration of using 'internal_error' with '?' to generate minimal error page. 39 | let x: i32 = "not_an_int".parse().map_err(internal_error)?; 40 | ctx.response().text(format!("you never see this: {}", x)); 41 | } 42 | _ => { 43 | ctx.response().status(404).text("Not Found"); 44 | } 45 | } 46 | Ok(()) 47 | } 48 | } 49 | 50 | pub async fn main_entry(req: JsValue) -> Result { 51 | wasm_service::service_request( 52 | req, 53 | ServiceConfig { 54 | logger: service_logging::ConsoleLogger::init(), 55 | handlers: vec![Box::new(MyHandler {})], 56 | ..Default::default() 57 | }, 58 | ) 59 | .await 60 | } 61 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /target 3 | pkg/ 4 | wasm-pack.log 5 | worker/generated/ 6 | src/config.rs 7 | env 8 | 9 | -------------------------------------------------------------------------------- /examples/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-service-example-simple" 3 | version = "0.2.5" 4 | authors = ["stevelr "] 5 | edition = "2018" 6 | description = "Template for WASM service using Cloudflare Workers" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [features] 12 | default = ["wee_alloc"] 13 | 14 | [dependencies] 15 | cfg-if = "1.0" 16 | wasm-bindgen = { version="0.2", features=["serde-serialize"] } 17 | wasm-bindgen-futures = "0.4" 18 | async-trait = "0.1" 19 | js-sys = "0.3" 20 | reqwest = { version="0.11", features=["json"] } 21 | 22 | # custom allocator 23 | wasm-service = { version="0.5", features=["alloc"] } 24 | service-logging = { version="0.4", features=["alloc"] } 25 | serde = { version="1.0", default-features=false, features=["alloc","derive"]} 26 | serde_json = {version = "1.0", default-features = false, features = ["alloc"]} 27 | 28 | # The `console_error_panic_hook` crate provides better debugging of panics by 29 | # logging them with `console.error`. This is great for development, but requires 30 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 31 | # code size when deploying. 32 | #console_error_panic_hook = { version = "0.1.1", optional = true } 33 | 34 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 35 | # compared to the default allocator's ~10K. It is slower than the default 36 | # allocator, however. 37 | wee_alloc = { version = "0.4", optional = true } 38 | 39 | [dependencies.web-sys] 40 | version = "0.3" 41 | features = [ 42 | 'Event', 43 | 'console', 44 | ] 45 | 46 | [build-dependencies] 47 | config_struct = { version = "0.5", features=["toml-parsing"] } 48 | 49 | [dev-dependencies] 50 | wasm-bindgen-test = "0.3" 51 | 52 | [profile.release] 53 | # Tell `rustc` to optimize for small code size. 54 | opt-level = "s" 55 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | "Hello world" simple example of using wasm-service 2 | 3 | - includes Coralogix logging 4 | 5 | -------------------------------------------------------------------------------- /examples/simple/build.rs: -------------------------------------------------------------------------------- 1 | // build.rs 2 | // Compile config.toml settings into src/config.rs 3 | 4 | use config_struct::{Error, StructOptions}; 5 | 6 | fn main() -> Result<(), Error> { 7 | config_struct::create_struct("config.toml", "src/config.rs", &StructOptions::default()) 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// wasm-service template 2 | /// 3 | use async_trait::async_trait; 4 | use cfg_if::cfg_if; 5 | use service_logging::{log, CoralogixConfig, CoralogixLogger, Severity}; 6 | use wasm_bindgen::{prelude::*, JsValue}; 7 | use wasm_service::{Context, Handler, HandlerReturn, Method::GET, Request, ServiceConfig}; 8 | 9 | // compile-time config settings, defined in config.toml 10 | mod config; 11 | use config::CONFIG; 12 | 13 | cfg_if! { 14 | if #[cfg(feature="wee_alloc")] { 15 | // Use `wee_alloc` as the global allocator. 16 | #[global_allocator] 17 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 18 | } 19 | } 20 | 21 | struct MyHandler {} 22 | #[async_trait(?Send)] 23 | impl Handler for MyHandler { 24 | async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn> { 25 | match (req.method(), req.url().path()) { 26 | (GET, "/") => { 27 | log!(ctx, Severity::Info, _:"hello logger"); 28 | ctx.response().text("OK"); 29 | } 30 | (GET, "/hello") => { 31 | ctx.response().text("Hello friend!"); 32 | } 33 | _ => {} // 404 fallthrough is handled by wasm-service 34 | } 35 | Ok(()) 36 | } 37 | } 38 | 39 | /// Main entry to service worker, called from javascript 40 | #[wasm_bindgen] 41 | pub async fn main_entry(req: JsValue) -> Result { 42 | let logger = match CONFIG.logging.logger.as_ref() { 43 | //"console" => ConsoleLogger::init(), 44 | "coralogix" => CoralogixLogger::init(CoralogixConfig { 45 | api_key: &CONFIG.logging.coralogix.api_key, 46 | application_name: &CONFIG.logging.coralogix.application_name, 47 | endpoint: &CONFIG.logging.coralogix.endpoint, 48 | }) 49 | .map_err(|e| JsValue::from_str(&e.to_string()))?, 50 | _ => { 51 | return Err(JsValue::from_str(&format!( 52 | "Invalid logger configured:'{}'", 53 | CONFIG.logging.logger 54 | ))); 55 | } 56 | }; 57 | wasm_service::service_request( 58 | req, 59 | ServiceConfig { 60 | logger, 61 | handlers: vec![Box::new(MyHandler {})], 62 | ..Default::default() 63 | }, 64 | ) 65 | .await 66 | } 67 | -------------------------------------------------------------------------------- /examples/simple/worker/metadata_wasm.json: -------------------------------------------------------------------------------- 1 | { 2 | "body_part": "script", 3 | "bindings": [ 4 | { 5 | "name": "wasm", 6 | "type": "wasm_module", 7 | "part": "wasmprogram" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/simple/worker/worker.js: -------------------------------------------------------------------------------- 1 | // worker.js 2 | // send fetch events to rust wasm 3 | addEventListener('fetch', event => { 4 | event.respondWith(handleRequest(event)) 5 | }) 6 | 7 | // Forward incoming requests to Rust Handler. 8 | // 9 | // Deferred tasks, if any, are passed in a Promise to event.waitUntil, 10 | // so they can be processed after the response is returned to the client. 11 | async function handleRequest(event) { 12 | const { main_entry, run_deferred } = wasm_bindgen; 13 | await wasm_bindgen(wasm); 14 | const request = event.request; 15 | var result, response; 16 | try { 17 | if (request.cf !== undefined) { 18 | const tlsVersion = request.cf.tlsVersion 19 | // Using "Security by Default" principles, this is set to limit 20 | // requests to at least TLS 1.3. If you need to enable TLS 1.2, 21 | // modify the condition below to 22 | // if (tlsVersion != "TLSv1.2" && tlsVersion != "TLSv1.3") { 23 | if (tlsVersion != "TLSv1.3") { 24 | return new Response( 25 | "Please use TLS version 1.3 or higher.", { status: 403, } 26 | ); 27 | } 28 | } 29 | 30 | // Fully read body (synchronously) before calling Rust handler. 31 | // For protection against excessive uploads, the maximum data upload size 32 | // can be set in dash.cloudflare.com -> Network -> "Maximum Upload Size" 33 | let input = new Map(); 34 | input.set("body", new Uint8Array(await request.arrayBuffer())); 35 | input.set("method", request.method); 36 | input.set("url", request.url); 37 | input.set("headers", request.headers); 38 | input.set("event", event); 39 | 40 | // call rust handler, put results into a Response object 41 | result = await main_entry(input); 42 | var body_bin = result.get("body"); // Uint8Array 43 | response = new Response(body_bin, { 44 | status: result.get("status"), 45 | headers: result.get("headers"), 46 | }); 47 | } catch(error) { 48 | response = new Response("Error:" + error, {status: 200}); 49 | } 50 | return response; 51 | } 52 | -------------------------------------------------------------------------------- /src/assets.rs: -------------------------------------------------------------------------------- 1 | use crate::{handler_return, Context, Error, Handler, HandlerReturn, HttpDate, Method, Request}; 2 | use async_trait::async_trait; 3 | //use service_logging::{log, Severity}; 4 | use std::str::FromStr; 5 | 6 | use kv_assets::{AssetMetadata, KVAssets}; 7 | 8 | /// Serves static assets out of Worker KV storage. 9 | pub struct StaticAssetHandler<'assets> { 10 | kv: KVAssets<'assets>, 11 | } 12 | 13 | impl<'assets> StaticAssetHandler<'assets> { 14 | /// Initialize static asset handler 15 | /// `index_bin` is the serialized AssetIndex, which will be deserialized lazily (if needed) 16 | /// `account_id` is Cloudflare account id 17 | /// `namespace_id` is cloudflare KV namespace id (the long hex string, not the friendly name) 18 | /// `auth_token` Cloudflare api OAuth token 19 | pub fn init( 20 | index_bin: &'assets [u8], 21 | account_id: &'_ str, 22 | namespace_id: &'_ str, 23 | auth_token: &'_ str, 24 | ) -> Self { 25 | Self { 26 | kv: KVAssets::init(index_bin, account_id, namespace_id, auth_token), 27 | } 28 | } 29 | 30 | /// Returns true if there is a static asset matching this path. 31 | /// Only checks the manifest - does not check KV. This could give a false positive 32 | /// positive if the manifest is out of date, so site developers must ensure that 33 | /// the manifest is regenerated and pushed if any user deletes static content from KV. 34 | /// That is unlikely to occur if kv-sync is being used to update the manifest 35 | /// and values in the static namespace at the same time. 36 | /// 37 | /// There is also a potential scenario where this function could return true and the handler 38 | /// later has a network problem reading from KV, or the account credentials are bad, 39 | /// and the end user isn't able to retrieve content for which this method returns true. 40 | /// 41 | /// Due to these two potential problems, a true result isn't a 100% guarantee that 42 | /// the user will receive content, but in the presence of good deploy practices 43 | /// and reliable networking, this should be accurate. 44 | pub fn has_asset(&self, req: &Request) -> bool { 45 | (req.method() == Method::GET || req.method() == Method::HEAD) 46 | && matches!(self.kv.lookup_key(req.url().path()), Ok(Some(_))) 47 | } 48 | 49 | /// Does some quick checks and may return 50 | /// - 304 Not Modified, if request had if-modified-since header and doc was <= header date 51 | /// - 200 if request was HEAD method 52 | /// Returns Ok(None) if content is not found (no path match) 53 | /// Returns Ok(Some(metadata)) if doc is found 54 | fn check_metadata( 55 | &self, 56 | path: &str, 57 | req: &Request, 58 | ctx: &mut Context, 59 | ) -> Result, HandlerReturn> { 60 | use reqwest::header::IF_MODIFIED_SINCE; 61 | 62 | match self.kv.lookup_key(path) { 63 | Err(e) => { 64 | ctx.raise_internal_error(Box::new(e)); 65 | Err(handler_return(200, "")) // handle internal error higher in the stack 66 | } 67 | Ok(None) => { 68 | // file not found 69 | Ok(None) 70 | } 71 | Ok(Some(md)) => { 72 | // GET or HEAD 73 | if let Some(dt) = req.get_header(IF_MODIFIED_SINCE.as_str()) { 74 | if let Ok(http_date) = HttpDate::from_str(dt.as_str()) { 75 | // valid if-modified-since header with parsable date 76 | // if kv is same or older (smaller time), return Not Modified 77 | if md.modified <= http_date.timestamp() as u64 { 78 | return Err(handler_return(304, "Not Modified")); 79 | } 80 | // else modified, so fall through 81 | } else { 82 | // don't bother logging date parse errors 83 | //log!(ctx, Severity::Warning, _:"parse_date_err", val:&dt) 84 | } 85 | } 86 | /* 87 | // HEAD only 88 | if req.method() == Method::HEAD { 89 | ctx.response() 90 | .header(LAST_MODIFIED, HttpDate::from(md.modified).to_string()) 91 | .unwrap(); // unwrap is ok because number.to_string() is always ascii 92 | Err(handler_return(200, "")) 93 | } else { 94 | Ok(md) 95 | } 96 | */ 97 | Ok(Some(md)) 98 | } 99 | } 100 | } 101 | } 102 | 103 | fn remove_leading_slash(path: &str) -> &str { 104 | path.strip_prefix('/').unwrap_or(path) 105 | } 106 | 107 | #[async_trait(?Send)] 108 | impl<'assets> Handler for StaticAssetHandler<'assets> { 109 | /// Process incoming Request. If no asset was found at the request path, response.is_unset() will be true. 110 | /// Only handles GET and HEAD requests. 111 | async fn handle(&self, req: &Request, mut ctx: &mut Context) -> Result<(), HandlerReturn> { 112 | let path = remove_leading_slash(req.url().path()); 113 | if (req.method() != Method::GET && req.method() != Method::HEAD) || path.is_empty() { 114 | return Ok(()); 115 | } 116 | // change trailing slash, indicating 'folder' to folder/index.html 117 | let path = if path.ends_with('/') { 118 | format!("{}/index.html", path) 119 | } else { 120 | path.to_string() 121 | }; 122 | // This may return quickly if response can be satisfied without querying KV, 123 | // such as HEAD requests, or If-modified-since header when it hasn't been modified 124 | let md = match self.check_metadata(&path, req, &mut ctx)? { 125 | None => return Ok(()), // not found: fall through to let service handler deal with it 126 | Some(md) => md, 127 | }; 128 | // have metadata, asset is in KV (unless manifest is out of date) 129 | match self.kv.get_kv_value(&md.path).await { 130 | Ok(bytes) => { 131 | // if we can figure out the content type, report it 132 | // otherwise let browser sniff it 133 | if let Some(mt) = crate::media_type(&md.path) { 134 | ctx.response() 135 | .header(reqwest::header::CONTENT_TYPE, mt.to_string()) 136 | .unwrap(); 137 | } 138 | ctx.response() 139 | .header("last-modified", HttpDate::from(md.modified).to_string()) 140 | .unwrap() 141 | .body(bytes.to_vec()); 142 | } 143 | Err(e) => { 144 | ctx.raise_internal_error(Box::new(Error::Other(format!( 145 | "static asset lookup failed path({}) url-path({}) error:{}", 146 | &md.path, 147 | path, 148 | e.to_string() 149 | )))); 150 | } 151 | } 152 | Ok(()) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use crate::Response; 2 | use crate::Runnable; 3 | use service_logging::{LogEntry, LogQueue}; 4 | use std::panic::UnwindSafe; 5 | 6 | /// Context manages the information flow for an incoming HTTP [`Request`], 7 | /// the application handler, and the generated HTTP [`Response`]. It holds a buffer 8 | /// for log messages, and a hook for deferred tasks to be processed after the [`Response`] is returned. 9 | #[derive(Default)] 10 | pub struct Context { 11 | response: Response, 12 | log_queue: LogQueue, 13 | deferred: Vec>, 14 | internal_error: Option>, 15 | } 16 | 17 | unsafe impl Send for Context {} 18 | 19 | impl Context { 20 | /// Creates response builder 21 | pub fn response(&mut self) -> &mut Response { 22 | &mut self.response 23 | } 24 | 25 | /// Adds a task to the deferred task queue. The task queue uses 26 | /// [event.waitUntil](https://developers.cloudflare.com/workers/runtime-apis/fetch-event) 27 | /// to extend the lifetime of the request event, and runs tasks after the response 28 | /// has been returned to the client. 29 | /// Deferred tasks are often useful for logging and analytics. 30 | pub fn defer(&mut self, task: Box) { 31 | self.deferred.push(task); 32 | } 33 | 34 | /// Returns pending log messages, emptying internal queue. 35 | /// This is used for sending queued messages to an external log service 36 | pub fn take_logs(&mut self) -> Vec { 37 | self.log_queue.take() 38 | } 39 | 40 | /// Returns deferred tasks, emptying internal list 41 | pub(crate) fn take_tasks(&mut self) -> Vec> { 42 | std::mem::take(&mut self.deferred) 43 | } 44 | 45 | /// Returns response, replacing self.response with default 46 | pub(crate) fn take_response(&mut self) -> Response { 47 | std::mem::take(&mut self.response) 48 | } 49 | 50 | /// Adds log to deferred queue 51 | pub fn log(&mut self, e: LogEntry) { 52 | self.log_queue.log(e); 53 | } 54 | 55 | /// Sets the internal error flag, which causes wasm_service to invoke the internal_error_handler 56 | pub fn raise_internal_error(&mut self, e: Box) { 57 | self.internal_error = Some(e); 58 | } 59 | 60 | /// Returns whether the internal error flag has been set 61 | pub fn is_internal_error(&self) -> Option<&dyn std::error::Error> { 62 | self.internal_error.as_deref() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Errors generated by this crate 2 | use std::fmt; 3 | use wasm_bindgen::JsValue; 4 | 5 | /// Errors generated by this crate 6 | /// It's not necessary for users of wasm_service to import this, 7 | /// because Error implements trait std::error::Error. 8 | #[derive(Debug)] 9 | pub enum Error { 10 | /// Error serializing/deserializing request, response, or log messages 11 | Json(serde_json::Error), 12 | 13 | /// Error converting parameters to/from javascript 14 | Js(String), 15 | 16 | /// Error in external http sub-request (via reqwest lib) 17 | Http(reqwest::Error), 18 | 19 | /// Error deserializing asset index 20 | DeserializeAssets(Box), 21 | 22 | /// Invalid header value (contains non-ascii characters) 23 | InvalidHeaderValue(String), 24 | 25 | /// No static asset is available at this path 26 | NoStaticAsset(String), 27 | 28 | /// KV asset not found 29 | #[allow(clippy::upper_case_acronyms)] 30 | KVKeyNotFound(String, u16), 31 | 32 | /// Error received from Cloudflare API while performing KV request 33 | #[allow(clippy::upper_case_acronyms)] 34 | KVApi(reqwest::Error), 35 | 36 | /// Catch-all 37 | Other(String), 38 | } 39 | 40 | impl std::error::Error for Error {} 41 | unsafe impl Send for Error {} 42 | 43 | impl From for Error { 44 | fn from(msg: String) -> Error { 45 | Error::Other(msg) 46 | } 47 | } 48 | 49 | impl From for Error { 50 | fn from(e: serde_json::Error) -> Self { 51 | Error::Json(e) 52 | } 53 | } 54 | 55 | impl From for Error { 56 | fn from(e: reqwest::Error) -> Self { 57 | Error::Http(e) 58 | } 59 | } 60 | 61 | impl From for Error { 62 | fn from(e: JsValue) -> Self { 63 | Error::Js( 64 | e.as_string() 65 | .unwrap_or_else(|| "Javascript error".to_string()), 66 | ) 67 | } 68 | } 69 | 70 | impl fmt::Display for Error { 71 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 72 | write!(f, "{:?}", self) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/httpdate.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// Time in UTC, with conversions to/from u64 and rfc2822 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 5 | pub struct HttpDate(u64); 6 | 7 | /// Convert HttpDate to printable string in rfc2822 format 8 | impl fmt::Display for HttpDate { 9 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 10 | use chrono::{DateTime, NaiveDateTime, Utc}; 11 | 12 | let dt = DateTime::::from_utc(NaiveDateTime::from_timestamp(self.0 as i64, 0), Utc); 13 | fmt::Display::fmt(&dt.to_rfc2822(), f) 14 | } 15 | } 16 | 17 | /// Convert u64 timestamp (seconds since EPOCH in UTC) to HttpDate 18 | impl From for HttpDate { 19 | fn from(utc_sec: u64) -> HttpDate { 20 | HttpDate(utc_sec) 21 | } 22 | } 23 | 24 | /// Convert i64 timestamp (seconds since EPOCH in UTC) to HttpDate 25 | impl From for HttpDate { 26 | fn from(utc_sec: i64) -> HttpDate { 27 | HttpDate(utc_sec as u64) 28 | } 29 | } 30 | 31 | impl std::str::FromStr for HttpDate { 32 | type Err = chrono::format::ParseError; 33 | 34 | /// Parse string to HttpDate 35 | fn from_str(s: &str) -> Result { 36 | let utc_sec = chrono::DateTime::parse_from_rfc2822(s).map(|dt| dt.timestamp() as u64)?; 37 | Ok(HttpDate(utc_sec)) 38 | } 39 | } 40 | 41 | impl HttpDate { 42 | /// Convert HttpDate to i64 timestamp (seconds since EPOCH in UTC) 43 | pub fn timestamp(&self) -> u64 { 44 | self.0 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/js_values.rs: -------------------------------------------------------------------------------- 1 | /// utilities for converting to/from JsValue 2 | /// Specifically, for use in getting params from incoming Request 3 | use wasm_bindgen::JsValue; 4 | 5 | /// Retrieve a string value from map 6 | pub(crate) fn get_map_str(map: &js_sys::Map, key: &str) -> Option { 7 | let val = map.get(&JsValue::from_str(key)); 8 | if !val.is_undefined() { 9 | val.as_string() 10 | } else { 11 | None 12 | } 13 | } 14 | 15 | /// Retrieve Vec from map 16 | pub(crate) fn get_map_bytes(map: &js_sys::Map, key: &str) -> Option> { 17 | let val = map.get(&JsValue::from_str(key)); 18 | if !val.is_undefined() { 19 | let arr = js_sys::Uint8Array::from(val); 20 | Some(arr.to_vec()) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | /// Retrieve headers from map 27 | pub(crate) fn get_map_headers(map: &js_sys::Map, key: &str) -> Option { 28 | let val = map.get(&JsValue::from_str(key)); 29 | if !val.is_undefined() { 30 | Some(web_sys::Headers::from(val)) 31 | } else { 32 | None 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! Base support for wasm service using Confluence Workers 3 | //! 4 | use async_trait::async_trait; 5 | use js_sys::{Function, Reflect}; 6 | use service_logging::{log, LogEntry, LogQueue, Logger, Severity}; 7 | use std::cell::RefCell; 8 | use std::fmt; 9 | use wasm_bindgen::JsValue; 10 | 11 | mod error; 12 | pub use error::Error; 13 | mod method; 14 | pub use method::Method; 15 | mod request; 16 | pub use request::Request; 17 | mod response; 18 | pub use response::{Body, Response}; 19 | mod media_type; 20 | pub use media_type::media_type; 21 | 22 | /// re-export url::Url 23 | pub use url::Url; 24 | 25 | mod context; 26 | pub use context::Context; 27 | mod assets; 28 | pub use assets::StaticAssetHandler; 29 | mod httpdate; 30 | pub(crate) mod js_values; 31 | pub use httpdate::HttpDate; 32 | 33 | /// Logging support for deferred tasks 34 | #[derive(Debug)] 35 | pub struct RunContext { 36 | /// queue of deferred messages 37 | pub log_queue: RefCell, 38 | } 39 | 40 | // workers are single-threaded 41 | unsafe impl Sync for RunContext {} 42 | 43 | impl RunContext { 44 | /// log message (used by log! macro) 45 | pub fn log(&self, entry: LogEntry) { 46 | self.log_queue.borrow_mut().log(entry); 47 | /* 48 | let mut guard = match self.log_queue.lock() { 49 | Ok(guard) => guard, 50 | Err(_poisoned) => { 51 | // lock shouldn't be poisoned because we don't have panics in production wasm, 52 | // so this case shouldn't occur 53 | return; 54 | } 55 | }; 56 | guard.log(entry); 57 | */ 58 | } 59 | } 60 | 61 | /// Runnable trait for deferred tasks 62 | /// Deferred tasks are often useful for logging and analytics. 63 | /// ```rust 64 | /// use std::{rc::Rc,sync::Mutex};; 65 | /// use async_trait::async_trait; 66 | /// use service_logging::{log,Logger,LogQueue,Severity}; 67 | /// use wasm_service::{Runnable,RunContext}; 68 | /// 69 | /// struct Data { s: String } 70 | /// #[async_trait] 71 | /// impl Runnable for Data { 72 | /// async fn run(&self, ctx: &RunContext) { 73 | /// log!(ctx, Severity::Info, msg: format!("Deferred with data: {}", self.s )); 74 | /// } 75 | /// } 76 | /// ``` 77 | #[async_trait] 78 | pub trait Runnable { 79 | /// Execute a deferred task. The task may append 80 | /// logs to `lq` using the [`log`] macro. Logs generated 81 | /// are sent to the log service after all deferred tasks have run. 82 | /// 83 | /// Note that if there is a failure sending logs to the logging service, 84 | /// those log messages (and the error from the send failure) will be unreported. 85 | async fn run(&self, ctx: &RunContext); 86 | } 87 | 88 | /// Generic page error return - doesn't require ctx 89 | #[derive(Clone, Debug)] 90 | pub struct HandlerReturn { 91 | /// status code (default: 200) 92 | pub status: u16, 93 | /// body text 94 | pub text: String, 95 | } 96 | 97 | impl fmt::Display for HandlerReturn { 98 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 99 | write!(f, "({},{})", self.status, self.text) 100 | } 101 | } 102 | 103 | /// Generate handler return "error" 104 | pub fn handler_return(status: u16, text: &str) -> HandlerReturn { 105 | HandlerReturn { 106 | status, 107 | text: text.to_string(), 108 | } 109 | } 110 | 111 | impl Default for HandlerReturn { 112 | fn default() -> Self { 113 | Self { 114 | status: 200, 115 | text: String::default(), 116 | } 117 | } 118 | } 119 | 120 | /// Trait that defines app/service's request handler and router 121 | /// See [rustwasm-service-template](https://github.com/stevelr/rustwasm-service-template/blob/master/src/lib.rs) 122 | /// for a more complete example 123 | /// 124 | ///```rust 125 | /// use service_logging::{Severity::Verbose,log,Logger}; 126 | /// use wasm_service::{Context,Handler,HandlerReturn,Request}; 127 | /// use async_trait::async_trait; 128 | /// struct MyHandler {} 129 | /// #[async_trait(?Send)] 130 | /// impl Handler for MyHandler { 131 | /// /// Process incoming Request 132 | /// async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn> { 133 | /// // log all incoming requests 134 | /// log!(ctx, Verbose, method: req.method(), url: req.url()); 135 | /// match (req.method(), req.url().path()) { 136 | /// (GET, "/hello") => { 137 | /// ctx.response().content_type("text/plain; charset=UTF-8").unwrap() 138 | /// .text("Hello world!"); 139 | /// } 140 | /// _ => { 141 | /// ctx.response().status(404).text("Not Found"); 142 | /// } 143 | /// } 144 | /// Ok(()) 145 | /// } 146 | /// } 147 | ///``` 148 | #[async_trait(?Send)] 149 | pub trait Handler { 150 | /// Implementation of application request handler 151 | async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn>; 152 | } 153 | 154 | /// Configuration parameters for service 155 | /// Parameter E is your crate's error type 156 | pub struct ServiceConfig { 157 | /// Logger 158 | pub logger: Box, 159 | 160 | /// Request handler 161 | pub handlers: Vec>, 162 | 163 | /// how to handle internal errors. This function should modify ctx.response() 164 | /// with results, which, for example, could include rendering a page or sending 165 | /// a redirect. The default implementation returns status 200 with a short text message. 166 | pub internal_error_handler: fn(req: &Request, ctx: &mut Context), 167 | 168 | /// how to handle Not Found (404) responses. This function should modify ctx.response() 169 | /// with results, which, for example, could include rendering a page or sending 170 | /// a redirect. The default implementation returns status 404 with a short text message. 171 | pub not_found_handler: fn(req: &Request, ctx: &mut Context), 172 | } 173 | 174 | impl Default for ServiceConfig { 175 | /// Default construction of ServiceConfig does no logging and handles no requests. 176 | fn default() -> ServiceConfig { 177 | ServiceConfig { 178 | logger: service_logging::silent_logger(), 179 | handlers: Vec::new(), 180 | internal_error_handler: default_internal_error_handler, 181 | not_found_handler: default_not_found_handler, 182 | } 183 | } 184 | } 185 | 186 | struct DeferredData { 187 | tasks: Vec>, 188 | logs: Vec, 189 | logger: Box, 190 | } 191 | 192 | /// Entrypoint for wasm-service. Converts parameters from javascript into [Request], 193 | /// invokes app-specific [Handler](trait.Handler.html), and converts [`Response`] to javascript. 194 | /// Also sends logs to [Logger](https://docs.rs/service-logging/0.3/service_logging/trait.Logger.html) and runs deferred tasks. 195 | pub async fn service_request(req: JsValue, config: ServiceConfig) -> Result { 196 | let mut is_err = false; 197 | let map = js_sys::Map::from(req); 198 | let req = Request::from_js(&map)?; 199 | let mut ctx = Context::default(); 200 | let mut handler_result = Ok(()); 201 | for handler in config.handlers.iter() { 202 | handler_result = handler.handle(&req, &mut ctx).await; 203 | if ctx.is_internal_error().is_some() { 204 | (config.internal_error_handler)(&req, &mut ctx); 205 | is_err = true; 206 | break; 207 | } 208 | // if handler set response, or returned HandlerReturn (which is a response), stop iter 209 | if handler_result.is_err() || !ctx.response().is_unset() { 210 | break; 211 | } 212 | } 213 | if let Err(result) = handler_result { 214 | // Convert HandlerReturn to status/body 215 | ctx.response().status(result.status).text(result.text); 216 | } else if ctx.response().is_unset() { 217 | // If NO handler set a response, it's content not found 218 | // the not-found handler might return a static page or redirect 219 | (config.not_found_handler)(&req, &mut ctx); 220 | } 221 | let response = ctx.take_response(); 222 | if response.get_status() < 200 || response.get_status() > 307 { 223 | is_err = true; 224 | } 225 | let severity = if response.get_status() == 404 { 226 | Severity::Warning 227 | } else if is_err { 228 | Severity::Error 229 | } else { 230 | Severity::Info 231 | }; 232 | log!(ctx, severity, _:"service", method: req.method(), url: req.url(), status: response.get_status()); 233 | if is_err { 234 | // if any error occurred, send logs now; fast path (on success) defers logging 235 | // also, if there was an error, don't execute deferred tasks 236 | let _ = config 237 | .logger 238 | .send("http", ctx.take_logs()) 239 | .await 240 | .map_err(|e| { 241 | ctx.response() 242 | .header("X-service-log-err-ret", e.to_string()) 243 | .unwrap() 244 | }); 245 | } else { 246 | // From incoming request, extract 'event' object, and get ref to its 'waitUntil' function 247 | let js_event = 248 | js_sys::Object::from(check_defined(map.get(&"event".into()), "missing event")?); 249 | let wait_func = Function::from( 250 | Reflect::get(&js_event, &JsValue::from_str("waitUntil")) 251 | .map_err(|_| "event without waitUntil")?, 252 | ); 253 | // this should always return OK (event has waitUntil property) unless api is broken. 254 | let promise = deferred_promise(Box::new(DeferredData { 255 | tasks: ctx.take_tasks(), 256 | logs: ctx.take_logs(), 257 | logger: config.logger, 258 | })); 259 | let _ = wait_func.call1(&js_event, &promise); // todo: handle result 260 | } 261 | Ok(response.into_js()) 262 | } 263 | 264 | /// Default implementation of internal error handler 265 | /// Sets status to 200 and returns a short error message 266 | fn default_internal_error_handler(req: &Request, ctx: &mut Context) { 267 | let error = ctx.is_internal_error(); 268 | log!(ctx, Severity::Error, _:"InternalError", url: req.url(), 269 | error: error.map(|e| e.to_string()).unwrap_or_else(|| String::from("none"))); 270 | ctx.response() 271 | .status(200) 272 | .content_type(mime::TEXT_PLAIN_UTF_8) 273 | .unwrap() 274 | .text("Sorry, an internal error has occurred. It has been logged."); 275 | } 276 | 277 | /// Default implementation of not-found handler. 278 | /// Sets status to 404 and returns a short message "Not Found" 279 | pub fn default_not_found_handler(req: &Request, ctx: &mut Context) { 280 | log!(ctx, Severity::Info, _:"NotFound", url: req.url()); 281 | ctx.response() 282 | .status(404) 283 | .content_type(mime::TEXT_PLAIN_UTF_8) 284 | .unwrap() 285 | .text("Not Found"); 286 | } 287 | 288 | /// Future task that will run deferred. Includes deferred logs plus user-defined tasks. 289 | /// This function contains a rust async wrapped in a Javascript Promise that will be passed 290 | /// to the event.waitUntil function, so it gets processed after response is returned. 291 | fn deferred_promise(args: Box) -> js_sys::Promise { 292 | wasm_bindgen_futures::future_to_promise(async move { 293 | // send first set of logs 294 | if let Err(e) = args.logger.send("http", args.logs).await { 295 | log_log_error(e); 296 | } 297 | // run each deferred task 298 | // let log_queue = Mutex::new(LogQueue::default()); 299 | let log_queue = RefCell::new(LogQueue::default()); 300 | let run_ctx = RunContext { log_queue }; 301 | for t in args.tasks.iter() { 302 | t.run(&run_ctx).await; 303 | } 304 | 305 | // if any logs were generated during processing of deferred tasks, send those 306 | let logs = run_ctx.log_queue.borrow_mut().take(); 307 | if let Err(e) = args.logger.send("http", logs).await { 308 | log_log_error(e); 309 | } 310 | // all done, return nothing 311 | Ok(JsValue::undefined()) 312 | }) 313 | } 314 | 315 | /// Returns javascript value, or Err if undefined 316 | fn check_defined(v: JsValue, msg: &str) -> Result { 317 | if v.is_undefined() { 318 | return Err(JsValue::from_str(msg)); 319 | } 320 | Ok(v) 321 | } 322 | 323 | /// logging fallback: if we can't send to external logger, 324 | /// log to "console" so it can be seen in worker logs 325 | fn log_log_error(e: Box) { 326 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 327 | "Error sending logs: {:?}", 328 | e 329 | ))) 330 | } 331 | -------------------------------------------------------------------------------- /src/media_type.rs: -------------------------------------------------------------------------------- 1 | /// Determines the Media Type (aka MIME) for file based on extension. 2 | /// If type is not known (based on implemented list), returns None. 3 | /// ``` 4 | /// # use wasm_service::media_type; 5 | /// assert_eq!(media_type("index.html"), Some("text/html; charset=utf-8")); 6 | /// ``` 7 | /// 8 | /// All type values are valid utf-8 strings, so it is safe to use unwrap() 9 | /// when setting headers, e.g. 10 | /// ``` 11 | /// # cfg_if::cfg_if!{ if #[cfg(target_arch = "wasm32")] { 12 | /// # use wasm_service::{Response,media_type}; 13 | /// # use reqwest::header::CONTENT_TYPE; 14 | /// # let response = Response::default(); 15 | /// if let Some(mtype) = media_type("index.html") { 16 | /// response.header(CONTENT_TYPE, mtype).unwrap(); 17 | /// } 18 | /// assert_eq!(55, 22); 19 | /// # }} 20 | /// ``` 21 | pub fn media_type(file_path: &str) -> Option<&'static str> { 22 | std::path::Path::new(file_path) 23 | .extension() 24 | .and_then(std::ffi::OsStr::to_str) 25 | .map(ext_to_mime) 26 | .unwrap_or_default() 27 | } 28 | 29 | /// map extension to mime type 30 | // References 31 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types 32 | // https://www.iana.org/assignments/media-types/media-types.xhtml#application 33 | fn ext_to_mime(ext: &str) -> Option<&'static str> { 34 | match ext { 35 | "html" => Some(mime::TEXT_HTML_UTF_8.as_ref()), 36 | "css" => Some(mime::TEXT_CSS.as_ref()), 37 | "js" | "ts" => Some(mime::TEXT_JAVASCRIPT.as_ref()), 38 | "jpg" | "jpeg" => Some(mime::IMAGE_JPEG.as_ref()), 39 | "png" => Some(mime::IMAGE_PNG.as_ref()), 40 | "gif" => Some(mime::IMAGE_GIF.as_ref()), 41 | "toml" => Some("application/toml"), 42 | "yaml" | "yml" => Some("text/x-yaml"), 43 | "json" => Some(mime::APPLICATION_JSON.as_ref()), 44 | "txt" | "py" | "rs" | "hbs" => Some(mime::TEXT_PLAIN.as_ref()), 45 | "md" => Some("text/markdown"), 46 | "wasm" => Some("application/wasm"), 47 | "ico" => Some("image/vnd.microsoft.icon"), 48 | "csv" => Some(mime::TEXT_CSV_UTF_8.as_ref()), 49 | "pdf" => Some(mime::APPLICATION_PDF.as_ref()), 50 | "bin" | "enc" | "dat" | "gz" | "tar" | "z" => Some(mime::APPLICATION_OCTET_STREAM.as_ref()), 51 | _ => None, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/method.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use wasm_bindgen::JsValue; 3 | 4 | /// HTTP Method 5 | #[derive(Clone, Copy, PartialEq, Debug)] 6 | #[allow(clippy::upper_case_acronyms)] 7 | pub enum Method { 8 | /// HTTP GET method 9 | GET, 10 | /// HTTP POST method 11 | POST, 12 | /// HTTP PUT method 13 | PUT, 14 | /// HTTP DELETE method 15 | DELETE, 16 | /// HTTP HEAD method 17 | HEAD, 18 | /// HTTP OPTIONS method 19 | OPTIONS, 20 | } 21 | 22 | impl Method { 23 | /// Converts string to Method 24 | pub fn from(s: &str) -> Result { 25 | Ok(match s { 26 | "GET" | "get" => Method::GET, 27 | "POST" | "post" => Method::POST, 28 | "PUT" | "put" => Method::PUT, 29 | "DELETE" | "delete" => Method::DELETE, 30 | "HEAD" | "head" => Method::HEAD, 31 | "OPTIONS" | "options" => Method::OPTIONS, 32 | _ => return Err(JsValue::from_str("Unsupported http method")), 33 | }) 34 | } 35 | } 36 | 37 | impl fmt::Display for Method { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 39 | write!( 40 | f, 41 | "{}", 42 | match self { 43 | Method::GET => "GET", 44 | Method::POST => "POST", 45 | Method::PUT => "PUT", 46 | Method::DELETE => "DELETE", 47 | Method::HEAD => "HEAD", 48 | Method::OPTIONS => "OPTIONS", 49 | } 50 | ) 51 | } 52 | } 53 | 54 | #[test] 55 | fn method_str() { 56 | // to_string() and from() 57 | assert_eq!(Method::from("GET").unwrap().to_string(), "GET"); 58 | assert_eq!(Method::from("POST").unwrap().to_string(), "POST"); 59 | assert_eq!(Method::from("PUT").unwrap().to_string(), "PUT"); 60 | assert_eq!(Method::from("DELETE").unwrap().to_string(), "DELETE"); 61 | assert_eq!(Method::from("HEAD").unwrap().to_string(), "HEAD"); 62 | assert_eq!(Method::from("OPTIONS").unwrap().to_string(), "OPTIONS"); 63 | 64 | // PartialEq 65 | assert!(Method::from("GET").unwrap() == Method::GET); 66 | 67 | // Debug 68 | assert_eq!(format!("{:?}", Method::PUT), "PUT"); 69 | 70 | // parse error 71 | // moved this to tests/method.rs because it depends on web_sys::JsValue 72 | //assert!(Method::from("none").is_err()); 73 | } 74 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use crate::js_values; 2 | use crate::{Error, Method}; 3 | use serde::de::DeserializeOwned; 4 | use std::borrow::Cow; 5 | use url::Url; 6 | use wasm_bindgen::JsValue; 7 | 8 | /// Incoming HTTP request (to Worker). 9 | #[derive(Debug, Clone)] 10 | pub struct Request { 11 | method: Method, 12 | url: Url, 13 | headers: web_sys::Headers, 14 | body: Option>, 15 | } 16 | unsafe impl Sync for Request {} 17 | 18 | impl Request { 19 | /// Creates Request object representing incoming HTTP request 20 | pub fn new( 21 | method: Method, 22 | url: Url, 23 | headers: web_sys::Headers, 24 | body: Option>, 25 | ) -> Request { 26 | Request { 27 | method, 28 | url, 29 | headers, 30 | body, 31 | } 32 | } 33 | 34 | /// Creates Request from javascript object 35 | pub(crate) fn from_js(map: &js_sys::Map) -> Result { 36 | Ok(Request::new( 37 | Method::from( 38 | &js_values::get_map_str(&map, "method") 39 | .ok_or_else(|| JsValue::from_str("invalid_req.method"))?, 40 | )?, 41 | Url::parse( 42 | &js_values::get_map_str(&map, "url") 43 | .ok_or_else(|| JsValue::from_str("invalid_req.url"))?, 44 | ) 45 | .map_err(|e| JsValue::from_str(&format!("invalid req.url:{}", e.to_string())))?, 46 | js_values::get_map_headers(&map, "headers") 47 | .ok_or_else(|| JsValue::from_str("invalid_req"))?, 48 | js_values::get_map_bytes(&map, "body"), 49 | )) 50 | } 51 | 52 | /// Returns the HTTP method 53 | pub fn method(&self) -> Method { 54 | self.method 55 | } 56 | 57 | /// Returns the parsed url 58 | pub fn url(&self) -> &Url { 59 | &self.url 60 | } 61 | 62 | /// Returns the set of request headers 63 | pub fn headers(&self) -> &web_sys::Headers { 64 | &self.headers 65 | } 66 | 67 | /// Returns the value of the header, or None if the header is not set. 68 | /// Header name search is case-insensitive. 69 | pub fn get_header(&self, name: &str) -> Option { 70 | match self.headers.get(name) { 71 | Ok(v) => v, 72 | Err(_) => None, 73 | } 74 | } 75 | 76 | /// Returns true if the header is set. Name is case-insensitive. 77 | pub fn has_header(&self, name: &str) -> bool { 78 | self.headers.has(name).unwrap_or(false) 79 | } 80 | 81 | /// Returns true if the body is empty 82 | pub fn is_empty(&self) -> bool { 83 | self.body.is_none() || self.body.as_ref().unwrap().is_empty() 84 | } 85 | 86 | /// Returns request body as byte vector, or None if body is empty 87 | pub fn body(&self) -> Option<&Vec> { 88 | self.body.as_ref() 89 | } 90 | 91 | /// Interpret body as json object. 92 | pub fn json(&self) -> Result { 93 | if let Some(vec) = self.body.as_ref() { 94 | Ok(serde_json::from_slice(vec)?) 95 | } else { 96 | Err(Error::Other("body is empty".to_string())) 97 | } 98 | } 99 | 100 | /// Returns the cookie string, if set 101 | pub fn get_cookie_value(&self, cookie_name: &str) -> Option { 102 | self.get_header("cookie") 103 | .map(|cookie| { 104 | (&cookie) 105 | .split(';') 106 | // allow spaces around ';' 107 | .map(|s| s.trim()) 108 | // if name=value, return value 109 | .find_map(|part| cookie_value(part, cookie_name)) 110 | .map(|v| v.to_string()) 111 | }) 112 | .unwrap_or_default() 113 | } 114 | 115 | /// returns the query variable from the url, or None if not found 116 | pub fn get_query_value<'req>(&'req self, key: &'_ str) -> Option> { 117 | self.url() 118 | .query_pairs() 119 | .find(|(k, _)| k == key) 120 | .map(|(_, v)| v) 121 | } 122 | } 123 | 124 | // If 'part' is of the form 'name=value', return value 125 | fn cookie_value<'cookie>(part: &'cookie str, name: &str) -> Option<&'cookie str> { 126 | if part.len() > name.len() { 127 | let (left, right) = part.split_at(name.len()); 128 | if left == name && right.starts_with('=') { 129 | return Some(&right[1..]); 130 | } 131 | } 132 | None 133 | } 134 | 135 | #[test] 136 | // test cookie_value function. Additional tests of Request are in tests/request.rs 137 | fn test_cookie_value() { 138 | // short value 139 | assert_eq!(cookie_value("x=y", "x"), Some("y")); 140 | 141 | // longer value 142 | assert_eq!(cookie_value("foo=bar", "foo"), Some("bar")); 143 | 144 | // missing value 145 | assert_eq!(cookie_value("x=y", "z"), None); 146 | 147 | // empty value 148 | assert_eq!(cookie_value("foo=", "foo"), Some("")); 149 | } 150 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use bytes::Bytes; 3 | use serde::Serialize; 4 | use std::fmt; 5 | use wasm_bindgen::JsValue; 6 | 7 | /// Worker response for HTTP requests. 8 | /// The Response is created/accessed from `ctx.response()` and has a builder-like api. 9 | #[derive(Debug)] 10 | pub struct Response { 11 | status: u16, 12 | headers: Option, 13 | body: Body, 14 | unset: bool, 15 | } 16 | 17 | impl Default for Response { 18 | fn default() -> Self { 19 | Self { 20 | status: 200, 21 | headers: None, 22 | body: Body::from(Bytes::new()), 23 | unset: true, 24 | } 25 | } 26 | } 27 | 28 | impl Response { 29 | /// Sets response status 30 | pub fn status(&mut self, status: u16) -> &mut Self { 31 | self.status = status; 32 | self.unset = false; 33 | self 34 | } 35 | 36 | /// Sets response body to the binary data 37 | pub fn body>(&mut self, body: T) -> &mut Self { 38 | self.body = body.into(); 39 | self.unset = false; 40 | self 41 | } 42 | 43 | /// Sets response body to value serialized as json, and sets content-type to application/json 44 | pub fn json(&mut self, value: &T) -> Result<&mut Self, Error> { 45 | use mime::APPLICATION_JSON; 46 | self.body = serde_json::to_vec(value)?.into(); 47 | self.content_type(APPLICATION_JSON).unwrap(); 48 | self.unset = false; 49 | Ok(self) 50 | } 51 | 52 | /// Sets response body to the text string, encoded as utf-8 53 | pub fn text>(&mut self, text: T) -> &mut Self { 54 | let str_val = text.into(); 55 | self.body = str_val.into(); 56 | self.unset = false; 57 | self 58 | } 59 | 60 | /// Sets a header for this response 61 | pub fn header, V: AsRef>( 62 | &mut self, 63 | key: K, 64 | val: V, 65 | ) -> Result<&mut Self, Error> { 66 | if self.headers.is_none() { 67 | self.headers = Some(web_sys::Headers::new().unwrap()); 68 | } 69 | if let Some(ref mut headers) = self.headers { 70 | headers.set(key.as_ref(), val.as_ref())?; 71 | } 72 | Ok(self) 73 | } 74 | 75 | /// Sets response content type 76 | pub fn content_type>(&mut self, ctype: T) -> Result<&mut Self, Error> { 77 | self.header(reqwest::header::CONTENT_TYPE, ctype)?; 78 | Ok(self) 79 | } 80 | 81 | /// Returns the status of this response 82 | pub fn get_status(&self) -> u16 { 83 | self.status 84 | } 85 | 86 | /// Returns body of this response. 87 | pub fn get_body(&self) -> &[u8] { 88 | &self.body.inner.as_ref() 89 | } 90 | 91 | /// Returns headers for this response, or None if no headers have been set 92 | pub fn get_headers(&self) -> Option<&web_sys::Headers> { 93 | self.headers.as_ref() 94 | } 95 | 96 | /// Returns true if the body is empty 97 | pub fn is_empty(&self) -> bool { 98 | self.body.is_empty() 99 | } 100 | 101 | /// Converts Response to JsValue 102 | /// This is destructive to self (removes headers) and is used after 103 | /// application request handling has completed. 104 | pub(crate) fn into_js(mut self) -> JsValue { 105 | let map = js_sys::Map::new(); 106 | map.set( 107 | &JsValue::from_str("status"), 108 | &JsValue::from_f64(self.status as f64), 109 | ); 110 | map.set( 111 | &JsValue::from_str("body"), 112 | &js_sys::Uint8Array::from(self.body.inner.as_ref()), 113 | ); 114 | if self.headers.is_some() { 115 | let headers = std::mem::take(&mut self.headers).unwrap(); 116 | map.set(&JsValue::from_str("headers"), &JsValue::from(headers)); 117 | } else { 118 | map.set( 119 | &JsValue::from_str("headers"), 120 | &JsValue::from(web_sys::Headers::new().unwrap()), 121 | ); 122 | } 123 | JsValue::from(map) 124 | } 125 | 126 | /// True if the response has not been filled in (none of status(), text() or body() has been 127 | /// called). (even if status() is called with 200 status or body is set to empty) 128 | /// This could be used as a flag for chained handlers to determine whether a previous 129 | /// handler has filled in the response yet. 130 | /// Setting headers (including content_type or user_agent) does not mark the request "set" 131 | /// This is so that headers can be set at the top of a handler, before errors may occur 132 | pub fn is_unset(&self) -> bool { 133 | self.unset 134 | } 135 | } 136 | 137 | /// The body of a `Response`. 138 | // this is adapted from reqwest::wasm::Body, which is used in requests 139 | pub struct Body { 140 | inner: Bytes, 141 | } 142 | 143 | impl Body { 144 | /// True if the body is empty 145 | pub fn is_empty(&self) -> bool { 146 | self.inner.is_empty() 147 | } 148 | } 149 | 150 | impl From for Body { 151 | #[inline] 152 | fn from(bytes: Bytes) -> Body { 153 | Body { inner: bytes } 154 | } 155 | } 156 | 157 | impl From> for Body { 158 | #[inline] 159 | fn from(vec: Vec) -> Body { 160 | Body { inner: vec.into() } 161 | } 162 | } 163 | 164 | impl From<&'static [u8]> for Body { 165 | #[inline] 166 | fn from(s: &'static [u8]) -> Body { 167 | Body { 168 | inner: Bytes::from_static(s), 169 | } 170 | } 171 | } 172 | 173 | impl From for Body { 174 | #[inline] 175 | fn from(s: String) -> Body { 176 | Body { inner: s.into() } 177 | } 178 | } 179 | 180 | impl From<&'static str> for Body { 181 | #[inline] 182 | fn from(s: &'static str) -> Body { 183 | s.as_bytes().into() 184 | } 185 | } 186 | 187 | impl fmt::Debug for Body { 188 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 189 | f.debug_struct("Body").finish() 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/context.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | 3 | use wasm_bindgen_test::*; 4 | use wasm_service::Context; 5 | 6 | #[wasm_bindgen_test] 7 | fn response_defaults() { 8 | let mut ctx = crate::Context::default(); 9 | assert_eq!(ctx.response().get_status(), 200); 10 | assert_eq!(ctx.response().get_body().len(), 0); 11 | assert_eq!(ctx.response().is_empty(), true); 12 | } 13 | 14 | #[wasm_bindgen_test] 15 | fn response_unset_status() { 16 | let mut ctx = crate::Context::default(); 17 | assert_eq!(ctx.response().is_unset(), true); 18 | 19 | ctx.response().status(200); // any status change, even 200 should make unset false 20 | assert_eq!(ctx.response().is_unset(), false); 21 | } 22 | 23 | #[wasm_bindgen_test] 24 | fn response_unset_body() { 25 | let mut ctx = crate::Context::default(); 26 | assert_eq!(ctx.response().is_unset(), true); 27 | 28 | ctx.response().body(""); // any body change, even empty, should make unset false 29 | assert_eq!(ctx.response().is_unset(), false); 30 | } 31 | 32 | #[wasm_bindgen_test] 33 | fn response_body_empty() { 34 | let mut ctx = Context::default(); 35 | 36 | assert_eq!(ctx.response().is_empty(), true); 37 | ctx.response().body("x"); 38 | assert_eq!(ctx.response().is_empty(), false); 39 | } 40 | 41 | #[wasm_bindgen_test] 42 | fn response_body_into() { 43 | let mut ctx = Context::default(); 44 | 45 | // from &'static str 46 | ctx.response().body("hello"); 47 | assert_eq!(ctx.response().get_body(), b"hello"); 48 | 49 | // from Vec 50 | let v: Vec = vec![1, 1, 2, 3, 5, 8]; 51 | ctx.response().body(v.clone()); 52 | assert_eq!(ctx.response().get_body(), &v); 53 | 54 | // from Bytes 55 | let buf = bytes::Bytes::from_static(b"xyz"); 56 | ctx.response().body(buf); 57 | assert_eq!(ctx.response().get_body(), b"xyz"); 58 | 59 | // from &'static [u8] 60 | let static_buf: &'static [u8] = b"alice"; 61 | ctx.response().body(static_buf); 62 | assert_eq!(ctx.response().get_body(), static_buf); 63 | } 64 | 65 | #[wasm_bindgen_test] 66 | fn response_text() { 67 | let mut ctx = Context::default(); 68 | ctx.response().status(201).text("hello"); 69 | 70 | assert_eq!(ctx.response().get_status(), 201); 71 | assert_eq!(&ctx.response().get_body(), b"hello"); 72 | assert_eq!(ctx.response().is_empty(), false); 73 | } 74 | 75 | #[wasm_bindgen_test] 76 | fn response_bin() { 77 | let mut ctx = Context::default(); 78 | ctx.response().status(202).body("bytes"); 79 | 80 | assert_eq!(ctx.response().get_status(), 202); 81 | assert_eq!(&ctx.response().get_body(), b"bytes"); 82 | assert_eq!(ctx.response().is_empty(), false); 83 | } 84 | 85 | #[wasm_bindgen_test] 86 | fn response_headers() { 87 | let mut ctx = Context::default(); 88 | ctx.response() 89 | .header("Content-Type", "application/json") 90 | .expect("set-header"); 91 | 92 | let sv = ctx 93 | .response() 94 | .get_headers() 95 | .unwrap() 96 | .get("Content-Type") 97 | .expect("get header"); 98 | assert!(sv.is_some(), "is-defined content-type"); 99 | assert_eq!(sv.unwrap(), "application/json", "content-type value"); 100 | } 101 | -------------------------------------------------------------------------------- /tests/method.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | 3 | use wasm_bindgen_test::*; 4 | use wasm_service::Method; 5 | 6 | // other method tests are in src/method.rs 7 | // This has to be here because it depends on web-sys 8 | #[wasm_bindgen_test] 9 | fn method_parse() { 10 | assert!(Method::from("HEAD").is_ok()); 11 | assert!(Method::from("none").is_err()); 12 | } 13 | -------------------------------------------------------------------------------- /tests/request.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | 3 | #[cfg(target_arch = "wasm32")] 4 | mod test { 5 | use wasm_bindgen_test::*; 6 | use wasm_service::{Method, Request, Url}; 7 | 8 | #[wasm_bindgen_test] 9 | fn req_method() { 10 | let req = Request::new( 11 | Method::POST, 12 | Url::parse("https://www.example.com").unwrap(), 13 | web_sys::Headers::new().unwrap(), 14 | None, 15 | ); 16 | 17 | assert_eq!(req.method(), Method::POST); 18 | 19 | let req = Request::new( 20 | Method::DELETE, 21 | Url::parse("https://www.example.com").unwrap(), 22 | web_sys::Headers::new().unwrap(), 23 | None, 24 | ); 25 | assert_eq!(req.method(), Method::DELETE); 26 | } 27 | 28 | #[wasm_bindgen_test] 29 | fn req_url() { 30 | let req = Request::new( 31 | Method::GET, 32 | Url::parse("https://www.example.com").unwrap(), 33 | web_sys::Headers::new().unwrap(), 34 | None, 35 | ); 36 | 37 | assert_eq!(&req.url().host().unwrap().to_string(), "www.example.com"); 38 | } 39 | 40 | #[wasm_bindgen_test] 41 | fn req_headers() { 42 | let headers = web_sys::Headers::new().expect("new"); 43 | headers.set("Content-Type", "application/json").expect("ok"); 44 | headers.set("X-Custom-Shape", "round").expect("ok"); 45 | 46 | let req = Request::new( 47 | Method::GET, 48 | Url::parse("https://www.example.com").unwrap(), 49 | headers, 50 | None, 51 | ); 52 | 53 | // has_header, case-insensitive, success 54 | assert_eq!(req.has_header("content-type"), true); 55 | 56 | // has_header, non-existent 57 | assert_eq!(req.has_header("not-here"), false); 58 | 59 | // get_header, success 60 | assert_eq!(&req.get_header("content-type").unwrap(), "application/json"); 61 | 62 | // get_header, non-existent 63 | assert_eq!(req.get_header("not-here"), None); 64 | } 65 | 66 | #[wasm_bindgen_test] 67 | fn req_body() { 68 | let ascii_text = "hello-world"; 69 | 70 | let req = Request::new( 71 | Method::GET, 72 | Url::parse("https://www.example.com").unwrap(), 73 | web_sys::Headers::new().unwrap(), 74 | Some(ascii_text.as_bytes().to_vec()), 75 | ); 76 | 77 | assert_eq!(req.body().unwrap(), ascii_text.as_bytes()); 78 | 79 | let body_bin = vec![0, 1, 2, 3]; 80 | 81 | let req = Request::new( 82 | Method::GET, 83 | Url::parse("https://www.example.com").unwrap(), 84 | web_sys::Headers::new().unwrap(), 85 | Some(body_bin), 86 | ); 87 | let body = req.body().unwrap(); 88 | assert_eq!(body.len(), 4); 89 | assert_eq!(body[1], 1); 90 | } 91 | 92 | #[wasm_bindgen_test] 93 | fn req_query() { 94 | let req = Request::new( 95 | Method::GET, 96 | Url::parse("https://www.example.com?fruit=apple&shape=round").unwrap(), 97 | web_sys::Headers::new().unwrap(), 98 | None, 99 | ); 100 | 101 | assert_eq!(req.get_query_value("fruit").unwrap(), "apple"); 102 | assert_eq!(req.get_query_value("shape").unwrap(), "round"); 103 | assert_eq!(req.get_query_value("size"), None); 104 | } 105 | 106 | #[wasm_bindgen_test] 107 | fn req_cookie() { 108 | let headers = web_sys::Headers::new().expect("new"); 109 | headers.set("Cookie", "foo=bar;color=green").expect("ok"); 110 | 111 | let req = Request::new( 112 | Method::GET, 113 | Url::parse("https://www.example.com").unwrap(), 114 | headers, 115 | None, 116 | ); 117 | 118 | assert_eq!(&req.get_cookie_value("foo").unwrap(), "bar"); 119 | assert_eq!(req.get_cookie_value("bar"), None); 120 | assert_eq!(&req.get_cookie_value("color").unwrap(), "green"); 121 | 122 | // test parsing of cookie with spaces around ';' 123 | let headers = web_sys::Headers::new().expect("new"); 124 | headers 125 | .set("Cookie", "foo=bar ; color=green ; bar=baz") 126 | .expect("ok"); 127 | let req = Request::new( 128 | Method::GET, 129 | Url::parse("https://www.example.com").unwrap(), 130 | headers, 131 | None, 132 | ); 133 | assert_eq!(&req.get_cookie_value("foo").unwrap(), "bar"); // after 134 | assert_eq!(&req.get_cookie_value("color").unwrap(), "green"); // before and after 135 | assert_eq!(&req.get_cookie_value("bar").unwrap(), "baz"); // before 136 | } 137 | } 138 | --------------------------------------------------------------------------------