├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── appveyor.yml ├── build ├── apache │ ├── LICENSE │ ├── NOTICE │ └── mime.types ├── main.rs └── mime.rs ├── examples ├── filters.rs ├── handler_storage.rs ├── handler_storage │ ├── css │ │ └── style.css │ └── page.html ├── hello_world.rs ├── post.rs ├── post │ ├── form.html │ └── page.html ├── tiny_hello.rs └── todo.rs ├── scripts ├── cargo.sh ├── changelog.sh ├── id_rsa.enc ├── test_features.sh ├── travis-doc-upload.cfg └── upload_doc.sh ├── src ├── context │ ├── body.rs │ ├── hypermedia.rs │ ├── maybe_utf8.rs │ ├── mod.rs │ └── parameters.rs ├── file.rs ├── filter.rs ├── handler │ ├── method_router.rs │ ├── mod.rs │ ├── or_else.rs │ ├── routing.rs │ ├── status_router.rs │ ├── tree_router.rs │ └── variables.rs ├── lib.rs ├── macros.rs ├── net.rs ├── response.rs ├── server │ ├── config.rs │ ├── instance.rs │ └── mod.rs └── utils.rs └── version.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | *.rlib 3 | doc 4 | examples/*/main 5 | rustful-test 6 | target 7 | Cargo.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | cache: 4 | directories: 5 | - $HOME/.cargo 6 | rust: 7 | - stable 8 | - beta 9 | - nightly 10 | os: 11 | - linux 12 | - osx 13 | branches: 14 | only: 15 | - master 16 | - auto 17 | script: 18 | - cargo build -v --features strict 19 | - cargo test -v --features strict 20 | - bash scripts/test_features.sh 21 | - cargo doc 22 | after_success: 23 | - sh scripts/upload_doc.sh -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 0.9.0 - 2016-06-16 4 | 5 | * [#114][114]: Update hyper, url and anymap. 6 | * [#113][113]: Change the default content type to text/html. Closes [#107][107]. 7 | * [#112][112]: Bump multipart dependency to 0.7. 8 | 9 | ## Version 0.8.1 - 2016-06-10 10 | 11 | * [#111][111]: Use the runtime value of $OUT_DIR in build script. 12 | * [#103][103]: Fix some Clippy warnings. 13 | 14 | ## Version 0.8.0 - 2016-03-25 15 | 16 | * [#101][101]: Bypass the prompt in multirust-rs. 17 | 18 | ## Version 0.7.0 - 2016-03-09 19 | 20 | * [#96][96]: Router composition. Closes [#86][86]. 21 | * [#99][99]: Update dependencies. Closes [#84][84]. 22 | * [#92][92]: Minor todo example refactoring. 23 | * [#91][91]: Cleanup some warnings from Clippy. 24 | 25 | ## Version 0.6.1 - 2016-01-12 26 | 27 | * [#90][90]: Edit insert_routes! to avoid type ascription conflicts, and some fixes. 28 | 29 | ## Version 0.6.0 - 2015-11-24 30 | 31 | * [#81][81]: Remove the extension traits for BodyReader. Closes [#76][76]. 32 | * [#82][82]: Use multirust-rs to install Rust on AppVeyor. Closes [#77][77]. 33 | * [#79][79]: Give the user control over the server's keep-alive policy. 34 | * [#78][78]: Move hypermedia responsibility from router to handlers. 35 | * [#75][75]: Remove the logging system in favor of the Log crate. Closes [#71][71]. 36 | * [#74][74]: Implement Handler for Arc. Closes [#70][70]. 37 | * [#73][73]: Use check_path in the send_file[_with_mime] examples. Closes [#72][72]. 38 | 39 | ## Version 0.5.0 - 2015-09-19 40 | 41 | * [#69][69]: Get rid of some of the rough edges regarding MaybeUtf8. 42 | * [#67][67]: Force connections to close if the thread pool is too congested. Closes [#65][65]. 43 | * [#68][68]: Remove ICE hacks and set the oldest tested Rust version to 1.3. Closes [#17][17]. 44 | * [#66][66]: Automatically gather the features to be tested. 45 | * [#64][64]: Make wildcards work as path variables. 46 | * [#63][63]: Restructure the server module. 47 | * [#62][62]: Move file sending code to Response. 48 | * [#61][61]: Add an example of a quite minimalistic server. 49 | 50 | ## Version 0.4.0 - 2015-08-17 51 | 52 | * [#59][59]: Preserve non-UTF-8 data and handle * requests. 53 | * [#60][60]: Update unicase to 1.0. 54 | * [#56][56]: Implement a Parameters type. Closes [#55][55]. 55 | * [#58][58]: Test more and forbid warnings and missing docs. 56 | 57 | ## Version 0.3.1 - 2015-07-30 58 | 59 | * [#50][50]: Implement support for multipart requests. Closes [#49][49]. 60 | * [#52][52]: Bump Rust version for testing on Windows to 1.1.0. 61 | 62 | ## Version 0.3.0 - 2015-07-10 63 | 64 | * [#48][48]: Revert unicase reexport and add a todo example. 65 | * [#47][47]: Opt into container based Travis tests. 66 | * [#45][45]: Fixed size responses. Closes [#37][37]. 67 | * [#39][39]: Update to Hyper 0.6. 68 | 69 | ## Version 0.2.2 - 2015-07-05 70 | 71 | * [#44][44]: Enhance the insert_routes! macro. 72 | * [#43][43]: Write more documentation for Context and friends. Closes [#41][41], [#42][42]. 73 | * [#40][40]: Add a Gitter chat badge to README.md. 74 | 75 | ## Version 0.2.1 - 2015-06-29 76 | 77 | * [#38][38]: Add helper for file loading. Closes [#26][26]. 78 | 79 | ## Version 0.2.0 - 2015-06-23 80 | 81 | 82 | ## Version 0.1.1 - 2015-05-16 83 | 84 | [114]: https://github.com/Ogeon/rustful/pull/114 85 | [113]: https://github.com/Ogeon/rustful/pull/113 86 | [112]: https://github.com/Ogeon/rustful/pull/112 87 | [111]: https://github.com/Ogeon/rustful/pull/111 88 | [103]: https://github.com/Ogeon/rustful/pull/103 89 | [101]: https://github.com/Ogeon/rustful/pull/101 90 | [96]: https://github.com/Ogeon/rustful/pull/96 91 | [99]: https://github.com/Ogeon/rustful/pull/99 92 | [92]: https://github.com/Ogeon/rustful/pull/92 93 | [91]: https://github.com/Ogeon/rustful/pull/91 94 | [90]: https://github.com/Ogeon/rustful/pull/90 95 | [81]: https://github.com/Ogeon/rustful/pull/81 96 | [82]: https://github.com/Ogeon/rustful/pull/82 97 | [79]: https://github.com/Ogeon/rustful/pull/79 98 | [78]: https://github.com/Ogeon/rustful/pull/78 99 | [75]: https://github.com/Ogeon/rustful/pull/75 100 | [74]: https://github.com/Ogeon/rustful/pull/74 101 | [73]: https://github.com/Ogeon/rustful/pull/73 102 | [69]: https://github.com/Ogeon/rustful/pull/69 103 | [67]: https://github.com/Ogeon/rustful/pull/67 104 | [68]: https://github.com/Ogeon/rustful/pull/68 105 | [66]: https://github.com/Ogeon/rustful/pull/66 106 | [64]: https://github.com/Ogeon/rustful/pull/64 107 | [63]: https://github.com/Ogeon/rustful/pull/63 108 | [62]: https://github.com/Ogeon/rustful/pull/62 109 | [61]: https://github.com/Ogeon/rustful/pull/61 110 | [59]: https://github.com/Ogeon/rustful/pull/59 111 | [60]: https://github.com/Ogeon/rustful/pull/60 112 | [56]: https://github.com/Ogeon/rustful/pull/56 113 | [58]: https://github.com/Ogeon/rustful/pull/58 114 | [50]: https://github.com/Ogeon/rustful/pull/50 115 | [52]: https://github.com/Ogeon/rustful/pull/52 116 | [48]: https://github.com/Ogeon/rustful/pull/48 117 | [47]: https://github.com/Ogeon/rustful/pull/47 118 | [45]: https://github.com/Ogeon/rustful/pull/45 119 | [39]: https://github.com/Ogeon/rustful/pull/39 120 | [44]: https://github.com/Ogeon/rustful/pull/44 121 | [43]: https://github.com/Ogeon/rustful/pull/43 122 | [40]: https://github.com/Ogeon/rustful/pull/40 123 | [38]: https://github.com/Ogeon/rustful/pull/38 124 | [107]: https://github.com/Ogeon/rustful/issues/107 125 | [86]: https://github.com/Ogeon/rustful/issues/86 126 | [84]: https://github.com/Ogeon/rustful/issues/84 127 | [76]: https://github.com/Ogeon/rustful/issues/76 128 | [77]: https://github.com/Ogeon/rustful/issues/77 129 | [71]: https://github.com/Ogeon/rustful/issues/71 130 | [70]: https://github.com/Ogeon/rustful/issues/70 131 | [72]: https://github.com/Ogeon/rustful/issues/72 132 | [65]: https://github.com/Ogeon/rustful/issues/65 133 | [17]: https://github.com/Ogeon/rustful/issues/17 134 | [55]: https://github.com/Ogeon/rustful/issues/55 135 | [49]: https://github.com/Ogeon/rustful/issues/49 136 | [37]: https://github.com/Ogeon/rustful/issues/37 137 | [41]: https://github.com/Ogeon/rustful/issues/41 138 | [42]: https://github.com/Ogeon/rustful/issues/42 139 | [26]: https://github.com/Ogeon/rustful/issues/26 140 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustful" 3 | version = "0.9.0" #automatically updated 4 | authors = ["Erik Hedvall "] 5 | exclude = ["scripts/*", "examples/*", ".travis.yml", "appveyor.yml", ".gitignore", "CHANGELOG.md", "version.sh"] 6 | description = "A light HTTP framework, with some REST-like features and the ambition of being simple, modular and non-intrusive." 7 | documentation = "http://ogeon.github.io/docs/rustful/master/rustful/index.html" 8 | repository = "https://github.com/Ogeon/rustful" 9 | readme = "README.md" 10 | keywords = ["web", "rest", "framework", "http", "routing"] 11 | license = "MIT OR Apache-2.0" 12 | build = "build/main.rs" 13 | 14 | [lib] 15 | name = "rustful" 16 | path = "src/lib.rs" 17 | 18 | [features] 19 | default = ["multipart"] 20 | 21 | #internal 22 | benchmark = [] 23 | strict = [] 24 | 25 | [dependencies] 26 | time = "0.1" 27 | url = "1" 28 | anymap = "0.12" 29 | phf = "0.7" 30 | num_cpus = "1" 31 | log = "0.4" 32 | 33 | [dependencies.hyper] 34 | version = "0.10" 35 | 36 | [dependencies.multipart] 37 | #feature 38 | version = "0.14" 39 | default-features = false 40 | features = ["server"] 41 | optional = true 42 | 43 | [dev-dependencies] 44 | serde = "1.0" 45 | serde_derive = "1.0" 46 | serde_json = "1.0" 47 | unicase = "1.4" 48 | mime = "0.3" 49 | env_logger = "0.5" 50 | 51 | [build-dependencies] 52 | phf_codegen = "0.7" 53 | -------------------------------------------------------------------------------- /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 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Erik Hedvall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rustful 2 | ======= 3 | 4 | [![Join the chat at https://gitter.im/Ogeon/rustful](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Ogeon/rustful?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | [![Build Status](https://travis-ci.org/Ogeon/rustful.png?branch=master)](https://travis-ci.org/Ogeon/rustful) 7 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/6a95paoex0eptbgn/branch/master?svg=true)](https://ci.appveyor.com/project/Ogeon/rustful/branch/master) 8 | 9 | A light HTTP framework for Rust, with REST-like features. The main purpose 10 | of Rustful is to create a simple, modular and non-intrusive foundation for 11 | HTTP applications. It has a mainly stateless structure, which naturally allows 12 | it to run both as one single server and as multiple instances in a cluster. 13 | 14 | Some of the features are: 15 | 16 | * Generic response handlers. Just use a function or implement the Handler trait. 17 | * Some handy macros reduces the risk for typos and makes life easier. 18 | * Variables in routes, that can capture parts of the requested path. 19 | * Pluggable request and response filtering. 20 | 21 | [Online documentation](http://ogeon.github.io/docs/rustful/master/rustful/index.html). 22 | 23 | # Getting Started 24 | 25 | ## Cargo.toml Entries 26 | 27 | Add the following lines to your `Cargo.toml` file: 28 | 29 | ```toml 30 | [dependencies] 31 | rustful = "0.9" 32 | ``` 33 | 34 | ### Cargo Features 35 | 36 | Some parts of Rustful can be toggled using Cargo features: 37 | 38 | * `ssl` - Enable SSL, and thereby HTTPS. Enabled by default. 39 | * `multipart` - Enable parsing of `multipart/form-data` requests. Enabled by default. 40 | 41 | ### Using SSL 42 | 43 | Rustful support SSL (HTTPS), but does not provide the actual SSL connection. 44 | It's however compatible with anything that's made for the same Hyper version, 45 | so all you have to do is find the one that suits your needs and plug it into 46 | the server: 47 | 48 | ```rust 49 | let server_result = Server { 50 | handlers: router, 51 | host: 8080.into(), 52 | ..Server::default() 53 | }.run_https(my_ssl_server); 54 | ``` 55 | 56 | ## Write Your Server 57 | 58 | Here is a simple example of what a simple project could look like. Visit 59 | `http://localhost:8080` or `http://localhost:8080/Olivia` (if your name is 60 | Olivia) to try it. 61 | 62 | ```rust 63 | //Include macros to be able to use `insert_routes!`. 64 | #[macro_use] 65 | extern crate log; 66 | extern crate env_logger; 67 | 68 | extern crate rustful; 69 | 70 | use std::error::Error; 71 | 72 | use rustful::{Server, Context, Response, DefaultRouter}; 73 | 74 | fn say_hello(context: Context, response: Response) { 75 | //Get the value of the path variable `:person`, from below. 76 | let person = match context.variables.get("person") { 77 | Some(name) => name, 78 | None => "stranger".into() 79 | }; 80 | 81 | //Use the name from the path variable to say hello. 82 | response.send(format!("Hello, {}!", person)); 83 | } 84 | 85 | fn main() { 86 | env_logger::init().unwrap(); 87 | 88 | //Create a DefaultRouter and fill it with handlers. 89 | let mut router = DefaultRouter::::new(); 90 | router.build().many(|mut node| { 91 | //Handle requests for root... 92 | node.then().on_get(say_hello); 93 | 94 | //...and one level below. 95 | //`:person` is a path variable and it will be accessible in the handler. 96 | node.path(":person").then().on_get(say_hello); 97 | }); 98 | 99 | //Build and run the server. 100 | let server_result = Server { 101 | handlers: router, 102 | 103 | //Turn a port number into an IPV4 host address (0.0.0.0:8080 in this case). 104 | host: 8080.into(), 105 | 106 | //Use default values for everything else. 107 | ..Server::default() 108 | }.run(); 109 | 110 | match server_result { 111 | Ok(_server) => {}, 112 | Err(e) => error!("could not start server: {}", e.description()) 113 | } 114 | } 115 | ``` 116 | 117 | ## Contributing 118 | 119 | Contributions are always welcome, even if it's a small typo fix (or maybe I 120 | should say "especially typo fixes"). You can fork the project and open a pull 121 | request with your changes, or create an issue if you just want to report or 122 | request something. Are you not sure about how to implement your change? Is it 123 | still a work in progress? Don't worry. You can still open a pull request where 124 | we can discuss it and do it step by step. 125 | 126 | New features are as welcome as fixes, so pull requests and proposals with 127 | enhancements are very much appreciated, but please explain your feature and 128 | give a good motivation to why it should be included. It makes things much 129 | easier, both for reviewing the feature and for those who are not as familiar 130 | with how things work. You can always open an issue where we can discuss the 131 | feature and see if it should be included. Asking is better than assuming! 132 | 133 | ### Testing 134 | 135 | Rustful is tested on Linux, using Travis, and on Windows, using AppVeyor and a 136 | pull request will not be approved unless it passes these tests. It is 137 | therefore a good idea to run tests locally, before pushing your changes, so here 138 | is a small list of useful commands: 139 | 140 | * `cargo test` - Basic unit, documentation and compile tests. 141 | * `cargo build --no-default-features` - Check if the most minimal version of Rustful builds. 142 | * `cargo build --no-default-features --features "feature1 feature2"` - Check if Rustful with only `feature1` and `feature2` enabled builds. 143 | * `cargo run --example example_name` - check if the example `example_name` behaves as expected (see the `example` directory). 144 | 145 | Travis and AppVeyor will run the tests with the `strict` feature enabled. This 146 | turns warnings and missing documentation into compile errors, which may be 147 | harsh, but it's for the sake of the user. Everything should have a description 148 | and it's not nice to see warnings from your dependencies when you are 149 | compiling your project, right? It's therefore recommend that you run your own 150 | tests with the `strict` feature enabled before pushing, just to see if you 151 | missed something. 152 | 153 | ### Automatic Feature Testing 154 | 155 | User facing Cargo features are automatically gathered from `Cargo.toml` and 156 | tested one at the time, using `scripts/test_features.sh`. The lack of public 157 | and private features forces us to use a special annotation to differ between 158 | internal and user facing feature. Here is an simple example snippet of how the 159 | `Cargo.toml` is expected to look: 160 | 161 | ```toml 162 | #... 163 | 164 | [features] 165 | default = ["feature_a", "feature_b"] 166 | feature_a = ["feature_c"] 167 | feature_b = [] 168 | 169 | #internal 170 | feature_c = [] 171 | 172 | [dependencies.optional_lib] 173 | #feature 174 | optional=true 175 | 176 | #... 177 | ``` 178 | 179 | Features that are supposed to be available to the user has to be declared 180 | before the `#internal` comment. This will tell the test script that these are 181 | supposed to be tested. 182 | 183 | Dependency libraries can also be features, so we have to annotate these as 184 | well. Each dependency that is supposed to work as a user facing feature will 185 | need a `#feature` comment somewhere within its declaration. This will only 186 | work with features that are declared using the above form, and not the 187 | `feature_lib = { ... }` form. 188 | 189 | ## License 190 | 191 | Licensed under either of 192 | 193 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 194 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 195 | 196 | at your option. 197 | 198 | ### Contribution 199 | 200 | Unless you explicitly state otherwise, any contribution intentionally submitted 201 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 202 | additional terms or conditions. 203 | 204 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | branches: 3 | only: 4 | - master 5 | skip_tags: true 6 | platform: x64 7 | os: MinGW 8 | install: 9 | - cmd: SET PATH=C:\MINGW\bin\;C:\MINGW\msys\1.0\bin\;C:\Users\appveyor\.multirust\toolchains\stable\bin\;%PATH% 10 | - ps: Start-FileDownload "https://github.com/rust-lang-nursery/multirust-rs-binaries/raw/master/i686-pc-windows-gnu/multirust-setup.exe" 11 | - multirust-setup -y -v 12 | - rustc --version 13 | - cargo --version 14 | build: false 15 | test_script: 16 | - cargo build -v --features strict 17 | - cargo test --lib -v --features strict 18 | - bash scripts\test_features.sh 19 | -------------------------------------------------------------------------------- /build/apache/NOTICE: -------------------------------------------------------------------------------- 1 | Apache HTTP Server 2 | Copyright 2015 The Apache Software Foundation. 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | 7 | Portions of this software were developed at the National Center 8 | for Supercomputing Applications (NCSA) at the University of 9 | Illinois at Urbana-Champaign. 10 | 11 | This software contains code derived from the RSA Data Security 12 | Inc. MD5 Message-Digest Algorithm, including various 13 | modifications by Spyglass Inc., Carnegie Mellon University, and 14 | Bell Communications Research, Inc (Bellcore). 15 | -------------------------------------------------------------------------------- /build/main.rs: -------------------------------------------------------------------------------- 1 | extern crate phf_codegen; 2 | 3 | mod mime; 4 | 5 | fn main() { 6 | mime::gen(); 7 | } -------------------------------------------------------------------------------- /build/mime.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Write, BufRead, BufReader}; 2 | use std::fs::File; 3 | use std::collections::HashMap; 4 | use std::borrow::Cow; 5 | use std::env; 6 | 7 | macro_rules! or_continue { 8 | ($e: expr) => (if let Some(v) = $e { 9 | v 10 | } else { 11 | continue; 12 | }) 13 | } 14 | 15 | pub fn gen() { 16 | let input = File::open("build/apache/mime.types").unwrap_or_else(|e| { 17 | panic!("could not open 'build/apache/mime.types': {}", e); 18 | }); 19 | 20 | let out_path = format!("{}/mime.rs", env::var("OUT_DIR").unwrap()); 21 | let mut output = File::create(&out_path).unwrap_or_else(|e| { 22 | panic!("could not create '{}': {}", out_path, e); 23 | }); 24 | 25 | let mut types = HashMap::new(); 26 | 27 | for line in BufReader::new(input).lines().filter_map(Result::ok) { 28 | if let Some('#') = line.chars().next() { 29 | continue; 30 | } 31 | 32 | let mut parts = line.split('\t').filter(|v| v.len() > 0); 33 | let mut mime_type = or_continue!(parts.next()).split('/'); 34 | 35 | let top: Cow<_> = match or_continue!(mime_type.next()) { 36 | "*" => "MaybeKnown::Known(::mime::TopLevel::Star)".into(), 37 | "text" => "MaybeKnown::Known(::mime::TopLevel::Text)".into(), 38 | "image" => "MaybeKnown::Known(::mime::TopLevel::Image)".into(), 39 | "audio" => "MaybeKnown::Known(::mime::TopLevel::Audio)".into(), 40 | "video" => "MaybeKnown::Known(::mime::TopLevel::Image)".into(), 41 | "application" => "MaybeKnown::Known(::mime::TopLevel::Application)".into(), 42 | "multipart" => "MaybeKnown::Known(::mime::TopLevel::Multipart)".into(), 43 | "message" => "MaybeKnown::Known(::mime::TopLevel::Message)".into(), 44 | "model" => "MaybeKnown::Known(::mime::TopLevel::Model)".into(), 45 | top => format!("MaybeKnown::Unknown(\"{}\")", top).into() 46 | }; 47 | 48 | let sub: Cow<_> = match or_continue!(mime_type.next()) { 49 | "*" => "MaybeKnown::Known(::mime::SubLevel::Star)".into(), 50 | "plain" => "MaybeKnown::Known(::mime::SubLevel::Plain)".into(), 51 | "html" => "MaybeKnown::Known(::mime::SubLevel::Html)".into(), 52 | "xml" => "MaybeKnown::Known(::mime::SubLevel::Xml)".into(), 53 | "javascript" => "MaybeKnown::Known(::mime::SubLevel::Javascript)".into(), 54 | "css" => "MaybeKnown::Known(::mime::SubLevel::Css)".into(), 55 | "json" => "MaybeKnown::Known(::mime::SubLevel::Json)".into(), 56 | "www-form-url-encoded" => "MaybeKnown::Known(::mime::SubLevel::WwwFormUrlEncoded)".into(), 57 | "form-data" => "MaybeKnown::Known(::mime::SubLevel::FormData)".into(), 58 | "png" => "MaybeKnown::Known(::mime::SubLevel::Png)".into(), 59 | "gif" => "MaybeKnown::Known(::mime::SubLevel::Gif)".into(), 60 | "bmp" => "MaybeKnown::Known(::mime::SubLevel::Bmp)".into(), 61 | "jpeg" => "MaybeKnown::Known(::mime::SubLevel::Jpeg)".into(), 62 | sub => format!("MaybeKnown::Unknown(\"{}\")", sub).into() 63 | }; 64 | 65 | for ext in or_continue!(parts.next()).split(' ') { 66 | types.insert(String::from(ext), format!("({}, {})", top, sub)); 67 | } 68 | } 69 | 70 | write!(&mut output, "static MIME: ::phf::Map<&'static str, (MaybeKnown, MaybeKnown)> = ").unwrap(); 71 | let mut mimes = ::phf_codegen::Map::new(); 72 | for (ext, ty) in &types { 73 | mimes.entry(&ext[..], ty); 74 | } 75 | mimes.build(&mut output).unwrap(); 76 | 77 | write!(&mut output, ";\n").unwrap(); 78 | } -------------------------------------------------------------------------------- /examples/filters.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rustful; 3 | 4 | use std::sync::RwLock; 5 | use std::error::Error; 6 | 7 | #[macro_use] 8 | extern crate log; 9 | extern crate env_logger; 10 | 11 | use rustful::{Server, DefaultRouter, Context, Response}; 12 | use rustful::filter::{FilterContext, ResponseFilter, ResponseAction, ContextFilter, ContextAction}; 13 | use rustful::response::Data; 14 | use rustful::StatusCode; 15 | use rustful::header::Headers; 16 | use rustful::header::ContentType; 17 | use rustful::context::{UriPath, MaybeUtf8Owned}; 18 | 19 | fn say_hello(mut context: Context, mut response: Response, format: &Format) { 20 | //Take the name of the JSONP function from the query variables 21 | let is_jsonp = if let Some(jsonp_name) = context.query.remove("jsonp") { 22 | response.filter_storage_mut().insert(JsonpFn(jsonp_name.into())); 23 | true 24 | } else { 25 | false 26 | }; 27 | 28 | //Set appropriate Content-Type, and decide if we need to quote it 29 | let (mime_type, quote_msg) = match *format { 30 | _ if is_jsonp => (content_type!(Application / Javascript; Charset = Utf8), true), 31 | Format::Json => (content_type!(Application / Json; Charset = Utf8), true), 32 | Format::Text => (content_type!(Text / Plain; Charset = Utf8), false) 33 | }; 34 | response.headers_mut().set(ContentType(mime_type)); 35 | 36 | //Is the format supposed to be a JSON structure? Then set a variable name 37 | if let Format::Json = *format { 38 | response.filter_storage_mut().insert(JsonVar("message")); 39 | } 40 | 41 | let person = match context.variables.get("person") { 42 | Some(name) => name, 43 | None => "stranger".into() 44 | }; 45 | 46 | let message = if quote_msg { 47 | format!("\"Hello, {}!\"", person) 48 | } else { 49 | format!("Hello, {}!", person) 50 | }; 51 | 52 | //Using `try_send` allows us to catch eventual errors from the filters. 53 | //This example should not produce any errors, so this is only for show. 54 | if let Err(e) = response.try_send(message) { 55 | error!("could not send hello: {}", e.description()); 56 | } 57 | } 58 | 59 | enum Format { 60 | Json, 61 | Text 62 | } 63 | 64 | struct Handler(fn(Context, Response, &Format), Format); 65 | 66 | impl rustful::Handler for Handler { 67 | fn handle(&self, context: Context, response: Response) { 68 | self.0(context, response, &self.1); 69 | } 70 | } 71 | 72 | fn main() { 73 | env_logger::init(); 74 | 75 | println!("Visit http://localhost:8080, http://localhost:8080/Peter or http://localhost:8080/json/Peter (if your name is Peter) to try this example."); 76 | println!("Append ?jsonp=someFunction to get a JSONP response."); 77 | println!("Run this example with the environment variable 'RUST_LOG' set to 'debug' to see the debug prints."); 78 | 79 | let mut router = DefaultRouter::::new(); 80 | router.build().path("print").many(|node| { 81 | node.then().on_get(Handler(say_hello, Format::Text)); 82 | node.path(":person").then().on_get(Handler(say_hello, Format::Text)); 83 | 84 | node.path("json").many(|node| { 85 | node.then().on_get(Handler(say_hello, Format::Json)); 86 | node.path(":person").then().on_get(Handler(say_hello, Format::Json)); 87 | }); 88 | }); 89 | 90 | let server_result = Server { 91 | host: 8080.into(), 92 | handlers: router, 93 | 94 | //Log path, change path, log again 95 | context_filters: vec![ 96 | Box::new(RequestLogger::new()), 97 | Box::new(PathPrefix::new("print")), 98 | Box::new(RequestLogger::new()) 99 | ], 100 | 101 | response_filters: vec![Box::new(Jsonp), Box::new(Json)], 102 | 103 | ..Server::default() 104 | }.run(); 105 | 106 | match server_result { 107 | Ok(_server) => {}, 108 | Err(e) => error!("could not start server: {}", e.description()) 109 | } 110 | } 111 | 112 | struct RequestLogger { 113 | counter: RwLock 114 | } 115 | 116 | impl RequestLogger { 117 | pub fn new() -> RequestLogger { 118 | RequestLogger { 119 | counter: RwLock::new(0) 120 | } 121 | } 122 | } 123 | 124 | impl ContextFilter for RequestLogger { 125 | ///Count requests and log the path. 126 | fn modify(&self, _ctx: FilterContext, context: &mut Context) -> ContextAction { 127 | *self.counter.write().unwrap() += 1; 128 | debug!("Request #{} is to '{}'", *self.counter.read().unwrap(), context.uri_path); 129 | ContextAction::next() 130 | } 131 | } 132 | 133 | 134 | struct PathPrefix { 135 | prefix: &'static str 136 | } 137 | 138 | impl PathPrefix { 139 | pub fn new(prefix: &'static str) -> PathPrefix { 140 | PathPrefix { 141 | prefix: prefix 142 | } 143 | } 144 | } 145 | 146 | impl ContextFilter for PathPrefix { 147 | ///Append the prefix to the path 148 | fn modify(&self, _ctx: FilterContext, context: &mut Context) -> ContextAction { 149 | let new_path = context.uri_path.as_path().map(|path| { 150 | let mut new_path = MaybeUtf8Owned::from("/"); 151 | new_path.push_str(self.prefix.trim_matches('/')); 152 | new_path.push_bytes(path.as_ref()); 153 | UriPath::Path(new_path) 154 | }); 155 | if let Some(path) = new_path { 156 | context.uri_path = path; 157 | } 158 | ContextAction::next() 159 | } 160 | } 161 | 162 | struct JsonVar(&'static str); 163 | 164 | struct Json; 165 | 166 | impl ResponseFilter for Json { 167 | fn begin(&self, ctx: FilterContext, status: StatusCode, _headers: &mut Headers) -> (StatusCode, ResponseAction) { 168 | //Check if a JSONP function is defined and write the beginning of the call 169 | let output = if let Some(&JsonVar(var)) = ctx.storage.get() { 170 | Some(format!("{{\"{}\": ", var)) 171 | } else { 172 | None 173 | }; 174 | 175 | (status, ResponseAction::next(output)) 176 | } 177 | 178 | fn write<'a>(&'a self, _ctx: FilterContext, bytes: Option>) -> ResponseAction { 179 | ResponseAction::next(bytes) 180 | } 181 | 182 | fn end(&self, ctx: FilterContext) -> ResponseAction { 183 | //Check if a JSONP function is defined and write the end of the call 184 | let output = ctx.storage.get::().map(|_| "}"); 185 | ResponseAction::next(output) 186 | } 187 | } 188 | 189 | struct JsonpFn(String); 190 | 191 | struct Jsonp; 192 | 193 | impl ResponseFilter for Jsonp { 194 | fn begin(&self, ctx: FilterContext, status: StatusCode, _headers: &mut Headers) -> (StatusCode, ResponseAction) { 195 | //Check if a JSONP function is defined and write the beginning of the call 196 | let output = if let Some(&JsonpFn(ref function)) = ctx.storage.get() { 197 | Some(format!("{}(", function)) 198 | } else { 199 | None 200 | }; 201 | 202 | (status, ResponseAction::next(output)) 203 | } 204 | 205 | fn write<'a>(&'a self, _ctx: FilterContext, bytes: Option>) -> ResponseAction { 206 | ResponseAction::next(bytes) 207 | } 208 | 209 | fn end(&self, ctx: FilterContext) -> ResponseAction { 210 | //Check if a JSONP function is defined and write the end of the call 211 | let output = ctx.storage.get::().map(|_| ");"); 212 | ResponseAction::next(output) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /examples/handler_storage.rs: -------------------------------------------------------------------------------- 1 | extern crate rustful; 2 | 3 | use std::io::{self, Read}; 4 | use std::fs::File; 5 | use std::path::Path; 6 | use std::sync::{Arc, RwLock}; 7 | use std::error::Error; 8 | 9 | #[macro_use] 10 | extern crate log; 11 | extern crate env_logger; 12 | 13 | use rustful::{ 14 | Server, 15 | Context, 16 | Response, 17 | Handler, 18 | DefaultRouter, 19 | StatusCode 20 | }; 21 | use rustful::file::check_path; 22 | 23 | fn main() { 24 | env_logger::init(); 25 | 26 | println!("Visit http://localhost:8080 to try this example."); 27 | 28 | //Read the page before we start 29 | let page = Arc::new(read_string("examples/handler_storage/page.html").unwrap()); 30 | 31 | //The shared counter state 32 | let value = Arc::new(RwLock::new(0)); 33 | 34 | let mut router = DefaultRouter::::new(); 35 | router.build().many(|node| { 36 | node.then().on_get(Api::Counter { 37 | page: page.clone(), 38 | value: value.clone(), 39 | operation: None 40 | }); 41 | node.path("add").then().on_get(Api::Counter { 42 | page: page.clone(), 43 | value: value.clone(), 44 | operation: Some(add) 45 | }); 46 | node.path("sub").then().on_get(Api::Counter { 47 | page: page.clone(), 48 | value: value.clone(), 49 | operation: Some(sub) 50 | }); 51 | node.path("res/*file").then().on_get(Api::File); 52 | }); 53 | 54 | let server_result = Server { 55 | host: 8080.into(), 56 | handlers: router, 57 | ..Server::default() 58 | }.run(); 59 | 60 | match server_result { 61 | Ok(_server) => {}, 62 | Err(e) => error!("could not start server: {}", e.description()) 63 | } 64 | } 65 | 66 | 67 | fn add(value: i32) -> i32 { 68 | value + 1 69 | } 70 | 71 | fn sub(value: i32) -> i32 { 72 | value - 1 73 | } 74 | 75 | fn read_string>(path: P) -> io::Result { 76 | //Read file into a string 77 | let mut string = String::new(); 78 | File::open(path).and_then(|mut f| f.read_to_string(&mut string)).map(|_| string) 79 | } 80 | 81 | 82 | enum Api { 83 | Counter { 84 | //We are using the handler to preload the page in this example 85 | page: Arc, 86 | 87 | value: Arc>, 88 | operation: Option i32> 89 | }, 90 | File 91 | } 92 | 93 | impl Handler for Api { 94 | fn handle(&self, context: Context, mut response: Response) { 95 | match *self { 96 | Api::Counter { ref page, ref value, ref operation } => { 97 | operation.map(|op| { 98 | //Lock the value for writing and update it 99 | let mut value = value.write().unwrap(); 100 | *value = op(*value); 101 | }); 102 | 103 | //Insert the value into the page and write it to the response 104 | let count = value.read().unwrap().to_string(); 105 | response.send(page.replace("{}", &count[..])); 106 | }, 107 | Api::File => { 108 | if let Some(file) = context.variables.get("file") { 109 | let file_path = Path::new(file.as_ref()); 110 | 111 | //Check if the path is valid 112 | if check_path(file_path).is_ok() { 113 | //Make a full path from the file name and send it 114 | let path = Path::new("examples/handler_storage").join(file_path); 115 | let res = response.try_send(path) 116 | .or_else(|e| e.send_not_found("the file was not found")) 117 | .or_else(|e| e.ignore_send_error()); 118 | 119 | //Check if a more fatal file error than "not found" occurred 120 | if let Err((error, mut response)) = res { 121 | //Something went horribly wrong 122 | error!("failed to open '{}': {}", file, error); 123 | response.set_status(StatusCode::InternalServerError); 124 | } 125 | } else { 126 | //Accessing parent directories is forbidden 127 | response.set_status(StatusCode::Forbidden); 128 | } 129 | } else { 130 | //No file name was specified 131 | response.set_status(StatusCode::Forbidden); 132 | } 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /examples/handler_storage/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | font-size: 500%; 7 | height: 100%; 8 | margin: 0; 9 | } 10 | 11 | div { 12 | display: table; 13 | height: 100%; 14 | width: 100%; 15 | } 16 | 17 | span { 18 | display: table-cell; 19 | vertical-align: middle; 20 | text-align: center; 21 | } 22 | 23 | a { 24 | display: inline-block; 25 | color: gray; 26 | text-decoration: none; 27 | width: 1.3em; 28 | height: 1.3em; 29 | padding: 0.2em; 30 | border: 1px solid gray; 31 | } 32 | 33 | a:hover { 34 | background: lightgray; 35 | } -------------------------------------------------------------------------------- /examples/handler_storage/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Counter 5 | 6 | 7 | 8 | 9 | 10 |
- {} +
11 | 12 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | //Include macros to be able to use `insert_routes!`. 2 | #[macro_use] 3 | extern crate log; 4 | extern crate env_logger; 5 | 6 | extern crate rustful; 7 | 8 | use std::error::Error; 9 | 10 | use rustful::{Server, Context, Response, DefaultRouter}; 11 | 12 | fn say_hello(context: Context, response: Response) { 13 | //Get the value of the path variable `:person`, from below. 14 | let person = match context.variables.get("person") { 15 | Some(name) => name, 16 | None => "stranger".into() 17 | }; 18 | 19 | //Use the name from the path variable to say hello. 20 | response.send(format!("Hello, {}!", person)); 21 | } 22 | 23 | fn main() { 24 | env_logger::init(); 25 | 26 | println!("Visit http://localhost:8080 or http://localhost:8080/Olivia (if your name is Olivia) to try this example."); 27 | 28 | //Create a DefaultRouter and fill it with handlers. 29 | let mut router = DefaultRouter::::new(); 30 | router.build().many(|node| { 31 | //Handle requests for root... 32 | node.then().on_get(say_hello); 33 | 34 | //...and one level below. 35 | //`:person` is a path variable and it will be accessible in the handler. 36 | node.path(":person").then().on_get(say_hello); 37 | }); 38 | 39 | //Build and run the server. 40 | let server_result = Server { 41 | handlers: router, 42 | 43 | //Turn a port number into an IPV4 host address (0.0.0.0:8080 in this case). 44 | host: 8080.into(), 45 | 46 | //Use default values for everything else. 47 | ..Server::default() 48 | }.run(); 49 | 50 | match server_result { 51 | Ok(_server) => {}, 52 | Err(e) => error!("could not start server: {}", e.description()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/post.rs: -------------------------------------------------------------------------------- 1 | extern crate rustful; 2 | 3 | use std::io::{self, Read}; 4 | use std::fs::File; 5 | use std::path::Path; 6 | use std::borrow::Cow; 7 | use std::error::Error as ErrorTrait; 8 | 9 | #[macro_use] 10 | extern crate log; 11 | extern crate env_logger; 12 | 13 | use rustful::{Server, Context, Response, ContentFactory, SendResponse}; 14 | use rustful::StatusCode::{InternalServerError, BadRequest}; 15 | 16 | fn say_hello(mut context: Context) -> Result { 17 | let body = context.body.read_query_body().map_err(|_| Error::CouldNotReadBody)?; 18 | let files: &Files = context.global.get().ok_or(Error::MissingFileCache)?; 19 | 20 | //Format the name or use the cached form 21 | let content = if let Some(name) = body.get("name") { 22 | Cow::Owned(format!("

Hello, {}!

", name)) 23 | } else { 24 | Cow::Borrowed(&files.form) 25 | }; 26 | 27 | //Insert the content into the page and write it to the response 28 | Ok(files.page.replace("{}", &content)) 29 | } 30 | 31 | fn main() { 32 | env_logger::init(); 33 | 34 | println!("Visit http://localhost:8080 to try this example."); 35 | 36 | //Preload the files 37 | let files = Files { 38 | page: read_string("examples/post/page.html").unwrap(), 39 | form: read_string("examples/post/form.html").unwrap() 40 | }; 41 | 42 | //The ContentFactory wrapper allows simplified handlers that return their 43 | //responses 44 | let server_result = Server { 45 | host: 8080.into(), 46 | global: Box::new(files).into(), 47 | ..Server::new(ContentFactory(say_hello)) 48 | }.run(); 49 | 50 | //Check if the server started successfully 51 | match server_result { 52 | Ok(_server) => {}, 53 | Err(e) => error!("could not start server: {}", e.description()) 54 | } 55 | } 56 | 57 | fn read_string>(path: P) -> io::Result { 58 | //Read file into a string 59 | let mut string = String::new(); 60 | File::open(path).and_then(|mut f| f.read_to_string(&mut string)).map(|_| string) 61 | } 62 | 63 | //We want to store the files as strings 64 | struct Files { 65 | page: String, 66 | form: String 67 | } 68 | 69 | enum Error { 70 | CouldNotReadBody, 71 | MissingFileCache 72 | } 73 | 74 | impl<'a, 'b> SendResponse<'a, 'b> for Error { 75 | type Error = rustful::Error; 76 | 77 | fn send_response(self, mut response: Response<'a, 'b>) -> Result<(), rustful::Error> { 78 | match self { 79 | Error::CouldNotReadBody => response.set_status(BadRequest), 80 | Error::MissingFileCache => { 81 | error!("the global data should be of the type `Files`, but it's not"); 82 | response.set_status(InternalServerError); 83 | }, 84 | } 85 | 86 | response.try_send("") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/post/form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
-------------------------------------------------------------------------------- /examples/post/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello 6 | 7 | 8 | {} 9 | 10 | -------------------------------------------------------------------------------- /examples/tiny_hello.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | extern crate env_logger; 4 | 5 | extern crate rustful; 6 | use std::error::Error; 7 | use rustful::{Server, Context, Response}; 8 | 9 | fn main() { 10 | env_logger::init(); 11 | 12 | println!("Visit http://localhost:8080 to try this example."); 13 | let server_result = Server { 14 | host: 8080.into(), 15 | ..Server::new(|_: Context, res: Response| res.send("Hello!")) 16 | }.run(); 17 | 18 | match server_result { 19 | Ok(_server) => {}, 20 | Err(e) => error!("could not start server: {}", e.description()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/todo.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rustful; 3 | extern crate serde; 4 | extern crate serde_json; 5 | #[macro_use] 6 | extern crate serde_derive; 7 | extern crate unicase; 8 | 9 | #[macro_use] 10 | extern crate log; 11 | extern crate env_logger; 12 | 13 | use std::sync::RwLock; 14 | use std::collections::btree_map::{BTreeMap, Iter}; 15 | 16 | use unicase::UniCase; 17 | 18 | use rustful::{ 19 | Server, 20 | Context, 21 | Response, 22 | Handler, 23 | DefaultRouter, 24 | SendResponse 25 | }; 26 | use rustful::header::{ 27 | ContentType, 28 | AccessControlAllowOrigin, 29 | AccessControlAllowMethods, 30 | AccessControlAllowHeaders, 31 | Host 32 | }; 33 | use rustful::StatusCode; 34 | 35 | fn main() { 36 | env_logger::init(); 37 | 38 | let mut router = DefaultRouter::::new(); 39 | 40 | //Global actions 41 | router.build().then().many(|endpoint| { 42 | endpoint.on_get(Api(Some(list_all))); 43 | endpoint.on_post(Api(Some(store))); 44 | endpoint.on_delete(Api(Some(clear))); 45 | endpoint.on_options(Api(None)); 46 | }); 47 | 48 | //Note actions 49 | router.build().path(":id").then().many(|endpoint| { 50 | endpoint.on_get(Api(Some(get_todo))); 51 | endpoint.on_patch(Api(Some(edit_todo))); 52 | endpoint.on_delete(Api(Some(delete_todo))); 53 | endpoint.on_options(Api(None)); 54 | }); 55 | 56 | //Enables hyperlink search, which will be used in CORS 57 | router.find_hyperlinks = true; 58 | 59 | //Our imitation of a database 60 | let database = RwLock::new(Table::new()); 61 | 62 | let server_result = Server { 63 | handlers: router, 64 | host: 8080.into(), 65 | content_type: content_type!(Application / Json; Charset = Utf8), 66 | global: Box::new(database).into(), 67 | ..Server::default() 68 | }.run(); 69 | 70 | match server_result { 71 | Ok(server) => { 72 | println!( 73 | "This example is a showcase implementation of the Todo-Backend project (http://todobackend.com/), \ 74 | visit http://localhost:{0}/ to try it or run reference test suite by pointing \ 75 | your browser to http://todobackend.com/specs/index.html?http://localhost:{0}", 76 | server.socket.port() 77 | ); 78 | }, 79 | Err(e) => error!("could not run the server: {}", e) 80 | }; 81 | } 82 | 83 | //Errors that may occur while parsing the request 84 | enum Error { 85 | ParseError, 86 | BadId, 87 | MissingHostHeader, 88 | } 89 | 90 | impl<'a, 'b> SendResponse<'a, 'b> for Error { 91 | type Error = rustful::Error; 92 | 93 | fn send_response(self, mut response: Response<'a, 'b>) -> Result<(), rustful::Error> { 94 | let message = match self { 95 | Error::ParseError => "Couldn't parse the todo", 96 | Error::BadId => "The 'id' parameter should be a non-negative integer", 97 | Error::MissingHostHeader => "No 'Host' header was sent", 98 | }; 99 | 100 | response.headers_mut().set(ContentType(content_type!(Text / Plain; Charset = Utf8))); 101 | response.set_status(StatusCode::BadRequest); 102 | message.send_response(response) 103 | } 104 | } 105 | 106 | 107 | 108 | //List all the to-dos in the database 109 | fn list_all(database: &Database, context: Context) -> Result, Error> { 110 | let host = try!(context.headers.get().ok_or(Error::MissingHostHeader)); 111 | 112 | let todos: Vec<_> = database.read().unwrap().iter() 113 | .map(|(&id, todo)| NetworkTodo::from_todo(todo, host, id)) 114 | .collect(); 115 | 116 | Ok(Some(serde_json::to_string(&todos).unwrap())) 117 | } 118 | 119 | //Store a new to-do with data from the request body 120 | fn store(database: &Database, context: Context) -> Result, Error> { 121 | let todo: NetworkTodo = try!( 122 | serde_json::from_reader(context.body).map_err(|_| Error::ParseError) 123 | ); 124 | 125 | let host = try!(context.headers.get().ok_or(Error::MissingHostHeader)); 126 | 127 | let mut database = database.write().unwrap(); 128 | database.insert(todo.into()); 129 | 130 | let todo = database.last().map(|(id, todo)| { 131 | NetworkTodo::from_todo(todo, host, id) 132 | }); 133 | 134 | Ok(Some(serde_json::to_string(&todo).unwrap())) 135 | } 136 | 137 | //Clear the database 138 | fn clear(database: &Database, _context: Context) -> Result, Error> { 139 | database.write().unwrap().clear(); 140 | Ok(Some("".into())) 141 | } 142 | 143 | //Send one particular to-do, selected by its id 144 | fn get_todo(database: &Database, context: Context) -> Result, Error> { 145 | let host = try!(context.headers.get().ok_or(Error::MissingHostHeader)); 146 | let id = try!(context.variables.parse("id").map_err(|_| Error::BadId)); 147 | 148 | let todo = database.read().unwrap().get(id).map(|todo| { 149 | NetworkTodo::from_todo(&todo, host, id) 150 | }); 151 | 152 | Ok(todo.map(|todo| serde_json::to_string(&todo).unwrap())) 153 | } 154 | 155 | //Update a to-do, selected by its id with data from the request body 156 | fn edit_todo(database: &Database, context: Context) -> Result, Error> { 157 | let edits: NetworkTodo = try!( 158 | serde_json::from_reader(context.body).map_err(|_| Error::ParseError) 159 | ); 160 | let host = try!(context.headers.get().ok_or(Error::MissingHostHeader)); 161 | let id = try!(context.variables.parse("id").map_err(|_| Error::BadId)); 162 | 163 | let mut database = database.write().unwrap(); 164 | let mut todo = database.get_mut(id); 165 | todo.as_mut().map(|todo| todo.update(edits)); 166 | 167 | let todo = todo.map(|todo| { 168 | NetworkTodo::from_todo(&todo, host, id) 169 | }); 170 | 171 | Ok(Some(serde_json::to_string(&todo).unwrap())) 172 | } 173 | 174 | //Delete a to-do, selected by its id 175 | fn delete_todo(database: &Database, context: Context) -> Result, Error> { 176 | let id = try!(context.variables.parse("id").map_err(|_| Error::BadId)); 177 | database.write().unwrap().delete(id); 178 | Ok(Some("".into())) 179 | } 180 | 181 | 182 | 183 | 184 | //An API endpoint with an optional action 185 | struct Api(Option Result, Error>>); 186 | 187 | impl Handler for Api { 188 | fn handle(&self, context: Context, mut response: Response) { 189 | //Collect the accepted methods from the provided hyperlinks 190 | let mut methods: Vec<_> = context.hyperlinks.iter().filter_map(|l| l.method.clone()).collect(); 191 | methods.push(context.method.clone()); 192 | 193 | //Setup cross origin resource sharing 194 | response.headers_mut().set(AccessControlAllowOrigin::Any); 195 | response.headers_mut().set(AccessControlAllowMethods(methods)); 196 | response.headers_mut().set(AccessControlAllowHeaders(vec![UniCase("content-type".into())])); 197 | 198 | //Get the database from the global storage 199 | let database = if let Some(database) = context.global.get() { 200 | database 201 | } else { 202 | error!("expected a globally accessible Database"); 203 | response.set_status(StatusCode::InternalServerError); 204 | return 205 | }; 206 | 207 | if let Some(action) = self.0 { 208 | response.send(action(database, context)); 209 | } 210 | } 211 | } 212 | 213 | //A read-write-locked Table will do as our database 214 | type Database = RwLock; 215 | 216 | //A simple imitation of a database table 217 | struct Table { 218 | next_id: usize, 219 | items: BTreeMap 220 | } 221 | 222 | impl Table { 223 | fn new() -> Table { 224 | Table { 225 | next_id: 0, 226 | items: BTreeMap::new() 227 | } 228 | } 229 | 230 | fn insert(&mut self, item: Todo) { 231 | self.items.insert(self.next_id, item); 232 | self.next_id += 1; 233 | } 234 | 235 | fn delete(&mut self, id: usize) { 236 | self.items.remove(&id); 237 | } 238 | 239 | fn clear(&mut self) { 240 | self.items.clear(); 241 | } 242 | 243 | fn last(&self) -> Option<(usize, &Todo)> { 244 | self.items.keys().next_back().cloned().and_then(|id| { 245 | self.items.get(&id).map(|item| (id, item)) 246 | }) 247 | } 248 | 249 | fn get(&self, id: usize) -> Option<&Todo> { 250 | self.items.get(&id) 251 | } 252 | 253 | fn get_mut(&mut self, id: usize) -> Option<&mut Todo> { 254 | self.items.get_mut(&id) 255 | } 256 | 257 | fn iter(&self) -> Iter { 258 | (&self.items).iter() 259 | } 260 | } 261 | 262 | 263 | //A structure for what will be sent and received over the network 264 | #[derive(Serialize, Deserialize)] 265 | struct NetworkTodo { 266 | title: Option, 267 | completed: Option, 268 | order: Option, 269 | url: Option 270 | } 271 | 272 | impl NetworkTodo { 273 | fn from_todo(todo: &Todo, host: &Host, id: usize) -> NetworkTodo { 274 | let url = if let Some(port) = host.port { 275 | format!("http://{}:{}/{}", host.hostname, port, id) 276 | } else { 277 | format!("http://{}/{}", host.hostname, id) 278 | }; 279 | 280 | NetworkTodo { 281 | title: Some(todo.title.clone()), 282 | completed: Some(todo.completed), 283 | order: Some(todo.order), 284 | url: Some(url) 285 | } 286 | } 287 | } 288 | 289 | 290 | //The stored to-do data 291 | struct Todo { 292 | title: String, 293 | completed: bool, 294 | order: u32 295 | } 296 | 297 | impl Todo { 298 | fn update(&mut self, changes: NetworkTodo) { 299 | if let Some(title) = changes.title { 300 | self.title = title; 301 | } 302 | 303 | if let Some(completed) = changes.completed { 304 | self.completed = completed; 305 | } 306 | 307 | if let Some(order) = changes.order { 308 | self.order = order 309 | } 310 | } 311 | } 312 | 313 | impl From for Todo { 314 | fn from(todo: NetworkTodo) -> Todo { 315 | Todo { 316 | title: todo.title.unwrap_or(String::new()), 317 | completed: todo.completed.unwrap_or(false), 318 | order: todo.order.unwrap_or(0) 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /scripts/cargo.sh: -------------------------------------------------------------------------------- 1 | if [ "$TRAVIS_RUST_VERSION" = "nightly" ]; then 2 | cargo $@ --features nightly 3 | else 4 | cargo $@ 5 | fi -------------------------------------------------------------------------------- /scripts/changelog.sh: -------------------------------------------------------------------------------- 1 | current_version="$(cargo read-manifest | sed 's/.*"version":"\([^"]\+\)".*/\1/g')" 2 | current_date= 3 | 4 | echo -e "# Changelog\n" > CHANGELOG.md 5 | echo -e "## Version $current_version - $(date +%F)\n" >> CHANGELOG.md 6 | 7 | pulls=() 8 | issues=() 9 | 10 | git log --pretty="%an<%ae>;%H;%ad;%s" --date=short | 11 | { 12 | while read line; do 13 | if [[ $line =~ Homu\\;.* ]]; then 14 | parts="$(echo "$line" | sed 's/.*;\([^;]*\);.*;Auto merge of #\([0-9]*\)*/\1 \2/g')" 15 | parts=($parts) 16 | description="$(git log -1 --pretty=format:%b ${parts[0]})" 17 | header="$(echo "$description" | head -n 1)" 18 | 19 | fixes="$(echo "$description" | grep -iEo "(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) #[0-9]+" | sed 's/.* #\([0-9]*\)/\1/g')" 20 | 21 | issues+=("$fixes") 22 | pulls+=("${parts[1]}") 23 | 24 | fixes="$(echo "$fixes" | sed ':a;N;$!ba;s/\n/, /g' | sed 's/\([0-9]\+\)/[#\1][\1]/g')" 25 | 26 | entry=" * [#${parts[1]}][${parts[1]}]: $header." 27 | 28 | if [[ "$fixes" != "" ]]; then 29 | echo "$entry Closes $fixes." >> CHANGELOG.md 30 | else 31 | echo "$entry" >> CHANGELOG.md 32 | fi 33 | elif [[ $line =~ .*\;.*\;.*\;Version\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then 34 | parts="$(echo "$line" | sed 's/.*;.*;\(.\+\);Version \(.*\)/\1 \2/g')" 35 | parts=($parts) 36 | echo -e "\n## Version ${parts[1]} - ${parts[0]}\n" >> CHANGELOG.md 37 | fi 38 | done 39 | 40 | for id in ${pulls[@]}; do 41 | echo "[$id]: https://github.com/Ogeon/rustful/pull/$id" >> CHANGELOG.md 42 | done 43 | 44 | for id in ${issues[@]}; do 45 | echo "[$id]: https://github.com/Ogeon/rustful/issues/$id" >> CHANGELOG.md 46 | done 47 | } 48 | -------------------------------------------------------------------------------- /scripts/id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ogeon/rustful/24173f35462a976e53d7826c1b107a995572d132/scripts/id_rsa.enc -------------------------------------------------------------------------------- /scripts/test_features.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | #List of features to test 4 | features="" 5 | 6 | #Features that will always be activated 7 | required_features="strict" 8 | 9 | 10 | #Find features 11 | walking_features=false 12 | current_dependency="" 13 | 14 | while read -r line || [[ -n "$line" ]]; do 15 | if [[ "$line" == "[features]" ]]; then 16 | walking_features=true 17 | elif [[ $walking_features == true ]] && [[ "$line" == "#internal" ]]; then 18 | walking_features=false 19 | elif [[ $walking_features == true ]] && echo "$line" | grep -E "^\[.*\]" > /dev/null; then 20 | walking_features=false 21 | elif [[ $walking_features == true ]] && echo "$line" | grep -E ".*=.*" > /dev/null; then 22 | feature="$(echo "$line" | cut -f1 -d"=")" 23 | feature="$(echo -e "${feature}" | tr -d '[[:space:]]')" 24 | if [[ "$feature" != "default" ]]; then 25 | echo "found feature '$feature'" 26 | features="$features $feature" 27 | fi 28 | elif echo "$line" | grep -E "^\[dependencies\..*\]" > /dev/null; then 29 | current_dependency="$(echo "$line" | sed 's/.*\[dependencies\.\([^]]*\)\].*/\1/g')" 30 | elif [[ "$line" == "#feature" ]] && [[ "$current_dependency" != "" ]]; then 31 | echo "found dependency feature '$current_dependency'" 32 | features="$features $current_dependency" 33 | fi 34 | done < "Cargo.toml" 35 | 36 | #Test without any optional feature 37 | echo compiling with --no-default-features --features "$required_features" 38 | cargo build --no-default-features --features "$required_features" 39 | 40 | #Isolated test of each optional feature 41 | for feature in $features; do 42 | echo compiling with --no-default-features --features "\"$feature $required_features\"" 43 | cargo build --no-default-features --features "$feature $required_features" 44 | done 45 | -------------------------------------------------------------------------------- /scripts/travis-doc-upload.cfg: -------------------------------------------------------------------------------- 1 | PROJECT_NAME=rustful 2 | DOCS_REPO=Ogeon/docs.git 3 | SSH_KEY_TRAVIS_ID=bfb9429b15f2 -------------------------------------------------------------------------------- /scripts/upload_doc.sh: -------------------------------------------------------------------------------- 1 | if [ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_OS_NAME" = "linux" ]; then 2 | curl https://raw.githubusercontent.com/ogeon/travis-doc-upload/master/travis-doc-upload.sh | sh 3 | fi -------------------------------------------------------------------------------- /src/context/body.rs: -------------------------------------------------------------------------------- 1 | //!Anything related to reading the request body. 2 | 3 | #[cfg(feature = "multipart")] 4 | use multipart::server::{HttpRequest, Multipart}; 5 | 6 | use std::io::{self, Read}; 7 | 8 | use hyper::buffer::BufReader; 9 | use hyper::http::h1::HttpReader; 10 | use hyper::net::NetworkStream; 11 | 12 | use context::Parameters; 13 | use header::Headers; 14 | 15 | ///A reader for a request body. 16 | pub struct BodyReader<'a, 'b: 'a> { 17 | reader: MaybeMock>>, 18 | 19 | #[cfg(feature = "multipart")] 20 | multipart_boundary: Option 21 | } 22 | 23 | impl<'a, 'b> BodyReader<'a, 'b> { 24 | #[doc(hidden)] 25 | #[cfg(feature = "multipart")] 26 | ///Internal and may change without warning. 27 | pub fn from_reader(reader: HttpReader<&'a mut BufReader<&'b mut NetworkStream>>, headers: &Headers) -> BodyReader<'a, 'b> { 28 | use header::ContentType; 29 | use mime::{Mime, TopLevel, SubLevel, Attr, Value}; 30 | 31 | let boundary = match headers.get() { 32 | Some(&ContentType(Mime(TopLevel::Multipart, SubLevel::FormData, ref attrs))) => { 33 | attrs.iter() 34 | .find(|&&(ref attr, _)| attr == &Attr::Boundary) 35 | .and_then(|&(_, ref val)| if let Value::Ext(ref boundary) = *val { 36 | Some(boundary.clone()) 37 | } else { 38 | None 39 | }) 40 | }, 41 | _ => None 42 | }; 43 | 44 | BodyReader { 45 | reader: MaybeMock::Actual(reader), 46 | multipart_boundary: boundary 47 | } 48 | } 49 | 50 | #[doc(hidden)] 51 | #[cfg(not(feature = "multipart"))] 52 | ///Internal and may change without warning. 53 | pub fn from_reader(reader: HttpReader<&'a mut BufReader<&'b mut NetworkStream>>, _headers: &Headers) -> BodyReader<'a, 'b> { 54 | BodyReader { 55 | reader: MaybeMock::Actual(reader) 56 | } 57 | } 58 | 59 | ///Create a non-functional body reader for testing purposes. 60 | #[cfg(feature = "multipart")] 61 | pub fn mock(headers: &'b Headers) -> BodyReader<'static, 'static> { 62 | use header::ContentType; 63 | use mime::{Mime, TopLevel, SubLevel, Attr, Value}; 64 | 65 | let boundary = match headers.get() { 66 | Some(&ContentType(Mime(TopLevel::Multipart, SubLevel::FormData, ref attrs))) => { 67 | attrs.iter() 68 | .find(|&&(ref attr, _)| attr == &Attr::Boundary) 69 | .and_then(|&(_, ref val)| if let Value::Ext(ref boundary) = *val { 70 | Some(boundary.clone()) 71 | } else { 72 | None 73 | }) 74 | }, 75 | _ => None 76 | }; 77 | 78 | BodyReader { 79 | reader: MaybeMock::Mock, 80 | multipart_boundary: boundary, 81 | } 82 | } 83 | 84 | ///Create a non-functional body reader for testing purposes. 85 | #[cfg(not(feature = "multipart"))] 86 | pub fn mock(_headers: &'b Headers) -> BodyReader<'static, 'static> { 87 | BodyReader { 88 | reader: MaybeMock::Mock 89 | } 90 | } 91 | } 92 | 93 | impl<'a, 'b> BodyReader<'a, 'b> { 94 | ///Try to create a `multipart/form-data` reader from the request body. 95 | /// 96 | ///``` 97 | ///# extern crate rustful; 98 | ///# extern crate mime; 99 | ///# extern crate multipart; 100 | ///use std::fmt::Write; 101 | ///use std::io::Read; 102 | ///use rustful::{Context, Response}; 103 | ///use rustful::StatusCode::BadRequest; 104 | ///use multipart::server::MultipartData; 105 | /// 106 | ///fn my_handler(mut context: Context, mut response: Response) { 107 | /// if let Some(mut multipart) = context.body.as_multipart() { 108 | /// let mut result = String::new(); 109 | /// 110 | /// //Iterate over the multipart entries and print info about them in `result` 111 | /// multipart.foreach_entry(|mut entry| { 112 | /// if let Some(content_mime) = entry.headers.content_type { 113 | /// if content_mime.type_() == mime::TEXT { 114 | /// //Found data from a text field 115 | /// let mut text = String::new(); 116 | /// entry.data.read_to_string(&mut text); 117 | /// writeln!(&mut result, "{}: '{}'", entry.headers.name, text); 118 | /// } 119 | /// else { 120 | /// //Found an uploaded file 121 | /// if let Some(file_name) = entry.headers.filename { 122 | /// writeln!(&mut result, "{}: a file called '{}'", entry.headers.name, file_name); 123 | /// } else { 124 | /// writeln!(&mut result, "{}: a nameless file", entry.headers.name); 125 | /// } 126 | /// } 127 | /// } 128 | /// else { 129 | /// //Content-type not supplied, default to text/plain as per IETF RFC 7578, section 4.4 130 | /// let mut text = String::new(); 131 | /// entry.data.read_to_string(&mut text); 132 | /// writeln!(&mut result, "{}: '{}'", entry.headers.name, text); 133 | /// } 134 | /// }); 135 | /// 136 | /// response.send(result); 137 | /// } else { 138 | /// //We expected it to be a valid `multipart/form-data` request, but it was not 139 | /// response.set_status(BadRequest); 140 | /// } 141 | ///} 142 | ///# fn main() {} 143 | ///``` 144 | #[cfg(feature = "multipart")] 145 | pub fn as_multipart<'r>(&'r mut self) -> Option>> { 146 | if let MaybeMock::Actual(ref mut reader) = self.reader { 147 | self.multipart_boundary.as_ref().and_then(move |boundary| 148 | Multipart::from_request(MultipartRequest { 149 | boundary: boundary, 150 | reader: reader 151 | }).ok() 152 | ) 153 | } else { 154 | None 155 | } 156 | } 157 | 158 | ///Read and parse the request body as a query string. The body will be 159 | ///decoded as UTF-8 and plain '+' characters will be replaced with spaces. 160 | /// 161 | ///A simplified example of how to parse `a=number&b=number`: 162 | /// 163 | ///``` 164 | ///use rustful::{Context, Response}; 165 | /// 166 | ///fn my_handler(mut context: Context, response: Response) { 167 | /// //Parse the request body as a query string 168 | /// let query = context.body.read_query_body().unwrap(); 169 | /// 170 | /// //Find "a" and "b" and assume that they are numbers 171 | /// let a: f64 = query.get("a").and_then(|number| number.parse().ok()).unwrap(); 172 | /// let b: f64 = query.get("b").and_then(|number| number.parse().ok()).unwrap(); 173 | /// 174 | /// response.send(format!("{} + {} = {}", a, b, a + b)); 175 | ///} 176 | ///``` 177 | #[inline] 178 | pub fn read_query_body(&mut self) -> io::Result { 179 | let mut buf = Vec::new(); 180 | try!(self.read_to_end(&mut buf)); 181 | Ok(::utils::parse_parameters(&buf)) 182 | } 183 | 184 | } 185 | 186 | impl<'a, 'b> Read for BodyReader<'a, 'b> { 187 | ///Read the request body. 188 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 189 | self.reader.read(buf) 190 | } 191 | } 192 | 193 | ///A specialized request representation for the multipart interface. 194 | #[cfg(feature = "multipart")] 195 | pub struct MultipartRequest<'r, 'a: 'r, 'b: 'a> { 196 | boundary: &'r str, 197 | reader: &'r mut HttpReader<&'a mut BufReader<&'b mut NetworkStream>> 198 | } 199 | 200 | #[cfg(feature = "multipart")] 201 | impl<'r, 'a, 'b> HttpRequest for MultipartRequest<'r, 'a, 'b> { 202 | type Body = Self; 203 | 204 | fn body(self) -> Self { 205 | self 206 | } 207 | 208 | fn multipart_boundary(&self) -> Option<&str> { 209 | Some(self.boundary) 210 | } 211 | } 212 | 213 | #[cfg(feature = "multipart")] 214 | impl<'r, 'a, 'b> Read for MultipartRequest<'r, 'a, 'b> { 215 | ///Read the request body. 216 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 217 | self.reader.read(buf) 218 | } 219 | } 220 | 221 | enum MaybeMock { 222 | Actual(R), 223 | Mock 224 | } 225 | 226 | impl Read for MaybeMock { 227 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 228 | if let &mut MaybeMock::Actual(ref mut reader) = self { 229 | reader.read(buf) 230 | } else { 231 | Ok(0) 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/context/hypermedia.rs: -------------------------------------------------------------------------------- 1 | //!Anything related to hypermedia and hyperlinks. 2 | use std::fmt; 3 | use std::cmp; 4 | 5 | use Method; 6 | use Handler; 7 | use context::MaybeUtf8Slice; 8 | 9 | ///A hyperlink. 10 | #[derive(Clone,)] 11 | pub struct Link<'a> { 12 | ///The HTTP method for which an endpoint is available. It can be left 13 | ///unspecified if the method doesn't matter. 14 | pub method: Option, 15 | ///A relative path from the current location. 16 | pub path: Vec>, 17 | ///The handler that will answer at the endpoint. 18 | pub handler: Option<&'a Handler>, 19 | } 20 | 21 | impl<'a> fmt::Debug for Link<'a> { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | write!(f, "method: {:?}, path: {:?}, handler present: {}", self.method, self.path, self.handler.is_some()) 24 | } 25 | } 26 | 27 | impl<'a> cmp::PartialEq for Link<'a> { 28 | fn eq(&self, other: &Link<'a>) -> bool { 29 | self.method == other.method && self.path == other.path 30 | } 31 | } 32 | 33 | impl<'a> cmp::Eq for Link<'a> {} 34 | 35 | impl<'a> cmp::PartialOrd for Link<'a> { 36 | fn partial_cmp(&self, other: &Link<'a>) -> Option { 37 | Some(self.cmp(other)) 38 | } 39 | } 40 | 41 | impl<'a> cmp::Ord for Link<'a> { 42 | fn cmp(&self, other: &Link<'a>) -> cmp::Ordering { 43 | let method_ord = match (&self.method, &other.method) { 44 | (&None, &None) => cmp::Ordering::Equal, 45 | (&Some(_), &None) => cmp::Ordering::Greater, 46 | (&None, &Some(_)) => cmp::Ordering::Less, 47 | (&Some(ref this_method), &Some(ref other_method)) => this_method.as_ref().cmp(&other_method.as_ref()), 48 | }; 49 | 50 | if method_ord == cmp::Ordering::Equal { 51 | self.path.cmp(&other.path) 52 | } else { 53 | method_ord 54 | } 55 | } 56 | } 57 | 58 | ///A segment of a hyperlink path. 59 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] 60 | pub struct LinkSegment<'a> { 61 | ///The expected static segment or the name of a variable segment. Variable 62 | ///segments are allowed to have an empty string as label if it's unknown or 63 | ///unimportant. 64 | pub label: MaybeUtf8Slice<'a>, 65 | ///The type of the segment (e.g. static or variable). 66 | pub ty: SegmentType 67 | } 68 | 69 | ///The type of a hyperlink segment. 70 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] 71 | pub enum SegmentType { 72 | ///A static part of a path. 73 | Static, 74 | ///A single dynamic part of a path. This will match one arbitrary path segment. 75 | VariableSegment, 76 | ///A dynamic sequence of segments. This works like a variable segment, but 77 | ///will match one or more segments until the rest of the pattern matches. 78 | VariableSequence, 79 | } -------------------------------------------------------------------------------- /src/context/maybe_utf8.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut, Drop}; 2 | use std::borrow::{Cow, Borrow}; 3 | use std::hash::{Hash, Hasher}; 4 | use std::cmp::Ordering; 5 | 6 | use ::utils::BytesExt; 7 | 8 | ///An owned string that may be UTF-8 encoded. 9 | pub type MaybeUtf8Owned = MaybeUtf8>; 10 | ///A slice of a string that may be UTF-8 encoded. 11 | pub type MaybeUtf8Slice<'a> = MaybeUtf8<&'a str, &'a [u8]>; 12 | 13 | ///String data that may or may not be UTF-8 encoded. 14 | #[derive(Debug, Clone)] 15 | pub enum MaybeUtf8 { 16 | ///A UTF-8 encoded string. 17 | Utf8(S), 18 | ///A non-UTF-8 string. 19 | NotUtf8(V) 20 | } 21 | 22 | impl MaybeUtf8 { 23 | ///Create an empty UTF-8 string. 24 | pub fn new() -> MaybeUtf8 where S: From<&'static str> { 25 | MaybeUtf8::Utf8("".into()) 26 | } 27 | 28 | ///Produce a slice of this string. 29 | /// 30 | ///``` 31 | ///use rustful::context::{MaybeUtf8Owned, MaybeUtf8Slice}; 32 | /// 33 | ///let owned = MaybeUtf8Owned::from("abc"); 34 | ///let slice: MaybeUtf8Slice = owned.as_slice(); 35 | ///``` 36 | pub fn as_slice(&self) -> MaybeUtf8<&Sref, &Vref> where S: AsRef, V: AsRef { 37 | match *self { 38 | MaybeUtf8::Utf8(ref s) => MaybeUtf8::Utf8(s.as_ref()), 39 | MaybeUtf8::NotUtf8(ref v) => MaybeUtf8::NotUtf8(v.as_ref()) 40 | } 41 | } 42 | 43 | ///Borrow the string if it's encoded as valid UTF-8. 44 | /// 45 | ///``` 46 | ///use rustful::context::MaybeUtf8Owned; 47 | /// 48 | ///let string = MaybeUtf8Owned::from("abc"); 49 | ///assert_eq!(Some("abc"), string.as_utf8()); 50 | ///``` 51 | pub fn as_utf8(&self) -> Option<&str> where S: AsRef { 52 | match *self { 53 | MaybeUtf8::Utf8(ref s) => Some(s.as_ref()), 54 | MaybeUtf8::NotUtf8(_) => None 55 | } 56 | } 57 | 58 | ///Borrow the string if it's encoded as valid UTF-8, or make a lossy conversion. 59 | /// 60 | ///``` 61 | ///use rustful::context::MaybeUtf8Owned; 62 | /// 63 | ///let string = MaybeUtf8Owned::from("abc"); 64 | ///assert_eq!("abc", string.as_utf8_lossy()); 65 | ///``` 66 | pub fn as_utf8_lossy(&self) -> Cow where S: AsRef, V: AsRef<[u8]> { 67 | match *self { 68 | MaybeUtf8::Utf8(ref s) => s.as_ref().into(), 69 | MaybeUtf8::NotUtf8(ref v) => String::from_utf8_lossy(v.as_ref()) 70 | } 71 | } 72 | 73 | ///Borrow the string as a slice of bytes. 74 | /// 75 | ///``` 76 | ///use rustful::context::MaybeUtf8Owned; 77 | /// 78 | ///let string = MaybeUtf8Owned::from("abc"); 79 | ///assert_eq!(b"abc", string.as_bytes()); 80 | ///``` 81 | pub fn as_bytes(&self) -> &[u8] where S: AsRef<[u8]>, V: AsRef<[u8]> { 82 | match *self { 83 | MaybeUtf8::Utf8(ref s) => s.as_ref(), 84 | MaybeUtf8::NotUtf8(ref v) => v.as_ref() 85 | } 86 | } 87 | 88 | ///Check if the string is valid UTF-8. 89 | /// 90 | ///``` 91 | ///use rustful::context::MaybeUtf8Owned; 92 | /// 93 | ///let valid = MaybeUtf8Owned::from("abc"); 94 | ///assert_eq!(valid.is_utf8(), true); 95 | /// 96 | ///let invalid = MaybeUtf8Owned::from(vec![255]); 97 | ///assert_eq!(invalid.is_utf8(), false); 98 | ///``` 99 | pub fn is_utf8(&self) -> bool { 100 | match *self { 101 | MaybeUtf8::Utf8(_) => true, 102 | MaybeUtf8::NotUtf8(_) => false 103 | } 104 | } 105 | } 106 | 107 | impl MaybeUtf8> { 108 | ///Push a single `char` to the end of the string. 109 | /// 110 | ///``` 111 | ///use rustful::context::MaybeUtf8Owned; 112 | /// 113 | ///let mut string = MaybeUtf8Owned::from("abc"); 114 | ///string.push_char('d'); 115 | ///assert_eq!("abcd", string); 116 | ///``` 117 | pub fn push_char(&mut self, c: char) { 118 | match *self { 119 | MaybeUtf8::Utf8(ref mut s) => s.push(c), 120 | MaybeUtf8::NotUtf8(ref mut v) => { 121 | //Do some witchcraft until encode_utf8 becomes a thing. 122 | let string: &mut String = unsafe { ::std::mem::transmute(v) }; 123 | string.push(c); 124 | } 125 | } 126 | } 127 | 128 | ///Push a single byte to the end of the string. The string's UTF-8 129 | ///compatibility will be reevaluated and may change each time `push_byte` 130 | ///is called. This may have a noticeable performance impact. 131 | /// 132 | ///``` 133 | ///use rustful::context::MaybeUtf8Owned; 134 | /// 135 | ///let mut string = MaybeUtf8Owned::from("abc"); 136 | ///string.push_byte(255); 137 | ///assert_eq!(string.is_utf8(), false); 138 | ///``` 139 | pub fn push_byte(&mut self, byte: u8) { 140 | self.push_bytes(&[byte]) 141 | } 142 | 143 | ///Extend the string. 144 | /// 145 | ///``` 146 | ///use rustful::context::MaybeUtf8Owned; 147 | /// 148 | ///let mut string = MaybeUtf8Owned::from("abc"); 149 | ///string.push_str("def"); 150 | ///assert_eq!("abcdef", string); 151 | ///``` 152 | pub fn push_str(&mut self, string: &str) { 153 | match *self { 154 | MaybeUtf8::Utf8(ref mut s) => s.push_str(string), 155 | MaybeUtf8::NotUtf8(ref mut v) => v.push_bytes(string.as_bytes()) 156 | } 157 | } 158 | 159 | ///Push a number of bytes to the string. The string's UTF-8 compatibility 160 | ///may change. 161 | /// 162 | ///``` 163 | ///use rustful::context::MaybeUtf8Owned; 164 | /// 165 | ///let mut string = MaybeUtf8Owned::from("abc"); 166 | ///string.push_bytes(&[100, 101, 102]); 167 | ///assert_eq!("abcdef", string); 168 | ///``` 169 | pub fn push_bytes(&mut self, bytes: &[u8]) { 170 | match ::std::str::from_utf8(bytes) { 171 | Ok(string) => self.push_str(string), 172 | Err(_) => { 173 | self.as_buffer().push_bytes(bytes); 174 | } 175 | } 176 | } 177 | 178 | ///Borrow this string as a mutable byte buffer. The string's UTF-8 179 | ///compatibility will be reevaluated when the buffer is dropped. 180 | pub fn as_buffer(&mut self) -> Buffer { 181 | let mut v = MaybeUtf8::NotUtf8(vec![]); 182 | ::std::mem::swap(self, &mut v); 183 | Buffer { 184 | bytes: v.into(), 185 | source: self 186 | } 187 | } 188 | } 189 | 190 | impl From for MaybeUtf8 { 191 | fn from(string: String) -> MaybeUtf8 { 192 | MaybeUtf8::Utf8(string) 193 | } 194 | } 195 | 196 | impl<'a, S: From<&'a str>, V> From<&'a str> for MaybeUtf8 { 197 | fn from(string: &'a str) -> MaybeUtf8 { 198 | MaybeUtf8::Utf8(string.into()) 199 | } 200 | } 201 | 202 | impl From> for MaybeUtf8> { 203 | fn from(bytes: Vec) -> MaybeUtf8> { 204 | match String::from_utf8(bytes) { 205 | Ok(string) => MaybeUtf8::Utf8(string), 206 | Err(e) => MaybeUtf8::NotUtf8(e.into_bytes()) 207 | } 208 | } 209 | } 210 | 211 | impl, V: AsRef<[u8]>> AsRef<[u8]> for MaybeUtf8 { 212 | fn as_ref(&self) -> &[u8] { 213 | match *self { 214 | MaybeUtf8::Utf8(ref s) => s.as_ref(), 215 | MaybeUtf8::NotUtf8(ref v) => v.as_ref() 216 | } 217 | } 218 | } 219 | 220 | impl, V: AsRef<[u8]>> Borrow<[u8]> for MaybeUtf8 { 221 | fn borrow(&self) -> &[u8] { 222 | self.as_ref() 223 | } 224 | } 225 | 226 | impl PartialEq for MaybeUtf8 where 227 | S: AsRef<[u8]>, 228 | V: AsRef<[u8]>, 229 | B: AsRef<[u8]> 230 | { 231 | fn eq(&self, other: &B) -> bool { 232 | self.as_ref().eq(other.as_ref()) 233 | } 234 | } 235 | 236 | impl PartialEq> for str where 237 | S: AsRef<[u8]>, 238 | V: AsRef<[u8]> 239 | { 240 | fn eq(&self, other: &MaybeUtf8) -> bool { 241 | other.eq(self) 242 | } 243 | } 244 | 245 | impl<'a, S, V> PartialEq> for &'a str where 246 | S: AsRef<[u8]>, 247 | V: AsRef<[u8]> 248 | { 249 | fn eq(&self, other: &MaybeUtf8) -> bool { 250 | other.eq(self) 251 | } 252 | } 253 | 254 | impl PartialEq> for String where 255 | S: AsRef<[u8]>, 256 | V: AsRef<[u8]> 257 | { 258 | fn eq(&self, other: &MaybeUtf8) -> bool { 259 | other.eq(self) 260 | } 261 | } 262 | 263 | impl<'a, S, V> PartialEq> for Cow<'a, str> where 264 | S: AsRef<[u8]>, 265 | V: AsRef<[u8]> 266 | { 267 | fn eq(&self, other: &MaybeUtf8) -> bool { 268 | other.eq(self.as_ref()) 269 | } 270 | } 271 | 272 | impl PartialEq> for [u8] where 273 | S: AsRef<[u8]>, 274 | V: AsRef<[u8]> 275 | { 276 | fn eq(&self, other: &MaybeUtf8) -> bool { 277 | other.eq(self) 278 | } 279 | } 280 | 281 | impl<'a, S, V> PartialEq> for &'a [u8] where 282 | S: AsRef<[u8]>, 283 | V: AsRef<[u8]> 284 | { 285 | fn eq(&self, other: &MaybeUtf8) -> bool { 286 | other.eq(self) 287 | } 288 | } 289 | 290 | impl PartialEq> for Vec where 291 | S: AsRef<[u8]>, 292 | V: AsRef<[u8]> 293 | { 294 | fn eq(&self, other: &MaybeUtf8) -> bool { 295 | other.eq(self) 296 | } 297 | } 298 | 299 | impl, V: AsRef<[u8]>> Eq for MaybeUtf8 {} 300 | 301 | impl PartialOrd for MaybeUtf8 where 302 | S: AsRef<[u8]>, 303 | V: AsRef<[u8]>, 304 | B: AsRef<[u8]> 305 | { 306 | fn partial_cmp(&self, other: &B) -> Option { 307 | self.as_ref().partial_cmp(other.as_ref()) 308 | } 309 | } 310 | 311 | impl PartialOrd> for str where 312 | S: AsRef<[u8]>, 313 | V: AsRef<[u8]> 314 | { 315 | fn partial_cmp(&self, other: &MaybeUtf8) -> Option { 316 | other.partial_cmp(self) 317 | } 318 | } 319 | 320 | impl<'a, S, V> PartialOrd> for &'a str where 321 | S: AsRef<[u8]>, 322 | V: AsRef<[u8]> 323 | { 324 | fn partial_cmp(&self, other: &MaybeUtf8) -> Option { 325 | other.partial_cmp(self) 326 | } 327 | } 328 | 329 | impl PartialOrd> for String where 330 | S: AsRef<[u8]>, 331 | V: AsRef<[u8]> 332 | { 333 | fn partial_cmp(&self, other: &MaybeUtf8) -> Option { 334 | other.partial_cmp(self) 335 | } 336 | } 337 | 338 | impl<'a, S, V> PartialOrd> for Cow<'a, str> where 339 | S: AsRef<[u8]>, 340 | V: AsRef<[u8]> 341 | { 342 | fn partial_cmp(&self, other: &MaybeUtf8) -> Option { 343 | other.partial_cmp(self.as_ref()) 344 | } 345 | } 346 | 347 | impl PartialOrd> for [u8] where 348 | S: AsRef<[u8]>, 349 | V: AsRef<[u8]> 350 | { 351 | fn partial_cmp(&self, other: &MaybeUtf8) -> Option { 352 | other.partial_cmp(self) 353 | } 354 | } 355 | 356 | impl<'a, S, V> PartialOrd> for &'a [u8] where 357 | S: AsRef<[u8]>, 358 | V: AsRef<[u8]> 359 | { 360 | fn partial_cmp(&self, other: &MaybeUtf8) -> Option { 361 | other.partial_cmp(self) 362 | } 363 | } 364 | 365 | impl PartialOrd> for Vec where 366 | S: AsRef<[u8]>, 367 | V: AsRef<[u8]> 368 | { 369 | fn partial_cmp(&self, other: &MaybeUtf8) -> Option { 370 | other.partial_cmp(self) 371 | } 372 | } 373 | 374 | impl, V: AsRef<[u8]>> Ord for MaybeUtf8 { 375 | fn cmp(&self, other: &MaybeUtf8) -> Ordering { 376 | self.partial_cmp(other).unwrap() 377 | } 378 | } 379 | 380 | impl, V: AsRef<[u8]>> Hash for MaybeUtf8 { 381 | fn hash(&self, hasher: &mut H) { 382 | self.as_ref().hash(hasher) 383 | } 384 | } 385 | 386 | impl, V: Into>> Into for MaybeUtf8 { 387 | fn into(self) -> String { 388 | match self { 389 | MaybeUtf8::Utf8(s) => s.into(), 390 | MaybeUtf8::NotUtf8(v) => { 391 | let bytes = v.into(); 392 | match String::from_utf8_lossy(&bytes) { 393 | Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(bytes) }, 394 | Cow::Owned(s) => s 395 | } 396 | } 397 | } 398 | } 399 | } 400 | 401 | impl>, V: Into>> Into> for MaybeUtf8 { 402 | fn into(self) -> Vec { 403 | match self { 404 | MaybeUtf8::Utf8(s) => s.into(), 405 | MaybeUtf8::NotUtf8(v) => v.into() 406 | } 407 | } 408 | } 409 | 410 | impl, V: AsRef<[u8]>> Deref for MaybeUtf8 { 411 | type Target=[u8]; 412 | 413 | fn deref(&self) -> &[u8] { 414 | self.as_ref() 415 | } 416 | } 417 | 418 | ///A byte buffer for more efficient `MaybeUtf8` manipulation. 419 | /// 420 | ///The buffer is essentially a `&mut Vec` that will be checked for UTF-8 421 | ///compatibility when dropped. It comes with a few extra convenience methods. 422 | pub struct Buffer<'a> { 423 | bytes: Vec, 424 | source: &'a mut MaybeUtf8> 425 | } 426 | 427 | impl<'a> Buffer<'a> { 428 | ///Push a number of bytes to the buffer in a relatively efficient way. 429 | /// 430 | ///``` 431 | ///use rustful::context::MaybeUtf8Owned; 432 | /// 433 | ///let mut string = MaybeUtf8Owned::new(); 434 | /// 435 | ///{ 436 | /// let mut buffer = string.as_buffer(); 437 | /// buffer.push_bytes("abc".as_bytes()); 438 | ///} 439 | /// 440 | ///assert!(string.is_utf8()); 441 | ///assert_eq!("abc", string); 442 | ///``` 443 | pub fn push_bytes(&mut self, bytes: &[u8]) { 444 | self.bytes.push_bytes(bytes) 445 | } 446 | 447 | ///Push a single `char` to the end of the buffer. 448 | /// 449 | ///``` 450 | ///use rustful::context::MaybeUtf8Owned; 451 | /// 452 | ///let mut string = MaybeUtf8Owned::new(); 453 | /// 454 | ///{ 455 | /// let mut buffer = string.as_buffer(); 456 | /// buffer.push_char('å'); 457 | /// buffer.push_char('1'); 458 | /// buffer.push_char('€'); 459 | ///} 460 | /// 461 | ///assert!(string.is_utf8()); 462 | ///assert_eq!("å1€", string); 463 | ///``` 464 | pub fn push_char(&mut self, c: char) { 465 | //Do some witchcraft until encode_utf8 becomes a thing. 466 | let string: &mut String = unsafe { ::std::mem::transmute(&mut self.bytes) }; 467 | string.push(c); 468 | } 469 | } 470 | 471 | impl<'a> Deref for Buffer<'a> { 472 | type Target = Vec; 473 | 474 | fn deref(&self) -> &Vec { 475 | &self.bytes 476 | } 477 | } 478 | 479 | impl<'a> DerefMut for Buffer<'a> { 480 | fn deref_mut(&mut self) -> &mut Vec { 481 | &mut self.bytes 482 | } 483 | } 484 | 485 | impl<'a> Drop for Buffer<'a> { 486 | fn drop(&mut self) { 487 | let mut v = vec![]; 488 | ::std::mem::swap(&mut v, &mut self.bytes); 489 | *self.source = v.into(); 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/context/mod.rs: -------------------------------------------------------------------------------- 1 | //!Handler context and request body reading extensions. 2 | //! 3 | //!#Context 4 | //! 5 | //!The [`Context`][context] contains all the input data for the request 6 | //!handlers, as well as some utilities. This is where request data, like 7 | //!headers, client address and the request body can be retrieved from and it 8 | //!can safely be picked apart, since its ownership is transferred to the 9 | //!handler. 10 | //! 11 | //!##Accessing Headers 12 | //! 13 | //!The headers are stored in the `headers` field. See the [`Headers`][headers] 14 | //!struct for more information about how to access them. 15 | //! 16 | //!``` 17 | //!use rustful::{Context, Response}; 18 | //!use rustful::header::UserAgent; 19 | //! 20 | //!fn my_handler(context: Context, response: Response) { 21 | //! if let Some(&UserAgent(ref user_agent)) = context.headers.get() { 22 | //! response.send(format!("got user agent string \"{}\"", user_agent)); 23 | //! } else { 24 | //! response.send("no user agent string provided"); 25 | //! } 26 | //!} 27 | //!``` 28 | //! 29 | //!##Path Variables 30 | //! 31 | //!A router may collect variable data from paths (for example `id` in 32 | //!`/products/:id`). The values from these variables can be accessed through 33 | //!the `variables` field. 34 | //! 35 | //!``` 36 | //!use rustful::{Context, Response}; 37 | //! 38 | //!fn my_handler(context: Context, response: Response) { 39 | //! if let Some(id) = context.variables.get("id") { 40 | //! response.send(format!("asking for product with id \"{}\"", id)); 41 | //! } else { 42 | //! //This will usually not happen, unless the handler is also 43 | //! //assigned to a path without the `id` variable 44 | //! response.send("no id provided"); 45 | //! } 46 | //!} 47 | //!``` 48 | //! 49 | //!##Other URL Parts 50 | //! 51 | //! * Query variables (`http://example.com?a=b&c=d`) can be found in the 52 | //!`query` field and they are accessed in exactly the same fashion as path 53 | //!variables are used. 54 | //! 55 | //! * The fragment (`http://example.com#foo`) is also parsed and can be 56 | //!accessed through `fragment` as an optional `String`. 57 | //! 58 | //!##Global Data 59 | //! 60 | //!There is also infrastructure for globally accessible data, that can be 61 | //!accessed through the `global` field. This is meant to provide a place for 62 | //!things like database connections or cached data that should be available to 63 | //!all handlers. The storage space itself is immutable when the server has 64 | //!started, so the only way to change it is through some kind of inner 65 | //!mutability. 66 | //! 67 | //!``` 68 | //!# #[macro_use] extern crate rustful; 69 | //!#[macro_use] extern crate log; 70 | //!use rustful::{Context, Response}; 71 | //!use rustful::StatusCode::InternalServerError; 72 | //! 73 | //!fn my_handler(context: Context, mut response: Response) { 74 | //! if let Some(some_wise_words) = context.global.get::<&str>() { 75 | //! response.send(format!("food for thought: {}", some_wise_words)); 76 | //! } else { 77 | //! error!("there should be a string literal in `global`"); 78 | //! response.set_status(InternalServerError); 79 | //! } 80 | //!} 81 | //! 82 | //!# fn main() {} 83 | //!``` 84 | //! 85 | //!##Request Body 86 | //! 87 | //!The body will not be read in advance, unlike the other parts of the 88 | //!request. It is instead available as a `BodyReader` in the field `body`, 89 | //!through which it can be read and parsed as various data formats, like JSON 90 | //!and query strings. The documentation for [`BodyReader`][body_reader] gives 91 | //!more examples. 92 | //! 93 | //!``` 94 | //!use std::io::{BufReader, BufRead}; 95 | //!use rustful::{Context, Response}; 96 | //! 97 | //!fn my_handler(context: Context, response: Response) { 98 | //! let mut numbered_lines = BufReader::new(context.body).lines().enumerate(); 99 | //! let mut writer = response.into_chunked(); 100 | //! 101 | //! while let Some((line_no, Ok(line))) = numbered_lines.next() { 102 | //! writer.send(format!("{}: {}", line_no + 1, line)); 103 | //! } 104 | //!} 105 | //!``` 106 | //! 107 | //![context]: struct.Context.html 108 | //![headers]: ../header/struct.Headers.html 109 | //![log]: ../log/index.html 110 | //![body_reader]: body/struct.BodyReader.html 111 | 112 | use std::net::{SocketAddr, SocketAddrV4, Ipv4Addr}; 113 | use std::fmt; 114 | use std::borrow::Cow; 115 | 116 | use HttpVersion; 117 | use Method; 118 | use header::Headers; 119 | use server::Global; 120 | 121 | use self::body::BodyReader; 122 | use self::hypermedia::Link; 123 | 124 | pub mod body; 125 | pub mod hypermedia; 126 | 127 | mod maybe_utf8; 128 | pub use self::maybe_utf8::{MaybeUtf8, MaybeUtf8Owned, MaybeUtf8Slice, Buffer}; 129 | 130 | mod parameters; 131 | pub use self::parameters::Parameters; 132 | 133 | ///A container for handler input, like request data and utilities. 134 | pub struct Context<'a, 'b: 'a, 'l, 'g> { 135 | ///Headers from the HTTP request. 136 | pub headers: Headers, 137 | 138 | ///The HTTP version used in the request. 139 | pub http_version: HttpVersion, 140 | 141 | ///The client address 142 | pub address: SocketAddr, 143 | 144 | ///The HTTP method. 145 | pub method: Method, 146 | 147 | ///The requested path. 148 | pub uri_path: UriPath, 149 | 150 | ///Hyperlinks from the current endpoint. 151 | pub hyperlinks: Vec>, 152 | 153 | ///Route variables. 154 | pub variables: Parameters, 155 | 156 | ///Query variables from the path. 157 | pub query: Parameters, 158 | 159 | ///The fragment part of the URL (after #), if provided. 160 | pub fragment: Option, 161 | 162 | ///Globally accessible data. 163 | pub global: &'g Global, 164 | 165 | ///A reader for the request body. 166 | pub body: BodyReader<'a, 'b>, 167 | } 168 | 169 | impl<'a, 'b, 'l, 'g> Context<'a, 'b, 'l, 'g> { 170 | ///Create a context with minimal setup, for testing purposes. 171 | pub fn mock>(method: Method, path: P, headers: Headers, global: &'g Global) -> Context<'static, 'static, 'l, 'g> { 172 | let body = BodyReader::mock(&headers); 173 | 174 | Context { 175 | headers: headers, 176 | http_version: HttpVersion::Http11, 177 | address: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 80)), 178 | method: method, 179 | uri_path: UriPath::Path(path.into().into()), 180 | hyperlinks: vec![], 181 | variables: Parameters::new(), 182 | query: Parameters::new(), 183 | fragment: None, 184 | global: global, 185 | body: body 186 | } 187 | } 188 | 189 | ///Replace the hyperlinks. This consumes the context and returns a new one 190 | ///with a different lifetime, together with the old hyperlinks. 191 | pub fn replace_hyperlinks<'n>(self, hyperlinks: Vec>) -> (Context<'a, 'b, 'n, 'g>, Vec>) { 192 | let old_links = self.hyperlinks; 193 | 194 | ( 195 | Context { 196 | headers: self.headers, 197 | http_version: self.http_version, 198 | address: self.address, 199 | method: self.method, 200 | uri_path: self.uri_path, 201 | hyperlinks: hyperlinks, 202 | variables: self.variables, 203 | query: self.query, 204 | fragment: self.fragment, 205 | global: self.global, 206 | body: self.body, 207 | }, 208 | old_links 209 | ) 210 | } 211 | } 212 | 213 | ///A URI Path that can be a path or an asterisk (`*`). 214 | /// 215 | ///The URI Path may be an invalid UTF-8 path and it is therefore represented as a 216 | ///percent decoded byte vector, but can easily be parsed as a string. 217 | #[derive(Clone, Debug, PartialEq, Eq)] 218 | pub enum UriPath { 219 | ///A path URI. 220 | Path(MaybeUtf8Owned), 221 | ///An asterisk (`*`) URI. 222 | Asterisk 223 | } 224 | 225 | impl UriPath { 226 | ///Borrow the URI as a raw path. 227 | pub fn as_path(&self) -> Option { 228 | match *self { 229 | UriPath::Path(ref path) => Some(path.as_slice()), 230 | UriPath::Asterisk => None 231 | } 232 | } 233 | 234 | ///Borrow the URI as a UTF-8 path, if valid. 235 | pub fn as_utf8_path(&self) -> Option<&str> { 236 | match *self { 237 | UriPath::Path(ref path) => path.as_utf8(), 238 | UriPath::Asterisk => None 239 | } 240 | } 241 | 242 | ///Borrow the URI as a UTF-8 path, if valid, or convert it to a valid 243 | ///UTF-8 string. 244 | pub fn as_utf8_path_lossy(&self) -> Option> { 245 | match *self { 246 | UriPath::Path(ref path) => Some(path.as_utf8_lossy()), 247 | UriPath::Asterisk => None 248 | } 249 | } 250 | 251 | ///Check if the URI is a path. 252 | pub fn is_path(&self) -> bool { 253 | match *self { 254 | UriPath::Path(_) => true, 255 | UriPath::Asterisk => false 256 | } 257 | } 258 | 259 | ///Check if the URI is an asterisk (`*`). 260 | pub fn is_asterisk(&self) -> bool { 261 | match *self { 262 | UriPath::Path(_) => false, 263 | UriPath::Asterisk => true 264 | } 265 | } 266 | } 267 | 268 | impl fmt::Display for UriPath { 269 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 270 | self.as_utf8_path_lossy().unwrap_or_else(|| "*".into()).fmt(f) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/context/parameters.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::{HashMap, Entry}; 2 | use std::iter::FromIterator; 3 | use std::fmt; 4 | use std::ops::{Deref, DerefMut}; 5 | use std::str::FromStr; 6 | use std::hash::Hash; 7 | use std::borrow::Cow; 8 | 9 | use context::MaybeUtf8Owned; 10 | 11 | ///An extended `HashMap` with extra functionality for value parsing. 12 | /// 13 | ///Some of the methods from `HashMap` has been wrapped to provide a more 14 | ///ergonomic API, where anything that can be represented as a byte slice can 15 | ///be used as a key. 16 | #[derive(Clone)] 17 | pub struct Parameters(HashMap); 18 | 19 | impl Parameters { 20 | ///Create an empty `Parameters`. 21 | pub fn new() -> Parameters { 22 | Parameters(HashMap::new()) 23 | } 24 | 25 | ///Get a parameter as a UTF-8 string. A lossy conversion will be performed 26 | ///if it's not encoded as UTF-8. Use `get_raw` to get the original data. 27 | pub fn get<'a, K: ?Sized>(&'a self, key: &K) -> Option> where 28 | K: Hash + Eq + AsRef<[u8]> 29 | { 30 | self.0.get(key.as_ref()).map(|v| v.as_utf8_lossy()) 31 | } 32 | 33 | ///Get a parameter that may or may not be a UTF-8 string. 34 | pub fn get_raw<'a, K: ?Sized>(&'a self, key: &K) -> Option<&'a MaybeUtf8Owned> where 35 | K: Hash + Eq + AsRef<[u8]> 36 | { 37 | self.0.get(key.as_ref()) 38 | } 39 | 40 | ///Get a mutable parameter that may or may not be a UTF-8 string. 41 | pub fn get_mut<'a, K: ?Sized>(&'a mut self, key: &K) -> Option<&'a mut MaybeUtf8Owned> where 42 | K: Hash + Eq + AsRef<[u8]> 43 | { 44 | self.0.get_mut(key.as_ref()) 45 | } 46 | 47 | ///Returns true if a parameter with the given key exists. 48 | pub fn contains_key(&self, key: &K) -> bool where 49 | K: Hash + Eq + AsRef<[u8]> 50 | { 51 | self.0.contains_key(key.as_ref()) 52 | } 53 | 54 | ///Insert a parameter. 55 | pub fn insert(&mut self, key: K, value: V) -> Option where 56 | K: Into, V: Into 57 | { 58 | self.0.insert(key.into(), value.into()) 59 | } 60 | 61 | ///Remove a parameter and return it. 62 | pub fn remove(&mut self, key: &K) -> Option where 63 | K: Hash + Eq + AsRef<[u8]> 64 | { 65 | self.0.remove(key.as_ref()) 66 | } 67 | 68 | ///Gets the given key's corresponding parameter in the map for in-place 69 | ///manipulation. 70 | pub fn entry(&mut self, key: K) -> Entry where K: Into { 71 | self.0.entry(key.into()) 72 | } 73 | 74 | ///Try to parse an entry as `T`, if it exists. The error will be `None` if 75 | ///the entry does not exist, and `Some` if it does exists, but the parsing 76 | ///failed. 77 | /// 78 | ///``` 79 | ///# use rustful::{Context, Response}; 80 | ///fn my_handler(context: Context, response: Response) { 81 | /// let age: Result = context.variables.parse("age"); 82 | /// match age { 83 | /// Ok(age) => response.send(format!("age: {}", age)), 84 | /// Err(Some(_)) => response.send("age must be a positive number"), 85 | /// Err(None) => response.send("no age provided") 86 | /// } 87 | ///} 88 | ///``` 89 | pub fn parse(&self, key: &K) -> Result> where 90 | K: Hash + Eq + AsRef<[u8]>, 91 | T: FromStr 92 | { 93 | if let Some(val) = self.0.get(key.as_ref()) { 94 | val.as_utf8_lossy().parse().map_err(Some) 95 | } else { 96 | Err(None) 97 | } 98 | } 99 | 100 | ///Try to parse an entry as `T`, if it exists, or return the default in 101 | ///`or`. 102 | /// 103 | ///``` 104 | ///# use rustful::{Context, Response}; 105 | ///fn my_handler(context: Context, response: Response) { 106 | /// let page = context.variables.parse_or("page", 0u8); 107 | /// response.send(format!("current page: {}", page)); 108 | ///} 109 | ///``` 110 | pub fn parse_or(&self, key: &K, or: T) -> T where 111 | K: Hash + Eq + AsRef<[u8]>, 112 | T: FromStr 113 | { 114 | self.parse(key).unwrap_or(or) 115 | } 116 | 117 | ///Try to parse an entry as `T`, if it exists, or create a new one using 118 | ///`or_else`. The `or_else` function will receive the parsing error if the 119 | ///value existed, but was impossible to parse. 120 | /// 121 | ///``` 122 | ///# use rustful::{Context, Response}; 123 | ///# fn do_heavy_stuff() -> u8 {0} 124 | ///fn my_handler(context: Context, response: Response) { 125 | /// let science = context.variables.parse_or_else("science", |_| do_heavy_stuff()); 126 | /// response.send(format!("science value: {}", science)); 127 | ///} 128 | ///``` 129 | pub fn parse_or_else(&self, key: &K, or_else: F) -> T where 130 | K: Hash + Eq + AsRef<[u8]>, 131 | T: FromStr, 132 | F: FnOnce(Option) -> T 133 | { 134 | self.parse(key).unwrap_or_else(or_else) 135 | } 136 | } 137 | 138 | impl Deref for Parameters { 139 | type Target = HashMap; 140 | 141 | fn deref(&self) -> &HashMap { 142 | &self.0 143 | } 144 | } 145 | 146 | impl DerefMut for Parameters { 147 | fn deref_mut(&mut self) -> &mut HashMap { 148 | &mut self.0 149 | } 150 | } 151 | 152 | impl AsRef> for Parameters { 153 | fn as_ref(&self) -> &HashMap { 154 | &self.0 155 | } 156 | } 157 | 158 | impl AsMut> for Parameters { 159 | fn as_mut(&mut self) -> &mut HashMap { 160 | &mut self.0 161 | } 162 | } 163 | 164 | impl Into> for Parameters { 165 | fn into(self) -> HashMap { 166 | self.0 167 | } 168 | } 169 | 170 | impl From> for Parameters { 171 | fn from(map: HashMap) -> Parameters { 172 | Parameters(map) 173 | } 174 | } 175 | 176 | impl PartialEq for Parameters { 177 | fn eq(&self, other: &Parameters) -> bool { 178 | self.0.eq(&other.0) 179 | } 180 | } 181 | 182 | impl Eq for Parameters {} 183 | 184 | impl fmt::Debug for Parameters { 185 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 186 | self.0.fmt(f) 187 | } 188 | } 189 | 190 | impl Default for Parameters { 191 | fn default() -> Parameters { 192 | Parameters::new() 193 | } 194 | } 195 | 196 | impl IntoIterator for Parameters { 197 | type IntoIter = as IntoIterator>::IntoIter; 198 | type Item = (MaybeUtf8Owned, MaybeUtf8Owned); 199 | 200 | fn into_iter(self) -> Self::IntoIter { 201 | self.0.into_iter() 202 | } 203 | } 204 | 205 | impl<'a> IntoIterator for &'a Parameters { 206 | type IntoIter = <&'a HashMap as IntoIterator>::IntoIter; 207 | type Item = (&'a MaybeUtf8Owned, &'a MaybeUtf8Owned); 208 | 209 | fn into_iter(self) -> Self::IntoIter { 210 | (&self.0).into_iter() 211 | } 212 | } 213 | 214 | impl<'a> IntoIterator for &'a mut Parameters { 215 | type IntoIter = <&'a mut HashMap as IntoIterator>::IntoIter; 216 | type Item = (&'a MaybeUtf8Owned, &'a mut MaybeUtf8Owned); 217 | 218 | fn into_iter(self) -> Self::IntoIter { 219 | (&mut self.0).into_iter() 220 | } 221 | } 222 | 223 | impl, V: Into> FromIterator<(K, V)> for Parameters { 224 | fn from_iter>(iterable: T) -> Parameters { 225 | HashMap::from_iter(iterable.into_iter().map(|(k, v)| (k.into(), v.into()))).into() 226 | } 227 | } 228 | 229 | impl, V: Into> Extend<(K, V)> for Parameters { 230 | fn extend>(&mut self, iter: T) { 231 | self.0.extend(iter.into_iter().map(|(k, v)| (k.into(), v.into()))) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | //!File related utilities. 2 | 3 | use std::path::{Path, Component}; 4 | 5 | use mime::{Mime, TopLevel, SubLevel}; 6 | 7 | include!(concat!(env!("OUT_DIR"), "/mime.rs")); 8 | 9 | ///Returns the MIME type from a given file extension, if known. 10 | /// 11 | ///The file extension to MIME type mapping is based on [data from the Apache 12 | ///server][apache]. 13 | /// 14 | ///``` 15 | ///use rustful::file::ext_to_mime; 16 | ///use rustful::mime::Mime; 17 | ///use rustful::mime::TopLevel::Image; 18 | ///use rustful::mime::SubLevel::Jpeg; 19 | /// 20 | ///let mime = ext_to_mime("jpg"); 21 | ///assert_eq!(mime, Some(Mime(Image, Jpeg, vec![]))); 22 | ///``` 23 | /// 24 | ///[apache]: http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=markup 25 | pub fn ext_to_mime(ext: &str) -> Option { 26 | MIME.get(ext).map(|&(ref top, ref sub)| { 27 | Mime(top.into(), sub.into(), vec![]) 28 | }) 29 | } 30 | 31 | enum MaybeKnown { 32 | Known(T), 33 | Unknown(&'static str) 34 | } 35 | 36 | impl<'a> Into for &'a MaybeKnown { 37 | fn into(self) -> TopLevel { 38 | match *self { 39 | MaybeKnown::Known(ref t) => t.clone(), 40 | MaybeKnown::Unknown(t) => TopLevel::Ext(t.into()) 41 | } 42 | } 43 | } 44 | 45 | impl<'a> Into for &'a MaybeKnown { 46 | fn into(self) -> SubLevel { 47 | match *self { 48 | MaybeKnown::Known(ref s) => s.clone(), 49 | MaybeKnown::Unknown(s) => SubLevel::Ext(s.into()) 50 | } 51 | } 52 | } 53 | 54 | ///Check if a path tries to escape its parent directory. 55 | /// 56 | ///Forbidden path components: 57 | /// 58 | /// * Root directory 59 | /// * Prefixes (e.g. `C:` on Windows) 60 | /// * Parent directory 61 | /// 62 | ///Allowed path components: 63 | /// 64 | /// * "Normal" components (e.g. `res/scripts`) 65 | /// * Current directory 66 | /// 67 | ///The first forbidden component is returned if the path is invalid. 68 | /// 69 | ///``` 70 | ///use std::path::Component; 71 | ///use rustful::file::check_path; 72 | /// 73 | ///let bad_path = ".."; 74 | /// 75 | ///assert_eq!(check_path(bad_path), Err(Component::ParentDir)); 76 | ///``` 77 | pub fn check_path>(path: &P) -> Result<(), Component> { 78 | for component in path.as_ref().components() { 79 | match component { 80 | c @ Component::RootDir | 81 | c @ Component::Prefix(_) | 82 | c @ Component::ParentDir => return Err(c), 83 | Component::Normal(_) | Component::CurDir => {} 84 | } 85 | } 86 | 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /src/filter.rs: -------------------------------------------------------------------------------- 1 | //!Request and context filters. 2 | 3 | use anymap::AnyMap; 4 | 5 | use StatusCode; 6 | use header::Headers; 7 | 8 | use context::Context; 9 | 10 | use response::Data; 11 | use server::Global; 12 | 13 | ///Contextual tools for filters. 14 | pub struct FilterContext<'a> { 15 | ///Shared storage for filters. It is local to the current request and 16 | ///accessible from the handler and all of the filters. It can be used to 17 | ///send data between these units. 18 | pub storage: &'a mut AnyMap, 19 | 20 | ///Globally accessible data. 21 | pub global: &'a Global, 22 | } 23 | 24 | ///A trait for context filters. 25 | /// 26 | ///They are able to modify and react to a `Context` before it's sent to the handler. 27 | pub trait ContextFilter: Send + Sync { 28 | ///Try to modify the handler `Context`. 29 | fn modify(&self, context: FilterContext, request_context: &mut Context) -> ContextAction; 30 | } 31 | 32 | ///The result from a context filter. 33 | #[derive(Clone)] 34 | pub enum ContextAction { 35 | ///Continue to the next filter in the stack. 36 | Next, 37 | 38 | ///Abort and set HTTP status. 39 | Abort(StatusCode) 40 | } 41 | 42 | impl<'a> ContextAction { 43 | ///Continue to the next filter in the stack. 44 | pub fn next() -> ContextAction { 45 | ContextAction::Next 46 | } 47 | 48 | ///Abort and set HTTP status. 49 | pub fn abort(status: StatusCode) -> ContextAction { 50 | ContextAction::Abort(status) 51 | } 52 | } 53 | 54 | 55 | ///A trait for response filters. 56 | /// 57 | ///They are able to modify headers and data before it gets written in the response. 58 | pub trait ResponseFilter: Send + Sync { 59 | ///Set or modify headers before they are sent to the client and maybe initiate the body. 60 | fn begin(&self, context: FilterContext, status: StatusCode, headers: &mut Headers) -> 61 | (StatusCode, ResponseAction); 62 | 63 | ///Handle content before writing it to the body. 64 | fn write<'a>(&'a self, context: FilterContext, content: Option>) -> ResponseAction; 65 | 66 | ///End of body writing. Last chance to add content. 67 | fn end(&self, context: FilterContext) -> ResponseAction; 68 | } 69 | 70 | ///The result from a response filter. 71 | #[derive(Clone)] 72 | pub enum ResponseAction<'a> { 73 | ///Continue to the next filter and maybe write data. 74 | Next(Option>), 75 | 76 | ///Do not continue to the next filter. 77 | SilentAbort, 78 | 79 | ///Abort with an error. 80 | Abort(String) 81 | } 82 | 83 | impl<'a> ResponseAction<'a> { 84 | ///Continue to the next filter and maybe write data. 85 | pub fn next>>(data: Option) -> ResponseAction<'a> { 86 | ResponseAction::Next(data.map(|d| d.into())) 87 | } 88 | 89 | ///Do not continue to the next filter. 90 | pub fn silent_abort() -> ResponseAction<'a> { 91 | ResponseAction::SilentAbort 92 | } 93 | 94 | ///Abort with an error. 95 | pub fn abort(message: String) -> ResponseAction<'a> { 96 | ResponseAction::Abort(message) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/handler/method_router.rs: -------------------------------------------------------------------------------- 1 | //! A router that selects a handler from an HTTP method. 2 | 3 | use std::collections::hash_map::{HashMap, Entry}; 4 | 5 | use {Method, StatusCode}; 6 | use context::hypermedia::Link; 7 | use handler::{HandleRequest, Environment, Build, FromHandler, BuilderContext, ApplyContext, Merge}; 8 | 9 | /// A router that selects a handler from an HTTP method. 10 | /// 11 | /// It's a simple mapping between `Method` and a router `T`, while the 12 | /// requested path is ignored. It's therefore a good idea to pair a 13 | /// `MethodRouter` with an exhaustive path router of some sort. 14 | #[derive(Clone)] 15 | pub struct MethodRouter { 16 | handlers: HashMap, 17 | } 18 | 19 | impl MethodRouter { 20 | /// Create an empty `MethodRouter`. 21 | pub fn new() -> MethodRouter { 22 | MethodRouter::default() 23 | } 24 | 25 | /// Build the router and its children using a chaninable API. 26 | /// 27 | /// ``` 28 | /// use rustful::{Context, Response}; 29 | /// use rustful::handler::MethodRouter; 30 | /// 31 | /// fn get(_context: Context, response: Response) { 32 | /// response.send("A GET request."); 33 | /// } 34 | /// 35 | /// fn post(_context: Context, response: Response) { 36 | /// response.send("A POST request."); 37 | /// } 38 | /// 39 | /// let mut method_router = MethodRouter::::new(); 40 | /// 41 | /// method_router.build().many(|mut method_router|{ 42 | /// method_router.on_get(get as fn(Context, Response)); 43 | /// method_router.on_post(post); 44 | /// }); 45 | /// ``` 46 | pub fn build(&mut self) -> Builder { 47 | self.get_builder(BuilderContext::new()) 48 | } 49 | 50 | /// Insert a handler that will listen for a specific status code. 51 | /// 52 | /// ``` 53 | /// use rustful::{Context, Response, Method}; 54 | /// use rustful::handler::{MethodRouter, TreeRouter}; 55 | /// 56 | /// let route_tree = TreeRouter::>::new(); 57 | /// //Fill route_tree with handlers... 58 | /// 59 | /// let mut method_router = MethodRouter::new(); 60 | /// method_router.insert(Method::Get, route_tree); 61 | /// ``` 62 | pub fn insert(&mut self, method: Method, handler: T) { 63 | self.handlers.insert(method, handler); 64 | } 65 | } 66 | 67 | impl HandleRequest for MethodRouter { 68 | fn handle_request<'a, 'b, 'l, 'g>(&self, mut environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>> { 69 | if let Some(handler) = self.handlers.get(&environment.context.method) { 70 | handler.handle_request(environment) 71 | } else { 72 | environment.response.set_status(StatusCode::MethodNotAllowed); 73 | Err(environment) 74 | } 75 | } 76 | 77 | fn hyperlinks<'a>(&'a self, base: Link<'a>) -> Vec> { 78 | self.handlers.iter().flat_map(|(method, handler)| { 79 | let mut link = base.clone(); 80 | link.method = Some(method.clone()); 81 | handler.hyperlinks(link) 82 | }).collect() 83 | } 84 | } 85 | 86 | impl Default for MethodRouter { 87 | fn default() -> MethodRouter { 88 | MethodRouter { 89 | handlers: HashMap::new(), 90 | } 91 | } 92 | } 93 | 94 | impl<'a, T: 'a> Build<'a> for MethodRouter { 95 | type Builder = Builder<'a, T>; 96 | 97 | fn get_builder(&'a mut self, context: BuilderContext) -> Self::Builder { 98 | Builder { 99 | router: self, 100 | context: context 101 | } 102 | } 103 | } 104 | 105 | impl ApplyContext for MethodRouter { 106 | fn apply_context(&mut self, context: BuilderContext) { 107 | for (_, handler) in &mut self.handlers { 108 | handler.apply_context(context.clone()); 109 | } 110 | } 111 | 112 | fn prepend_context(&mut self, context: BuilderContext) { 113 | for (_, handler) in &mut self.handlers { 114 | handler.prepend_context(context.clone()); 115 | } 116 | } 117 | } 118 | 119 | impl Merge for MethodRouter { 120 | fn merge(&mut self, other: MethodRouter) { 121 | println!("merging {:} methods with {:} methods", self.handlers.len(), other.handlers.len()); 122 | for (method, handler) in other.handlers { 123 | println!("merging {:}", method); 124 | match self.handlers.entry(method) { 125 | Entry::Vacant(entry) => { entry.insert(handler); }, 126 | Entry::Occupied(mut entry) => entry.get_mut().merge(handler), 127 | } 128 | } 129 | } 130 | } 131 | 132 | /// A builder for a `MethodRouter`. 133 | pub struct Builder<'a, T: 'a> { 134 | router: &'a mut MethodRouter, 135 | context: BuilderContext 136 | } 137 | 138 | impl<'a, T> Builder<'a, T> { 139 | /// Perform more than one operation on this builder. 140 | /// 141 | /// ``` 142 | /// use rustful::{Context, Response}; 143 | /// use rustful::handler::MethodRouter; 144 | /// 145 | /// fn get(_context: Context, response: Response) { 146 | /// response.send("A GET request."); 147 | /// } 148 | /// 149 | /// fn post(_context: Context, response: Response) { 150 | /// response.send("A POST request."); 151 | /// } 152 | /// 153 | /// let mut method_router = MethodRouter::::new(); 154 | /// 155 | /// method_router.build().many(|mut method_router|{ 156 | /// method_router.on_get(get as fn(Context, Response)); 157 | /// method_router.on_post(post); 158 | /// }); 159 | /// ``` 160 | pub fn many)>(&mut self, build: F) -> &mut Builder<'a, T> { 161 | build(self); 162 | self 163 | } 164 | 165 | /// Insert a handler for GET requests. 166 | /// 167 | /// ``` 168 | /// use rustful::{Context, Response}; 169 | /// use rustful::handler::MethodRouter; 170 | /// 171 | /// fn handler(_context: Context, response: Response) { 172 | /// response.send("Hello world!"); 173 | /// } 174 | /// 175 | /// let mut method_router = MethodRouter::::new(); 176 | /// 177 | /// method_router.build().on_get(handler as fn(Context, Response)); 178 | /// ``` 179 | pub fn on_get(&mut self, handler: H) where T: FromHandler { 180 | self.on(Method::Get, handler); 181 | } 182 | 183 | /// Insert a handler for POST requests. See `on_get` for an example. 184 | pub fn on_post(&mut self, handler: H) where T: FromHandler { 185 | self.on(Method::Post, handler); 186 | } 187 | 188 | /// Insert a handler for PATCH requests. See `on_get` for an example. 189 | pub fn on_patch(&mut self, handler: H) where T: FromHandler { 190 | self.on(Method::Patch, handler); 191 | } 192 | 193 | /// Insert a handler for PUT requests. See `on_get` for an example. 194 | pub fn on_put(&mut self, handler: H) where T: FromHandler { 195 | self.on(Method::Put, handler); 196 | } 197 | 198 | /// Insert a handler for DELETE requests. See `on_get` for an example. 199 | pub fn on_delete(&mut self, handler: H) where T: FromHandler { 200 | self.on(Method::Delete, handler); 201 | } 202 | 203 | /// Insert a handler for OPTION requests. See `on_get` for an example. 204 | pub fn on_options(&mut self, handler: H) where T: FromHandler { 205 | self.on(Method::Options, handler); 206 | } 207 | 208 | /// Insert a handler for HEAD requests. See `on_get` for an example. 209 | pub fn on_head(&mut self, handler: H) where T: FromHandler { 210 | self.on(Method::Head, handler); 211 | } 212 | 213 | /// Insert a handler for CONNECT requests. See `on_get` for an example. 214 | pub fn on_connect(&mut self, handler: H) where T: FromHandler { 215 | self.on(Method::Connect, handler); 216 | } 217 | 218 | /// Insert a handler, similar to `on_get`, but for any HTTP method. 219 | /// 220 | /// ``` 221 | /// use rustful::{Context, Response}; 222 | /// use rustful::handler::MethodRouter; 223 | /// # fn choose_a_method() -> rustful::Method { rustful::Method::Get } 224 | /// 225 | /// fn handler(_context: Context, response: Response) { 226 | /// response.send("Hello world!"); 227 | /// } 228 | /// 229 | /// let mut method_router = MethodRouter::::new(); 230 | /// 231 | /// method_router.build().on(choose_a_method(), handler as fn(Context, Response)); 232 | /// ``` 233 | pub fn on(&mut self, method: Method, handler: H) where T: FromHandler { 234 | self.router.handlers.insert(method, T::from_handler(self.context.clone(), handler)); 235 | } 236 | } 237 | 238 | impl<'a: 'b, 'b, T: Default + ApplyContext + Build<'b>> Builder<'a, T> { 239 | /// Build a handler and its children, for GET requests. 240 | /// 241 | /// ``` 242 | /// use rustful::{Context, Response}; 243 | /// use rustful::handler::{MethodRouter, TreeRouter}; 244 | /// 245 | /// fn handler(_context: Context, response: Response) { 246 | /// response.send("Hello world!"); 247 | /// } 248 | /// 249 | /// let mut method_router = MethodRouter::>>::new(); 250 | /// 251 | /// method_router.build().get().on_path("hello/world", handler as fn(Context, Response)); 252 | /// ``` 253 | pub fn get(&'b mut self) -> T::Builder where T: FromHandler { 254 | self.method(Method::Get) 255 | } 256 | 257 | /// Build a handler and its children, for POST requests. See `get` for an example. 258 | pub fn post(&'b mut self) -> T::Builder where T: FromHandler { 259 | self.method(Method::Post) 260 | } 261 | 262 | /// Build a handler and its children, for PATCH requests. See `get` for an example. 263 | pub fn patch(&'b mut self) -> T::Builder where T: FromHandler { 264 | self.method(Method::Patch) 265 | } 266 | 267 | /// Build a handler and its children, for PUT requests. See `get` for an example. 268 | pub fn put(&'b mut self) -> T::Builder where T: FromHandler { 269 | self.method(Method::Put) 270 | } 271 | 272 | /// Build a handler and its children, for DELETE requests. See `get` for an example. 273 | pub fn delete(&'b mut self) -> T::Builder where T: FromHandler { 274 | self.method(Method::Delete) 275 | } 276 | 277 | /// Build a handler and its children, for OPTIONS requests. See `get` for an example. 278 | pub fn options(&'b mut self) -> T::Builder where T: FromHandler { 279 | self.method(Method::Options) 280 | } 281 | 282 | /// Build a handler and its children, for HEAD requests. See `get` for an example. 283 | pub fn head(&'b mut self) -> T::Builder where T: FromHandler { 284 | self.method(Method::Head) 285 | } 286 | 287 | /// Build a handler and its children, for CONNECT requests. See `get` for an example. 288 | pub fn connect(&'b mut self) -> T::Builder where T: FromHandler { 289 | self.method(Method::Connect) 290 | } 291 | 292 | /// Build a handler and its children, similar to `get`, but for any HTTP method. 293 | /// 294 | /// ``` 295 | /// use rustful::{Context, Response}; 296 | /// use rustful::handler::{MethodRouter, TreeRouter}; 297 | /// # fn choose_a_method() -> rustful::Method { rustful::Method::Get } 298 | /// 299 | /// fn handler(_context: Context, response: Response) { 300 | /// response.send("Hello world!"); 301 | /// } 302 | /// 303 | /// let mut method_router = MethodRouter::>>::new(); 304 | /// 305 | /// method_router.build() 306 | /// .method(choose_a_method()) 307 | /// .on_path("hello/world", handler as fn(Context, Response)); 308 | /// ``` 309 | pub fn method(&'b mut self, method: Method) -> T::Builder where T: FromHandler { 310 | match self.router.handlers.entry(method) { 311 | Entry::Occupied(entry) => entry.into_mut().get_builder(self.context.clone()), 312 | Entry::Vacant(entry) => { 313 | let mut handler = T::default(); 314 | handler.apply_context(self.context.clone()); 315 | entry.insert(handler).get_builder(self.context.clone()) 316 | } 317 | } 318 | } 319 | } 320 | 321 | impl<'a, T: Merge + ApplyContext> Builder<'a, T> { 322 | ///Move handlers from another router into this, overwriting conflicting handlers and properties. 323 | pub fn merge(&mut self, mut other: MethodRouter) -> &mut Builder<'a, T> { 324 | other.apply_context(self.context.clone()); 325 | self.router.merge(other); 326 | 327 | self 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/handler/mod.rs: -------------------------------------------------------------------------------- 1 | //!Request handlers and routers. 2 | //! 3 | //!# Building Routers 4 | //! 5 | //!Rustful provides a tree structured all-round router called `DefaultRouter`, 6 | //!but any other type of router can be used, as long as it implements the 7 | //![`HandleRequest`][handle_request] trait. Implementing the [`Build`][build] 8 | //!trait will also make it compatible with router builders: 9 | //! 10 | //!``` 11 | //!extern crate rustful; 12 | //!use rustful::DefaultRouter; 13 | //!# use rustful::{Handler, Context, Response}; 14 | //! 15 | //!# struct ExampleHandler; 16 | //!# impl Handler for ExampleHandler { 17 | //!# fn handle(&self, _: Context, _: Response){} 18 | //!# } 19 | //!# fn main() { 20 | //!# let about_us = ExampleHandler; 21 | //!# let show_user = ExampleHandler; 22 | //!# let list_users = ExampleHandler; 23 | //!# let show_product = ExampleHandler; 24 | //!# let list_products = ExampleHandler; 25 | //!# let show_error = ExampleHandler; 26 | //!# let show_welcome = ExampleHandler; 27 | //!let mut router = DefaultRouter::::new(); 28 | //!router.build().many(|mut node| { 29 | //! node.then().on_get(show_welcome); 30 | //! node.path("about").then().on_get(about_us); 31 | //! node.path("users").many(|mut node| { 32 | //! node.then().on_get(list_users); 33 | //! node.path(":id").then().on_get(show_user); 34 | //! }); 35 | //! node.path("products").many(|mut node| { 36 | //! node.then().on_get(list_products); 37 | //! node.path(":id").then().on_get(show_product); 38 | //! }); 39 | //! node.path("*").then().on_get(show_error); 40 | //!}); 41 | //!# } 42 | //!``` 43 | //! 44 | //!#Variables 45 | //! 46 | //!Routes in the [`TreeRouter`][tree_router] may contain variables, that are 47 | //!useful for capturing parts of the requested path as input to the handler. 48 | //!The syntax for a variable is simply an indicator character (`:` or `*`) 49 | //!followed by a label. Variables without labels are also valid, but their 50 | //!values will be discarded. 51 | //! 52 | //!##Variable Segments (:label) 53 | //! 54 | //!A variable segment will match a single arbitrary segment. They are probably 55 | //!the most commonly used variables and may, for example, be used to select a 56 | //!blog post: `"posts/:year/:month/:day/:title_slug"`. 57 | //! 58 | //!```text 59 | //!pattern = "a/:v/b" 60 | //!"a/c/b" -> v = "c" 61 | //!"a/c/d/b" -> no match 62 | //!"a/b" -> no match 63 | //!"a/c/b/d" -> no match 64 | //!``` 65 | //! 66 | //!##Variable Sequences (*label) 67 | //! 68 | //!A variable sequence is similar to a variable segment, but with the 69 | //!difference that it may consume multiple segments until the rest of the path 70 | //!gives a match. An example use case is a route for downloadable files that 71 | //!may be arranged in arbitrary directories: `"downloads/*file_path"`. 72 | //! 73 | //!```text 74 | //!pattern = "a/*v/b" 75 | //!"a/c/b" -> v = "c" 76 | //!"a/c/d/b" -> v = "c/d" 77 | //!"a/b" -> no match 78 | //!"a/c/b/d" -> no match 79 | //!``` 80 | //! 81 | //!```text 82 | //!pattern = "a/b/*v" 83 | //!"a/b/c" -> v = "c" 84 | //!"a/b/c/d" -> v = "c/d" 85 | //!"a/b" -> no match 86 | //!``` 87 | //! 88 | //!# Router Composition 89 | //! 90 | //!The default router is actually a composition of three routers: 91 | //![`TreeRouter`][tree_router], [`MethodRouter`][method_router] and 92 | //![`Variables`][variables]. They come together as the type 93 | //!`DefaultRouter`, so no need to write it all out in most of the cases. 94 | //! 95 | //!There may, however, be cases where you want something else. What if you 96 | //!don't care about the HTTP method? Maybe your handler takes care of that 97 | //!internally. Sure, no problem: 98 | //! 99 | //!``` 100 | //!use rustful::handler::{Variables, TreeRouter}; 101 | //! 102 | //!let my_router = TreeRouter::>>::new(); 103 | //!# struct ExampleHandler; 104 | //!# impl rustful::Handler for ExampleHandler { 105 | //!# fn handle(&self, _: rustful::Context, _: rustful::Response){} 106 | //!# } 107 | //!``` 108 | //! 109 | //!And what about those route variables? Not using them at all? Well, just 110 | //!remove them too, if you don't want them: 111 | //! 112 | //!``` 113 | //!use rustful::handler::TreeRouter; 114 | //! 115 | //!let my_router = TreeRouter::>::new(); 116 | //!# struct ExampleHandler; 117 | //!# impl rustful::Handler for ExampleHandler { 118 | //!# fn handle(&self, _: rustful::Context, _: rustful::Response){} 119 | //!# } 120 | //!``` 121 | //! 122 | //!You can simply recombine and reorder the router types however you want, or 123 | //!why not make your own router? 124 | //! 125 | //![tree_router]: struct.TreeRouter.html 126 | //![method_router]: struct.MethodRouter.html 127 | //![variables]: struct.Variables.html 128 | //![handle_request]: trait.HandleRequest.html 129 | //![build]: trait.Build.html 130 | 131 | use std::borrow::Cow; 132 | use std::sync::Arc; 133 | use anymap; 134 | 135 | use context::{Context, MaybeUtf8Owned}; 136 | use context::hypermedia::Link; 137 | use response::{Response, SendResponse}; 138 | use self::routing::RouteState; 139 | use StatusCode; 140 | 141 | pub use self::tree_router::TreeRouter; 142 | pub use self::method_router::MethodRouter; 143 | pub use self::variables::Variables; 144 | pub use self::or_else::OrElse; 145 | pub use self::status_router::StatusRouter; 146 | 147 | pub mod routing; 148 | 149 | pub mod tree_router; 150 | pub mod method_router; 151 | pub mod or_else; 152 | pub mod status_router; 153 | mod variables; 154 | 155 | ///Alias for `TreeRouter>>`. 156 | /// 157 | ///This is probably the most common composition, which will select handlers 158 | ///based on path and then HTTP method, and collect any variables on the way. 159 | pub type DefaultRouter = TreeRouter>>; 160 | 161 | ///A trait for request handlers. 162 | pub trait Handler: Send + Sync + 'static { 163 | ///Handle a request from the client. Panicking within this method is 164 | ///discouraged, to allow the server to run smoothly. 165 | fn handle(&self, context: Context, response: Response); 166 | 167 | ///Get a description for the handler. 168 | fn description(&self) -> Option> { 169 | None 170 | } 171 | } 172 | 173 | impl Handler for F { 174 | fn handle(&self, context: Context, response: Response) { 175 | self(context, response); 176 | } 177 | } 178 | 179 | impl Handler for Arc { 180 | fn handle(&self, context: Context, response: Response) { 181 | (**self).handle(context, response); 182 | } 183 | } 184 | 185 | impl Handler for Box { 186 | fn handle(&self, context: Context, response: Response) { 187 | (**self).handle(context, response); 188 | } 189 | } 190 | 191 | ///A request environment, containing the context, response and route state. 192 | pub struct Environment<'a, 'b: 'a, 'l, 'g> { 193 | ///The request context, containing the request parameters. 194 | pub context: Context<'a, 'b, 'l, 'g>, 195 | 196 | ///The response that will be sent to the client. 197 | pub response: Response<'a, 'g>, 198 | 199 | ///A representation of the current routing state. 200 | pub route_state: RouteState<'g>, 201 | } 202 | 203 | impl<'a, 'b, 'l, 'g> Environment<'a, 'b, 'l, 'g> { 204 | ///Replace the hyperlinks. This consumes the whole request environment and 205 | ///returns a new one with a different lifetime, together with the old 206 | ///hyperlinks. 207 | pub fn replace_hyperlinks<'n>(self, hyperlinks: Vec>) -> (Environment<'a, 'b, 'n, 'g>, Vec>) { 208 | let (context, old_hyperlinks) = self.context.replace_hyperlinks(hyperlinks); 209 | ( 210 | Environment { 211 | context: context, 212 | response: self.response, 213 | route_state: self.route_state 214 | }, 215 | old_hyperlinks 216 | ) 217 | } 218 | } 219 | 220 | ///A low level request handler. 221 | /// 222 | ///Suitable for implementing routers, since it has complete access to the 223 | ///routing environment and can reject the request. 224 | pub trait HandleRequest: Send + Sync + 'static { 225 | ///Try to handle an incoming request from the client, or return the 226 | ///request environment to the parent handler. 227 | fn handle_request<'a, 'b, 'l, 'g>(&self, environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>>; 228 | 229 | ///List all of the hyperlinks into this handler, based on the provided 230 | ///base link. It's up to the handler implementation to decide how deep to 231 | ///go. 232 | fn hyperlinks<'a>(&'a self, base_link: Link<'a>) -> Vec>; 233 | } 234 | 235 | impl HandleRequest for H { 236 | fn handle_request<'a, 'b, 'l, 'g>(&self, environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>> { 237 | self.handle(environment.context, environment.response); 238 | Ok(()) 239 | } 240 | 241 | fn hyperlinks<'a>(&'a self, mut base_link: Link<'a>) -> Vec> { 242 | base_link.handler = Some(self); 243 | vec![base_link] 244 | } 245 | } 246 | 247 | impl HandleRequest for Option { 248 | fn handle_request<'a, 'b, 'l, 'g>(&self, mut environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>> { 249 | if let Some(ref handler) = *self { 250 | handler.handle_request(environment) 251 | } else { 252 | environment.response.set_status(StatusCode::NotFound); 253 | Err(environment) 254 | } 255 | } 256 | 257 | fn hyperlinks<'a>(&'a self, base_link: Link<'a>) -> Vec> { 258 | if let Some(ref handler) = *self { 259 | handler.hyperlinks(base_link) 260 | } else { 261 | vec![] 262 | } 263 | } 264 | } 265 | 266 | ///An adapter for simple content creation handlers. 267 | /// 268 | ///``` 269 | ///use rustful::{DefaultRouter, Context, Server, ContentFactory}; 270 | ///use rustful::Method::Get; 271 | /// 272 | ///type Router = DefaultRouter>; 273 | /// 274 | ///let mut router = Router::new(); 275 | ///router.build().path("*").then().on_get(|context: Context| format!("Visiting {}", context.uri_path)); 276 | /// 277 | ///let server = Server { 278 | /// handlers: router, 279 | /// ..Server::default() 280 | ///}; 281 | ///``` 282 | pub struct ContentFactory(pub T); 283 | 284 | impl>> FromHandler for ContentFactory { 285 | fn from_handler(_context: BuilderContext, handler: H) -> ContentFactory { 286 | handler.into() 287 | } 288 | } 289 | 290 | impl ApplyContext for ContentFactory { 291 | fn apply_context(&mut self, _context: BuilderContext) {} 292 | fn prepend_context(&mut self, _context: BuilderContext) {} 293 | } 294 | 295 | impl Merge for ContentFactory { 296 | fn merge(&mut self, other: ContentFactory) { 297 | *self = other; 298 | } 299 | } 300 | 301 | impl HandleRequest for ContentFactory { 302 | fn handle_request<'a, 'b, 'l, 'g>(&self, environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>> { 303 | environment.response.send(self.0.create_content(environment.context)); 304 | Ok(()) 305 | } 306 | 307 | fn hyperlinks<'a>(&'a self, base_link: Link<'a>) -> Vec> { 308 | vec![base_link] 309 | } 310 | } 311 | 312 | impl From for ContentFactory { 313 | fn from(factory: T) -> ContentFactory { 314 | ContentFactory(factory) 315 | } 316 | } 317 | 318 | ///A simplified handler that returns the response content. 319 | /// 320 | ///It's meant for situations where direct access to the `Response` is 321 | ///unnecessary. The benefit is that it can use early returns in a more 322 | ///convenient way. 323 | pub trait CreateContent: Send + Sync + 'static { 324 | ///The type of content that is created. 325 | type Output: for<'a, 'b> SendResponse<'a, 'b>; 326 | 327 | ///Create content that will be sent to the client. 328 | fn create_content(&self, context: Context) -> Self::Output; 329 | } 330 | 331 | impl CreateContent for T where 332 | T: Fn(Context) -> R + Send + Sync + 'static, 333 | R: for<'a, 'b> SendResponse<'a, 'b> 334 | { 335 | type Output = R; 336 | 337 | fn create_content(&self, context: Context) -> Self::Output { 338 | self(context) 339 | } 340 | } 341 | 342 | /// A collection of information that can be passed to child handlers while 343 | /// building a handler. 344 | pub type BuilderContext = anymap::Map; 345 | 346 | /// A trait for handlers that can be build, using a chainable API. 347 | pub trait Build<'a> { 348 | /// The type that provides the builder API. 349 | type Builder: 'a; 350 | 351 | /// Get the builder type for this type, and prepare it with a context. 352 | fn get_builder(&'a mut self, context: BuilderContext) -> Self::Builder; 353 | } 354 | 355 | /// Create a handler from another handler. 356 | pub trait FromHandler { 357 | /// Create a handler from another handler and a `BuilderContext`. 358 | fn from_handler(context: BuilderContext, handler: T) -> Self; 359 | } 360 | 361 | impl FromHandler for T { 362 | fn from_handler(_context: BuilderContext, handler: T) -> T { 363 | handler 364 | } 365 | } 366 | 367 | impl, H> FromHandler for Option { 368 | fn from_handler(context: BuilderContext, handler: H) -> Option { 369 | Some(T::from_handler(context, handler)) 370 | } 371 | } 372 | 373 | /// Apply a `BuilderContext` in various ways. 374 | pub trait ApplyContext { 375 | /// Set properties, based on a given context. 376 | fn apply_context(&mut self, context: BuilderContext); 377 | 378 | /// Prepend existing properties, based on a given context. 379 | fn prepend_context(&mut self, context: BuilderContext); 380 | } 381 | 382 | impl ApplyContext for T { 383 | fn apply_context(&mut self, _context: BuilderContext) {} 384 | fn prepend_context(&mut self, _context: BuilderContext) {} 385 | } 386 | 387 | impl ApplyContext for Option { 388 | fn apply_context(&mut self, context: BuilderContext) { 389 | self.as_mut().map(|handler| handler.apply_context(context)); 390 | } 391 | fn prepend_context(&mut self, context: BuilderContext) { 392 | self.as_mut().map(|handler| handler.prepend_context(context)); 393 | } 394 | } 395 | 396 | /// A trait for handlers that can be merged. 397 | pub trait Merge { 398 | /// Combine this handler with another, overwriting conflicting properties. 399 | fn merge(&mut self, other: Self); 400 | } 401 | 402 | impl Merge for T { 403 | fn merge(&mut self, other: T) { 404 | *self = other; 405 | } 406 | } 407 | 408 | impl Merge for Option { 409 | fn merge(&mut self, other: Option) { 410 | if let &mut Some(ref mut this_handler) = self { 411 | if let Some(other_handler) = other { 412 | this_handler.merge(other_handler); 413 | } 414 | } else { 415 | *self = other; 416 | } 417 | } 418 | } 419 | 420 | ///Context type for storing path variable names. 421 | #[derive(Clone, Debug, Default)] 422 | pub struct VariableNames(pub Vec); 423 | -------------------------------------------------------------------------------- /src/handler/or_else.rs: -------------------------------------------------------------------------------- 1 | //! A router that selects a secondary handler on error. 2 | 3 | use context::hypermedia::Link; 4 | use handler::{HandleRequest, Environment, Build, BuilderContext, ApplyContext, Merge}; 5 | 6 | /// A router that selects a secondary handler on error. 7 | /// 8 | /// ``` 9 | /// use rustful::{OrElse, Context, Response}; 10 | /// 11 | /// fn error_handler(_: Context, response: Response) { 12 | /// let status = response.status(); 13 | /// response.send(format!("Status: {}", status)); 14 | /// } 15 | /// 16 | /// //Every request will end up at error_handler 17 | /// let handler = OrElse::, _>::new(None, error_handler); 18 | /// ``` 19 | #[derive(Clone)] 20 | pub struct OrElse { 21 | ///The primary handler that will have the first chance to process the 22 | ///request. 23 | pub primary: A, 24 | 25 | ///The secondary handler that will take over if the first handler returns 26 | ///an error. 27 | pub secondary: B, 28 | } 29 | 30 | impl OrElse { 31 | ///Create a new `OrElse` with a primary and secondary handler. 32 | pub fn new(primary: A, secondary: B) -> OrElse { 33 | OrElse { 34 | primary: primary, 35 | secondary: secondary, 36 | } 37 | } 38 | 39 | /// Build the router and its children using a chaninable API. 40 | /// 41 | /// ``` 42 | /// use rustful::{Context, Response, OrElse}; 43 | /// use rustful::handler::MethodRouter; 44 | /// 45 | /// type Inner = MethodRouter; 46 | /// 47 | /// fn get(_context: Context, response: Response) { 48 | /// response.send("A GET request."); 49 | /// } 50 | /// 51 | /// fn post(_context: Context, response: Response) { 52 | /// response.send("A POST request."); 53 | /// } 54 | /// 55 | /// fn or_else(_context: Context, response: Response) { 56 | /// response.send("Hello world!"); 57 | /// } 58 | /// 59 | /// let mut router = OrElse::::with_secondary(or_else as fn(Context, Response)); 60 | /// 61 | /// router.build().primary().many(|mut inner_router|{ 62 | /// inner_router.on_get(get as fn(Context, Response)); 63 | /// inner_router.on_post(post); 64 | /// }); 65 | /// ``` 66 | pub fn build(&mut self) -> Builder { 67 | self.get_builder(BuilderContext::new()) 68 | } 69 | } 70 | 71 | impl OrElse { 72 | ///Create a new `OrElse` with a given primary and a default secondary handler. 73 | pub fn with_primary(primary: A) -> OrElse { 74 | OrElse { 75 | primary: primary, 76 | secondary: B::default(), 77 | } 78 | } 79 | } 80 | 81 | impl OrElse { 82 | ///Create a new `OrElse` with a given secondary and a default primary handler. 83 | pub fn with_secondary(secondary: B) -> OrElse { 84 | OrElse { 85 | primary: A::default(), 86 | secondary: secondary, 87 | } 88 | } 89 | } 90 | 91 | impl HandleRequest for OrElse { 92 | fn handle_request<'a, 'b, 'l, 'g>(&self, environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>> { 93 | self.primary.handle_request(environment).or_else(|e| self.secondary.handle_request(e)) 94 | } 95 | 96 | fn hyperlinks<'a>(&'a self, base: Link<'a>) -> Vec> { 97 | let mut links = self.primary.hyperlinks(base.clone()); 98 | links.extend(self.secondary.hyperlinks(base.clone())); 99 | links 100 | } 101 | } 102 | 103 | impl Default for OrElse { 104 | fn default() -> OrElse { 105 | OrElse { 106 | primary: A::default(), 107 | secondary: B::default(), 108 | } 109 | } 110 | } 111 | 112 | impl<'a, A: 'a, B: 'a> Build<'a> for OrElse { 113 | type Builder = Builder<'a, A, B>; 114 | 115 | fn get_builder(&'a mut self, context: BuilderContext) -> Builder<'a, A, B> { 116 | Builder { 117 | router: self, 118 | context: context 119 | } 120 | } 121 | } 122 | 123 | impl ApplyContext for OrElse { 124 | fn apply_context(&mut self, context: BuilderContext) { 125 | self.primary.apply_context(context.clone()); 126 | self.secondary.apply_context(context); 127 | } 128 | 129 | fn prepend_context(&mut self, context: BuilderContext) { 130 | self.primary.prepend_context(context.clone()); 131 | self.secondary.prepend_context(context); 132 | } 133 | } 134 | 135 | impl Merge for OrElse { 136 | fn merge(&mut self, other: OrElse) { 137 | self.primary.merge(other.primary); 138 | self.secondary.merge(other.secondary); 139 | } 140 | } 141 | 142 | /// A builder for an `OrElse`. 143 | pub struct Builder<'a, A: 'a, B: 'a> { 144 | router: &'a mut OrElse, 145 | context: BuilderContext 146 | } 147 | 148 | impl<'a, A, B> Builder<'a, A, B> { 149 | /// Perform more than one operation on this builder. 150 | /// 151 | /// ``` 152 | /// use rustful::{Context, Response, OrElse}; 153 | /// use rustful::handler::MethodRouter; 154 | /// 155 | /// type Inner = MethodRouter; 156 | /// 157 | /// fn get(_context: Context, response: Response) { 158 | /// response.send("A GET request."); 159 | /// } 160 | /// 161 | /// fn post(_context: Context, response: Response) { 162 | /// response.send("A POST request."); 163 | /// } 164 | /// 165 | /// let mut router = OrElse::::default(); 166 | /// 167 | /// router.build().many(|mut router|{ 168 | /// router.primary().on_get(get as fn(Context, Response)); 169 | /// router.secondary().on_post(post); 170 | /// }); 171 | /// ``` 172 | pub fn many)>(&mut self, build: F) -> &mut Builder<'a, A, B> { 173 | build(self); 174 | self 175 | } 176 | } 177 | 178 | impl<'a: 'b, 'b, A: Build<'b>, B> Builder<'a, A, B> { 179 | /// Build the primary router and its children. 180 | pub fn primary(&'b mut self) -> A::Builder { 181 | self.router.primary.get_builder(self.context.clone()) 182 | } 183 | } 184 | 185 | impl<'a: 'b, 'b, A, B: Build<'b>> Builder<'a, A, B> { 186 | /// Build the secondary router and its children. 187 | pub fn secondary(&'b mut self) -> B::Builder { 188 | self.router.secondary.get_builder(self.context.clone()) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/handler/routing.rs: -------------------------------------------------------------------------------- 1 | //!Routing related traits and types. 2 | 3 | use std::collections::HashMap; 4 | use std::iter::{Iterator, FlatMap}; 5 | use std::slice::Split; 6 | use std::ops::Deref; 7 | 8 | use context::MaybeUtf8Owned; 9 | 10 | ///A segmented route. 11 | pub trait Route<'a> { 12 | ///An iterator over route segments. 13 | type Segments: Iterator; 14 | 15 | ///Create a route segment iterator. The iterator is expected to return 16 | ///None for a root path (`/`). 17 | /// 18 | ///```rust 19 | ///# use rustful::handler::routing::Route; 20 | ///let root = "/"; 21 | ///assert_eq!(root.segments().next(), None); 22 | /// 23 | ///let path = ["/path", "to/somewhere/", "/", "/else/"]; 24 | ///let segments = path.segments().collect::>(); 25 | ///let expected = vec![ 26 | /// "path".as_bytes(), 27 | /// "to".as_bytes(), 28 | /// "somewhere".as_bytes(), 29 | /// "else".as_bytes() 30 | ///]; 31 | ///assert_eq!(segments, expected); 32 | ///``` 33 | fn segments(&'a self) -> >::Segments; 34 | } 35 | 36 | fn is_slash(c: &u8) -> bool { 37 | *c == b'/' 38 | } 39 | 40 | const IS_SLASH: &'static fn(&u8) -> bool = & (is_slash as fn(&u8) -> bool); 41 | 42 | impl<'a> Route<'a> for str { 43 | type Segments = RouteIter bool>>; 44 | 45 | fn segments(&'a self) -> >::Segments { 46 | self.as_bytes().segments() 47 | } 48 | } 49 | 50 | impl<'a> Route<'a> for [u8] { 51 | type Segments = RouteIter bool>>; 52 | 53 | fn segments(&'a self) -> >::Segments { 54 | let s = if self.starts_with(b"/") { 55 | &self[1..] 56 | } else { 57 | self 58 | }; 59 | let s = if s.ends_with(b"/") { 60 | &s[..s.len() - 1] 61 | } else { 62 | s 63 | }; 64 | 65 | if s.len() == 0 { 66 | RouteIter::Root 67 | } else { 68 | RouteIter::Path(s.split(IS_SLASH)) 69 | } 70 | } 71 | } 72 | 73 | 74 | impl<'a, 'b: 'a, I: 'a, T: 'a> Route<'a> for I where 75 | &'a I: IntoIterator, 76 | T: Deref, 77 | ::Target: Route<'a> + 'b 78 | { 79 | type Segments = FlatMap<<&'a I as IntoIterator>::IntoIter, <::Target as Route<'a>>::Segments, fn(&'a T) -> <::Target as Route<'a>>::Segments>; 80 | 81 | fn segments(&'a self) -> Self::Segments { 82 | fn segments<'a, 'b: 'a, T: Deref + 'b, R: ?Sized + Route<'a, Segments=S> + 'b, S: Iterator>(s: &'a T) -> S { 83 | s.segments() 84 | } 85 | 86 | self.into_iter().flat_map(segments) 87 | } 88 | } 89 | 90 | ///Utility iterator for when a root path may be hard to represent. 91 | #[derive(Clone)] 92 | pub enum RouteIter { 93 | ///A root path (`/`). 94 | Root, 95 | ///A non-root path (`path/to/somewhere`). 96 | Path(I) 97 | } 98 | 99 | impl Iterator for RouteIter { 100 | type Item = ::Item; 101 | 102 | fn next(&mut self) -> Option<::Item> { 103 | match *self { 104 | RouteIter::Path(ref mut i) => i.next(), 105 | RouteIter::Root => None 106 | } 107 | } 108 | 109 | fn size_hint(&self) -> (usize, Option) { 110 | match *self { 111 | RouteIter::Path(ref i) => i.size_hint(), 112 | RouteIter::Root => (0, Some(0)) 113 | } 114 | } 115 | } 116 | 117 | ///A state object for routing. 118 | #[derive(Clone)] 119 | pub struct RouteState<'a> { 120 | route: Vec<&'a [u8]>, 121 | variables: Vec>, 122 | index: usize, 123 | var_index: usize, 124 | } 125 | 126 | impl<'a> RouteState<'a> { 127 | ///Get the current path segment. 128 | pub fn get(&self) -> Option<&'a [u8]> { 129 | self.route.get(self.index).cloned() 130 | } 131 | 132 | ///Don't include this path segment in a variable. 133 | pub fn skip(&mut self) { 134 | self.variables.get_mut(self.index).map(|v| *v = None); 135 | self.index += 1; 136 | } 137 | 138 | ///Include this path segment as a variable. 139 | pub fn keep(&mut self) { 140 | let v_i = self.var_index; 141 | self.variables.get_mut(self.index).map(|v| *v = Some(v_i)); 142 | self.index += 1; 143 | self.var_index += 1; 144 | } 145 | 146 | ///Extend a previously saved variable value with this path segment, or 147 | ///save it as a new variable. 148 | pub fn fuse(&mut self) { 149 | let v_i = self.var_index; 150 | self.variables.get_mut(self.index).map(|v| *v = Some(v_i)); 151 | self.index += 1; 152 | } 153 | 154 | ///Assign names to the saved variables and return them. 155 | pub fn variables(&self, names: &[MaybeUtf8Owned]) -> HashMap { 156 | let values = self.route.iter().zip(self.variables.iter()).filter_map(|(v, keep)| { 157 | if let Some(index) = *keep { 158 | Some((index, *v)) 159 | } else { 160 | None 161 | } 162 | }); 163 | 164 | let mut var_map = HashMap::::with_capacity(names.len()); 165 | for (name, value) in VariableIter::new(names, values) { 166 | var_map.insert(name, value); 167 | } 168 | 169 | var_map 170 | } 171 | 172 | ///Get a snapshot of a part of the current state. 173 | pub fn snapshot(&self) -> (usize, usize) { 174 | (self.index, self.var_index) 175 | } 176 | 177 | ///Go to a previously recorded state. 178 | pub fn go_to(&mut self, snapshot: (usize, usize)) { 179 | let (index, var_index) = snapshot; 180 | self.index = index; 181 | self.var_index = var_index; 182 | } 183 | 184 | ///Check if there are no more segments. 185 | pub fn is_empty(&self) -> bool { 186 | self.index == self.route.len() 187 | } 188 | } 189 | 190 | impl<'a, R: Route<'a> + ?Sized> From<&'a R> for RouteState<'a> { 191 | fn from(route: &'a R) -> RouteState<'a> { 192 | let route: Vec<_> = route.segments().collect(); 193 | RouteState { 194 | variables: vec![None; route.len()], 195 | route: route, 196 | index: 0, 197 | var_index: 0, 198 | } 199 | } 200 | } 201 | 202 | struct VariableIter<'a, I> { 203 | iter: I, 204 | names: &'a [MaybeUtf8Owned], 205 | current: Option<(usize, MaybeUtf8Owned, MaybeUtf8Owned)> 206 | } 207 | 208 | impl<'a, I: Iterator> VariableIter<'a, I> { 209 | fn new(names: &'a [MaybeUtf8Owned], iter: I) -> VariableIter<'a, I> { 210 | VariableIter { 211 | iter: iter, 212 | names: names, 213 | current: None 214 | } 215 | } 216 | } 217 | 218 | impl<'a, I: Iterator> Iterator for VariableIter<'a, I> { 219 | type Item=(MaybeUtf8Owned, MaybeUtf8Owned); 220 | 221 | fn next(&mut self) -> Option { 222 | for (next_index, next_segment) in &mut self.iter { 223 | //validate next_index and check if the variable has a name 224 | debug_assert!(next_index < self.names.len(), format!("invalid variable name index! variable_names.len(): {}, index: {}", self.names.len(), next_index)); 225 | let next_name = match self.names.get(next_index) { 226 | None => continue, 227 | Some(n) if n.is_empty() => continue, 228 | Some(n) => n 229 | }; 230 | 231 | if let Some((index, name, mut segment)) = self.current.take() { 232 | if index == next_index { 233 | //this is a part of the current sequence 234 | segment.push_char('/'); 235 | segment.push_bytes(next_segment); 236 | self.current = Some((index, name, segment)); 237 | } else { 238 | //the current sequence has ended 239 | self.current = Some((next_index, (*next_name).clone(), next_segment.to_owned().into())); 240 | return Some((name, segment)); 241 | } 242 | } else { 243 | //this is the first named variable 244 | self.current = Some((next_index, (*next_name).clone(), next_segment.to_owned().into())); 245 | } 246 | } 247 | 248 | //return the last variable 249 | self.current.take().map(|(_, name, segment)| (name, segment)) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/handler/status_router.rs: -------------------------------------------------------------------------------- 1 | //! A router that selects an item from an HTTP status code. 2 | 3 | use std::collections::hash_map::{HashMap, Entry}; 4 | 5 | use context::hypermedia::Link; 6 | use StatusCode; 7 | use handler::{HandleRequest, Environment, Build, ApplyContext, Merge, FromHandler, BuilderContext}; 8 | 9 | /// A router that selects an item from an HTTP status code. 10 | /// 11 | /// It does neither alter nor preserve the status code of the response. 12 | #[derive(Clone)] 13 | pub struct StatusRouter { 14 | handlers: HashMap 15 | } 16 | 17 | impl StatusRouter { 18 | /// Create an empty `StatusRouter`. 19 | pub fn new() -> StatusRouter { 20 | StatusRouter::default() 21 | } 22 | 23 | /// Build the router and its children using a chaninable API. 24 | /// 25 | /// ``` 26 | /// use rustful::{Context, Response, StatusCode, StatusRouter}; 27 | /// 28 | /// fn on_404(context: Context, response: Response) { 29 | /// response.send(format!("{} was not found.", context.uri_path)); 30 | /// } 31 | /// 32 | /// fn on_500(_: Context, response: Response) { 33 | /// response.send("Looks like you found a bug"); 34 | /// } 35 | /// let mut status_router = StatusRouter::::new(); 36 | /// 37 | /// status_router.build().many(|mut status_router|{ 38 | /// status_router.on(StatusCode::NotFound, on_404 as fn(Context, Response)); 39 | /// status_router.on(StatusCode::InternalServerError, on_500); 40 | /// }); 41 | /// ``` 42 | pub fn build(&mut self) -> Builder { 43 | self.get_builder(BuilderContext::new()) 44 | } 45 | 46 | /// Insert a handler that will listen for a specific status code. 47 | /// 48 | /// ``` 49 | /// use rustful::{Context, Response, StatusCode, StatusRouter}; 50 | /// use rustful::handler::MethodRouter; 51 | /// 52 | /// let on_not_found = MethodRouter::::new(); 53 | /// //Fill on_not_found with handlers... 54 | /// 55 | /// let mut status_router = StatusRouter::new(); 56 | /// status_router.insert(StatusCode::NotFound, on_not_found); 57 | /// ``` 58 | pub fn insert(&mut self, status: StatusCode, handler: T) { 59 | self.handlers.insert(status, handler); 60 | } 61 | } 62 | 63 | impl HandleRequest for StatusRouter { 64 | fn handle_request<'a, 'b, 'l, 'g>(&self, environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>> { 65 | if let Some(handler) = self.handlers.get(&environment.response.status()) { 66 | handler.handle_request(environment) 67 | } else { 68 | Err(environment) 69 | } 70 | } 71 | 72 | fn hyperlinks<'a>(&'a self, base: Link<'a>) -> Vec> { 73 | self.handlers.iter().flat_map(|(_, item)| { 74 | item.hyperlinks(base.clone()) 75 | }).collect() 76 | } 77 | } 78 | 79 | impl Default for StatusRouter { 80 | fn default() -> StatusRouter { 81 | StatusRouter { 82 | handlers: HashMap::new(), 83 | } 84 | } 85 | } 86 | 87 | impl<'a, T: 'a> Build<'a> for StatusRouter { 88 | type Builder = Builder<'a, T>; 89 | 90 | fn get_builder(&'a mut self, context: BuilderContext) -> Builder<'a, T> { 91 | Builder { 92 | router: self, 93 | context: context 94 | } 95 | } 96 | } 97 | 98 | impl ApplyContext for StatusRouter { 99 | fn apply_context(&mut self, context: BuilderContext) { 100 | for (_, handler) in &mut self.handlers { 101 | handler.apply_context(context.clone()); 102 | } 103 | } 104 | 105 | fn prepend_context(&mut self, context: BuilderContext) { 106 | for (_, handler) in &mut self.handlers { 107 | handler.prepend_context(context.clone()); 108 | } 109 | } 110 | } 111 | 112 | impl Merge for StatusRouter { 113 | fn merge(&mut self, other: StatusRouter) { 114 | for (status, handler) in other.handlers { 115 | match self.handlers.entry(status) { 116 | Entry::Vacant(entry) => { entry.insert(handler); }, 117 | Entry::Occupied(mut entry) => entry.get_mut().merge(handler), 118 | } 119 | } 120 | } 121 | } 122 | 123 | /// A builder for a `StatusRouter`. 124 | pub struct Builder<'a, T: 'a> { 125 | router: &'a mut StatusRouter, 126 | context: BuilderContext 127 | } 128 | 129 | impl<'a, T> Builder<'a, T> { 130 | /// Perform more than one operation on this builder. 131 | /// 132 | /// ``` 133 | /// use rustful::{Context, Response, StatusCode, StatusRouter}; 134 | /// 135 | /// fn on_404(context: Context, response: Response) { 136 | /// response.send(format!("{} was not found.", context.uri_path)); 137 | /// } 138 | /// 139 | /// fn on_500(_: Context, response: Response) { 140 | /// response.send("Looks like you found a bug"); 141 | /// } 142 | /// let mut status_router = StatusRouter::::new(); 143 | /// 144 | /// status_router.build().many(|mut status_router|{ 145 | /// status_router.on(StatusCode::NotFound, on_404 as fn(Context, Response)); 146 | /// status_router.on(StatusCode::InternalServerError, on_500); 147 | /// }); 148 | /// ``` 149 | pub fn many)>(&mut self, build: F) -> &mut Builder<'a, T> { 150 | build(self); 151 | self 152 | } 153 | 154 | /// Insert a handler that will listen for a specific status code. 155 | /// 156 | /// ``` 157 | /// use rustful::{Context, Response, StatusCode, StatusRouter}; 158 | /// 159 | /// fn on_404(context: Context, response: Response) { 160 | /// response.send(format!("{} was not found.", context.uri_path)); 161 | /// } 162 | /// 163 | /// let mut status_router = StatusRouter::::new(); 164 | /// status_router.build().on(StatusCode::NotFound, on_404 as fn(Context, Response)); 165 | /// ``` 166 | pub fn on(&mut self, status: StatusCode, handler: H) where T: FromHandler { 167 | self.router.handlers.insert(status, T::from_handler(self.context.clone(), handler)); 168 | } 169 | } 170 | 171 | impl<'a: 'b, 'b, T: Default + ApplyContext + Build<'b>> Builder<'a, T> { 172 | /// Build a handler that will listen for a specific status code. 173 | /// 174 | /// ``` 175 | /// use rustful::{Context, Response, StatusCode, StatusRouter}; 176 | /// use rustful::handler::MethodRouter; 177 | /// 178 | /// fn on_get_404(context: Context, response: Response) { 179 | /// response.send(format!("GET {} was not found.", context.uri_path)); 180 | /// } 181 | /// 182 | /// let mut status_router = StatusRouter::>::new(); 183 | /// 184 | /// status_router.build() 185 | /// .status(StatusCode::NotFound) 186 | /// .on_get(on_get_404 as fn(Context, Response)); 187 | /// ``` 188 | pub fn status(&'b mut self, status: StatusCode) -> T::Builder { 189 | match self.router.handlers.entry(status) { 190 | Entry::Occupied(entry) => entry.into_mut().get_builder(self.context.clone()), 191 | Entry::Vacant(entry) => { 192 | let mut handler = T::default(); 193 | handler.apply_context(self.context.clone()); 194 | entry.insert(handler).get_builder(self.context.clone()) 195 | } 196 | } 197 | } 198 | } 199 | 200 | impl<'a, T: Merge + ApplyContext> Builder<'a, T> { 201 | ///Move handlers from another router into this, overwriting conflicting handlers and properties. 202 | pub fn merge(&mut self, mut other: StatusRouter) -> &mut Builder<'a, T> { 203 | other.apply_context(self.context.clone()); 204 | self.router.merge(other); 205 | 206 | self 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/handler/variables.rs: -------------------------------------------------------------------------------- 1 | use context::MaybeUtf8Owned; 2 | use context::hypermedia::Link; 3 | use handler::{HandleRequest, Environment, FromHandler, Build, BuilderContext, ApplyContext, Merge, VariableNames}; 4 | 5 | ///Assigns names to route variables. 6 | /// 7 | ///It's technically a single-item router, with the purpose of pairing route 8 | ///variable names with input values, but it has to come after other routers, 9 | ///since it has be at the end of a router chain to know what variables to use. 10 | ///It won't do any other routing work, so make sure to pair it with, at least, 11 | ///a path based router. 12 | #[derive(Clone)] 13 | pub struct Variables { 14 | handler: H, 15 | variables: Vec, 16 | } 17 | 18 | impl, H> FromHandler for Variables { 19 | fn from_handler(mut context: BuilderContext, handler: H) -> Variables { 20 | Variables { 21 | variables: context.remove::().unwrap_or_default().0, 22 | handler: T::from_handler(context, handler), 23 | } 24 | } 25 | } 26 | 27 | impl ApplyContext for Variables { 28 | fn apply_context(&mut self, mut context: BuilderContext) { 29 | if let Some(VariableNames(variables)) = context.remove() { 30 | self.variables = variables; 31 | } 32 | 33 | self.handler.apply_context(context); 34 | } 35 | 36 | fn prepend_context(&mut self, mut context: BuilderContext) { 37 | if let Some(VariableNames(mut variables)) = context.remove() { 38 | ::std::mem::swap(&mut self.variables, &mut variables); 39 | self.variables.extend(variables); 40 | } 41 | 42 | self.handler.prepend_context(context); 43 | } 44 | } 45 | 46 | impl Merge for Variables { 47 | fn merge(&mut self, other: Variables) { 48 | self.variables = other.variables; 49 | self.handler.merge(other.handler); 50 | } 51 | } 52 | 53 | impl<'a, H: Build<'a>> Build<'a> for Variables { 54 | type Builder = H::Builder; 55 | 56 | fn get_builder(&'a mut self, mut context: BuilderContext) -> Self::Builder { 57 | context.remove::(); 58 | self.handler.get_builder(context) 59 | } 60 | } 61 | 62 | impl HandleRequest for Variables { 63 | fn handle_request<'a, 'b, 'l, 'g>(&self, mut environment: Environment<'a, 'b, 'l, 'g>) -> Result<(), Environment<'a, 'b, 'l, 'g>> { 64 | environment.context.variables = environment.route_state.variables(&self.variables).into(); 65 | self.handler.handle_request(environment) 66 | } 67 | 68 | fn hyperlinks<'a>(&'a self, base: Link<'a>) -> Vec> { 69 | self.handler.hyperlinks(base) 70 | } 71 | } 72 | 73 | impl Default for Variables { 74 | fn default() -> Variables { 75 | Variables { 76 | handler: H::default(), 77 | variables: vec![], 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //!A light HTTP framework with REST-like features. The main purpose of Rustful 2 | //!is to create a simple, modular and non-intrusive foundation for HTTP 3 | //!applications. It has a mainly stateless structure, which naturally allows 4 | //!it to run both as one single server and as multiple instances in a cluster. 5 | //! 6 | //!A new server is created using the [`Server`][server] type, which contains 7 | //!all the necessary settings as fields: 8 | //! 9 | //!```no_run 10 | //!extern crate rustful; 11 | //!use rustful::{Server, Handler, Context, Response, DefaultRouter}; 12 | //! 13 | //!struct Phrase(&'static str); 14 | //! 15 | //!impl Handler for Phrase { 16 | //! fn handle(&self, context: Context, response: Response) { 17 | //! //Check if the client accessed /hello/:name or /good_bye/:name 18 | //! if let Some(name) = context.variables.get("name") { 19 | //! //Use the value of :name 20 | //! response.send(format!("{}, {}", self.0, name)); 21 | //! } else { 22 | //! response.send(self.0) 23 | //! } 24 | //! } 25 | //!} 26 | //! 27 | //!# fn main() { 28 | //!let mut my_router = DefaultRouter::::new(); 29 | //!my_router.build().many(|mut node| { 30 | //! //Receive GET requests to /hello and /hello/:name 31 | //! node.path("hello").many(|mut node| { 32 | //! node.then().on_get(Phrase("hello")); 33 | //! node.path(":name").then().on_get(Phrase("hello")); 34 | //! }); 35 | //! 36 | //! //Receive GET requests to /good_bye and /good_bye/:name 37 | //! node.path("good_bye").many(|mut node| { 38 | //! node.then().on_get(Phrase("good bye")); 39 | //! node.path(":name").then().on_get(Phrase("good bye")); 40 | //! }); 41 | //!}); 42 | //! 43 | //!Server { 44 | //! //Use a closure to handle requests. 45 | //! handlers: my_router, 46 | //! //Set the listening port to `8080`. 47 | //! host: 8080.into(), 48 | //! //Fill out everything else with default values. 49 | //! ..Server::default() 50 | //!}.run(); 51 | //!# } 52 | //!``` 53 | //! 54 | //![server]: server/struct.Server.html 55 | 56 | #![crate_name = "rustful"] 57 | 58 | #![crate_type = "rlib"] 59 | 60 | #![doc(html_root_url = "http://ogeon.github.io/docs/rustful/master/")] 61 | 62 | #![cfg_attr(all(test, feature = "benchmark"), feature(test))] 63 | #![cfg_attr(feature = "strict", deny(missing_docs))] 64 | #![cfg_attr(feature = "strict", deny(warnings))] 65 | 66 | #[cfg(test)] 67 | #[cfg(feature = "benchmark")] 68 | extern crate test; 69 | 70 | #[cfg(feature = "multipart")] 71 | extern crate multipart; 72 | 73 | extern crate url; 74 | extern crate time; 75 | extern crate hyper; 76 | extern crate anymap; 77 | extern crate phf; 78 | extern crate num_cpus; 79 | #[macro_use] 80 | extern crate log; 81 | 82 | pub use hyper::mime; 83 | pub use hyper::method::Method; 84 | pub use hyper::status::StatusCode; 85 | pub use hyper::header; 86 | pub use hyper::Result as HttpResult; 87 | pub use hyper::Error as HttpError; 88 | pub use hyper::version::HttpVersion; 89 | 90 | pub use self::server::Server; 91 | pub use self::context::Context; 92 | pub use self::response::Response; 93 | pub use self::response::Error; 94 | pub use self::response::SendResponse; 95 | pub use self::handler::Handler; 96 | pub use self::handler::DefaultRouter; 97 | pub use self::handler::ContentFactory; 98 | pub use self::handler::CreateContent; 99 | pub use self::handler::OrElse; 100 | pub use self::handler::StatusRouter; 101 | 102 | mod utils; 103 | #[macro_use] 104 | #[doc(hidden)] 105 | pub mod macros; 106 | 107 | pub mod server; 108 | pub mod handler; 109 | pub mod context; 110 | pub mod response; 111 | pub mod filter; 112 | pub mod file; 113 | pub mod net; 114 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | //!Some helpful macros. 2 | 3 | /** 4 | A macro for making content types. 5 | 6 | It takes a main type, a sub type and a parameter list. Instead of this: 7 | 8 | ```rust 9 | use rustful::header::ContentType; 10 | use rustful::mime::{Mime, TopLevel, SubLevel, Attr, Value}; 11 | 12 | ContentType( 13 | Mime ( 14 | TopLevel::Text, 15 | SubLevel::Html, 16 | vec![(Attr::Charset, Value::Utf8)] 17 | ) 18 | ); 19 | ``` 20 | 21 | it can be written like this: 22 | 23 | ``` 24 | #[macro_use(content_type)] 25 | extern crate rustful; 26 | use rustful::header::ContentType; 27 | 28 | # fn main() { 29 | ContentType(content_type!(Text / Html; Charset = Utf8)); 30 | # } 31 | ``` 32 | 33 | The `Charset = Utf8` part defines the parameter list for the content type and 34 | may contain more than one parameter, or be omitted. Here are some more 35 | examples showing that and how strings can be used for more exotic values: 36 | 37 | ``` 38 | #[macro_use(content_type)] 39 | extern crate rustful; 40 | use rustful::header::ContentType; 41 | 42 | # fn main() { 43 | ContentType(content_type!(Application / "octet-stream"; "type" = "image/gif"; "padding" = "4")); 44 | # } 45 | ``` 46 | 47 | ``` 48 | #[macro_use(content_type)] 49 | extern crate rustful; 50 | use rustful::header::ContentType; 51 | 52 | # fn main() { 53 | ContentType(content_type!(Image / Png)); 54 | # } 55 | ``` 56 | **/ 57 | #[macro_export] 58 | macro_rules! content_type { 59 | ($main_type:tt / $sub_type:tt) => ({ 60 | use $crate::macros::MimeHelper; 61 | $crate::mime::Mime ( 62 | { 63 | #[allow(unused_imports)] 64 | use $crate::mime::TopLevel::*; 65 | MimeHelper::from(content_type!(@__rustful_to_expr $main_type)).convert() 66 | }, 67 | { 68 | #[allow(unused_imports)] 69 | use $crate::mime::SubLevel::*; 70 | MimeHelper::from(content_type!(@__rustful_to_expr $sub_type)).convert() 71 | }, 72 | Vec::new() 73 | ) 74 | }); 75 | 76 | ($main_type:tt / $sub_type:tt; $($param:tt = $value:tt);+) => ({ 77 | use $crate::macros::MimeHelper; 78 | $crate::mime::Mime ( 79 | { 80 | #[allow(unused_imports)] 81 | use $crate::mime::TopLevel::*; 82 | MimeHelper::from(content_type!(@__rustful_to_expr $main_type)).convert() 83 | }, 84 | { 85 | #[allow(unused_imports)] 86 | use $crate::mime::SubLevel::*; 87 | MimeHelper::from(content_type!(@__rustful_to_expr $sub_type)).convert() 88 | }, 89 | vec![ $( ({ 90 | #[allow(unused_imports)] 91 | use $crate::mime::Attr::*; 92 | MimeHelper::from(content_type!(@__rustful_to_expr $param)).convert() 93 | }, { 94 | #[allow(unused_imports)] 95 | use $crate::mime::Value::*; 96 | MimeHelper::from(content_type!(@__rustful_to_expr $value)).convert() 97 | })),+ ] 98 | ) 99 | }); 100 | 101 | (@__rustful_to_expr $e: expr) => ($e); 102 | } 103 | 104 | use std::str::FromStr; 105 | use std::fmt::Debug; 106 | use mime::{TopLevel, SubLevel, Attr, Value}; 107 | 108 | #[doc(hidden)] 109 | pub enum MimeHelper<'a, T> { 110 | Str(&'a str), 111 | Target(T) 112 | } 113 | 114 | impl<'a, T: FromStr> MimeHelper<'a, T> where ::Err: Debug { 115 | pub fn convert(self) -> T { 116 | match self { 117 | MimeHelper::Str(s) => s.parse().unwrap(), 118 | MimeHelper::Target(t) => t 119 | } 120 | } 121 | } 122 | 123 | impl<'a, T: FromStr> From<&'a str> for MimeHelper<'a, T> { 124 | fn from(s: &'a str) -> MimeHelper<'a, T> { 125 | MimeHelper::Str(s) 126 | } 127 | } 128 | 129 | impl<'a> From for MimeHelper<'a, TopLevel> { 130 | fn from(t: TopLevel) -> MimeHelper<'a, TopLevel> { 131 | MimeHelper::Target(t) 132 | } 133 | } 134 | 135 | impl<'a> From for MimeHelper<'a, SubLevel> { 136 | fn from(t: SubLevel) -> MimeHelper<'a, SubLevel> { 137 | MimeHelper::Target(t) 138 | } 139 | } 140 | 141 | impl<'a> From for MimeHelper<'a, Attr> { 142 | fn from(t: Attr) -> MimeHelper<'a, Attr> { 143 | MimeHelper::Target(t) 144 | } 145 | } 146 | 147 | impl<'a> From for MimeHelper<'a, Value> { 148 | fn from(t: Value) -> MimeHelper<'a, Value> { 149 | MimeHelper::Target(t) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/net.rs: -------------------------------------------------------------------------------- 1 | //! Network related types and traits, reexported from Hyper. 2 | 3 | pub use hyper::net::{SslServer, HttpStream, NetworkStream}; 4 | -------------------------------------------------------------------------------- /src/server/config.rs: -------------------------------------------------------------------------------- 1 | use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6, Ipv4Addr}; 2 | use std::str::FromStr; 3 | use std::any::TypeId; 4 | use std::mem::swap; 5 | use std::time::Duration; 6 | 7 | use anymap::Map; 8 | use anymap::any::{Any, UncheckedAnyExt}; 9 | 10 | ///A host address and a port. 11 | /// 12 | ///Can be conveniently converted from an existing address-port pair or just a port: 13 | /// 14 | ///``` 15 | ///use std::net::Ipv4Addr; 16 | ///use rustful::server::Host; 17 | /// 18 | ///let host1: Host = (Ipv4Addr::new(0, 0, 0, 0), 80).into(); 19 | ///let host2: Host = 80.into(); 20 | /// 21 | ///assert_eq!(host1, host2); 22 | ///``` 23 | #[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] 24 | pub struct Host(SocketAddr); 25 | 26 | impl Host { 27 | ///Create a `Host` with the address `0.0.0.0:port`. This is the same as `port.into()`. 28 | pub fn any_v4(port: u16) -> Host { 29 | Host(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port))) 30 | } 31 | 32 | ///Change the port of the host address. 33 | pub fn port(&mut self, port: u16) { 34 | self.0 = match self.0 { 35 | SocketAddr::V4(addr) => SocketAddr::V4(SocketAddrV4::new(*addr.ip(), port)), 36 | SocketAddr::V6(addr) => { 37 | SocketAddr::V6(SocketAddrV6::new(*addr.ip(), port, addr.flowinfo(), addr.scope_id())) 38 | } 39 | }; 40 | } 41 | } 42 | 43 | impl From for SocketAddr { 44 | fn from(host: Host) -> SocketAddr { 45 | host.0 46 | } 47 | } 48 | 49 | impl From for Host { 50 | fn from(port: u16) -> Host { 51 | Host::any_v4(port) 52 | } 53 | } 54 | 55 | impl From for Host { 56 | fn from(addr: SocketAddr) -> Host { 57 | Host(addr) 58 | } 59 | } 60 | 61 | impl From for Host { 62 | fn from(addr: SocketAddrV4) -> Host { 63 | Host(SocketAddr::V4(addr)) 64 | } 65 | } 66 | 67 | impl From for Host { 68 | fn from(addr: SocketAddrV6) -> Host { 69 | Host(SocketAddr::V6(addr)) 70 | } 71 | } 72 | 73 | impl From<(Ipv4Addr, u16)> for Host { 74 | fn from((ip, port): (Ipv4Addr, u16)) -> Host { 75 | Host(SocketAddr::V4(SocketAddrV4::new(ip, port))) 76 | } 77 | } 78 | 79 | impl FromStr for Host { 80 | type Err = ::Err; 81 | 82 | fn from_str(s: &str) -> Result { 83 | s.parse().map(Host) 84 | } 85 | } 86 | 87 | ///A somewhat lazy container for globally accessible data. 88 | /// 89 | ///It will try to be as simple as possible and allocate as little as possible, 90 | ///depending on the number of stored values. 91 | /// 92 | /// * No value: Nothing is allocated and nothing is searched for during 93 | ///access. 94 | /// 95 | /// * One value: One `Box` is allocated. Searching for a value will only 96 | ///consist of a comparison of `TypeId` and a downcast. 97 | /// 98 | /// * Multiple values: An `AnyMap` is created, as well as a `Box` for each 99 | ///value. Searching for a value has the full overhead of `AnyMap`. 100 | /// 101 | ///`Global` can be created from a boxed value, from tuples or using the 102 | ///`Default` trait. More values can then be added using `insert(value)`. 103 | /// 104 | ///``` 105 | ///use rustful::server::Global; 106 | ///let mut g1: Global = Box::new(5).into(); 107 | ///assert_eq!(g1.get(), Some(&5)); 108 | ///assert_eq!(g1.get::<&str>(), None); 109 | /// 110 | ///let old = g1.insert(10); 111 | ///assert_eq!(old, Some(5)); 112 | ///assert_eq!(g1.get(), Some(&10)); 113 | /// 114 | ///g1.insert("cat"); 115 | ///assert_eq!(g1.get(), Some(&10)); 116 | ///assert_eq!(g1.get(), Some(&"cat")); 117 | /// 118 | ///let g2: Global = (5, "cat").into(); 119 | ///assert_eq!(g2.get(), Some(&5)); 120 | ///assert_eq!(g2.get(), Some(&"cat")); 121 | ///``` 122 | pub struct Global(GlobalState); 123 | 124 | impl Global { 125 | ///Borrow a value of type `T` if the there is one. 126 | pub fn get(&self) -> Option<&T> { 127 | match self.0 { 128 | GlobalState::None => None, 129 | GlobalState::One(id, ref a) => if id == TypeId::of::() { 130 | //Here be dragons! 131 | unsafe { Some(a.downcast_ref_unchecked()) } 132 | } else { 133 | None 134 | }, 135 | GlobalState::Many(ref map) => map.get() 136 | } 137 | } 138 | 139 | ///Insert a new value, returning the previous value of the same type, if 140 | ///any. 141 | pub fn insert(&mut self, value: T) -> Option { 142 | match self.0 { 143 | GlobalState::None => { 144 | *self = Box::new(value).into(); 145 | None 146 | }, 147 | GlobalState::One(id, _) => if id == TypeId::of::() { 148 | if let GlobalState::One(_, ref mut previous_value) = self.0 { 149 | let mut v = Box::new(value) as Box; 150 | swap(previous_value, &mut v); 151 | Some(unsafe { *v.downcast_unchecked() }) 152 | } else { 153 | unreachable!() 154 | } 155 | } else { 156 | //Here be more dragons! 157 | let mut other = GlobalState::Many(Map::new()); 158 | swap(&mut self.0, &mut other); 159 | if let GlobalState::Many(ref mut map) = self.0 { 160 | if let GlobalState::One(id, previous_value) = other { 161 | let mut raw = map.as_mut(); 162 | unsafe { raw.insert(id, previous_value); } 163 | } 164 | 165 | map.insert(value) 166 | } else { 167 | unreachable!() 168 | } 169 | }, 170 | GlobalState::Many(ref mut map) => { 171 | map.insert(value) 172 | } 173 | } 174 | } 175 | } 176 | 177 | impl From> for Global { 178 | fn from(data: Box) -> Global { 179 | Global(GlobalState::One(TypeId::of::(), data)) 180 | } 181 | } 182 | 183 | macro_rules! from_tuple { 184 | ($first: ident, $($t: ident),+) => ( 185 | impl<$first: Any + Send + Sync, $($t: Any + Send + Sync),+> From<($first, $($t),+)> for Global { 186 | #[allow(non_snake_case)] 187 | fn from(tuple: ($first, $($t),+))-> Global { 188 | let ($first, $($t),+) = tuple; 189 | let mut map = Map::new(); 190 | map.insert($first); 191 | $( 192 | map.insert($t); 193 | )+ 194 | 195 | Global(GlobalState::Many(map)) 196 | } 197 | } 198 | 199 | from_tuple!($($t),+); 200 | ); 201 | ($ty: ident) => ( 202 | impl<$ty: Any + Send + Sync> From<($ty,)> for Global { 203 | fn from(tuple: ($ty,)) -> Global { 204 | Box::new(tuple.0).into() 205 | } 206 | } 207 | ); 208 | } 209 | 210 | impl From<()> for Global { 211 | fn from(_: ()) -> Global { 212 | Global(GlobalState::None) 213 | } 214 | } 215 | 216 | from_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); 217 | 218 | impl Default for Global { 219 | fn default() -> Global { 220 | Global(GlobalState::None) 221 | } 222 | } 223 | 224 | enum GlobalState { 225 | None, 226 | One(TypeId, Box), 227 | Many(Map), 228 | } 229 | 230 | ///Settings for `keep-alive` connections to the server. 231 | pub struct KeepAlive { 232 | ///How long a `keep-alive` connection may idle before it's forced close. 233 | pub timeout: Duration, 234 | 235 | ///The number of threads in the thread pool that should be kept free from 236 | ///idling threads. Connections will be closed if the number of idle 237 | ///threads goes below this. 238 | pub free_threads: usize, 239 | } 240 | -------------------------------------------------------------------------------- /src/server/instance.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::sync::atomic::{AtomicUsize, Ordering}; 3 | 4 | use time; 5 | 6 | use num_cpus; 7 | 8 | use url::percent_encoding::percent_decode; 9 | use url::Url; 10 | 11 | use hyper; 12 | use hyper::server::Handler as HyperHandler; 13 | use hyper::header::{Date, ContentType}; 14 | use hyper::mime::Mime; 15 | use hyper::uri::RequestUri; 16 | 17 | pub use hyper::server::Listening; 18 | 19 | use anymap::AnyMap; 20 | 21 | use StatusCode; 22 | 23 | use context::{self, Context, UriPath, MaybeUtf8Owned, Parameters}; 24 | use filter::{FilterContext, ContextFilter, ContextAction, ResponseFilter}; 25 | use handler::{HandleRequest, Environment}; 26 | use response::Response; 27 | use header::HttpDate; 28 | use server::{Global, KeepAlive}; 29 | use net::SslServer; 30 | 31 | use HttpResult; 32 | use Server; 33 | 34 | use utils; 35 | 36 | ///A runnable instance of a server. 37 | /// 38 | ///It's not meant to be used directly, 39 | ///unless additional control is required. 40 | /// 41 | ///```no_run 42 | ///# use rustful::{Server, Handler, Context, Response}; 43 | ///# #[derive(Default)] 44 | ///# struct R; 45 | ///# impl Handler for R { 46 | ///# fn handle(&self, _context: Context, _response: Response) {} 47 | ///# } 48 | ///# let router = R; 49 | ///let server_instance = Server { 50 | /// host: 8080.into(), 51 | /// handlers: router, 52 | /// ..Server::default() 53 | ///}.build(); 54 | ///``` 55 | pub struct ServerInstance { 56 | handlers: R, 57 | 58 | host: SocketAddr, 59 | 60 | server: String, 61 | content_type: Mime, 62 | 63 | threads: usize, 64 | keep_alive: Option, 65 | threads_in_use: AtomicUsize, 66 | 67 | context_filters: Vec>, 68 | response_filters: Vec>, 69 | 70 | global: Global, 71 | } 72 | 73 | impl ServerInstance { 74 | ///Create a new server instance, with the provided configuration. This is 75 | ///the same as `Server{...}.build()`. 76 | pub fn new(config: Server) -> ServerInstance { 77 | ServerInstance { 78 | handlers: config.handlers, 79 | host: config.host.into(), 80 | server: config.server, 81 | content_type: config.content_type, 82 | threads: config.threads.unwrap_or_else(|| (num_cpus::get() * 5) / 4), 83 | keep_alive: config.keep_alive, 84 | threads_in_use: AtomicUsize::new(0), 85 | context_filters: config.context_filters, 86 | response_filters: config.response_filters, 87 | global: config.global, 88 | } 89 | } 90 | 91 | ///Start the server. 92 | pub fn run(self) -> HttpResult { 93 | let host = self.host; 94 | let threads = self.threads; 95 | let mut server = hyper::server::Server::http(host)?; 96 | server.keep_alive(self.keep_alive.as_ref().map(|k| k.timeout)); 97 | server.handle_threads(self, threads) 98 | } 99 | 100 | ///Start the server with SSL. 101 | pub fn run_https(self, ssl: S) -> HttpResult { 102 | let host = self.host; 103 | let threads = self.threads; 104 | let mut server = hyper::server::Server::https(host, ssl)?; 105 | server.keep_alive(self.keep_alive.as_ref().map(|k| k.timeout)); 106 | server.handle_threads(self, threads) 107 | } 108 | 109 | fn modify_context(&self, filter_storage: &mut AnyMap, context: &mut Context) -> ContextAction { 110 | let mut result = ContextAction::Next; 111 | 112 | for filter in &self.context_filters { 113 | result = match result { 114 | ContextAction::Next => { 115 | let filter_context = FilterContext { 116 | storage: filter_storage, 117 | global: &self.global, 118 | }; 119 | filter.modify(filter_context, context) 120 | }, 121 | _ => return result 122 | }; 123 | } 124 | 125 | result 126 | } 127 | 128 | } 129 | 130 | struct ParsedUri { 131 | host: Option<(String, Option)>, 132 | uri_path: UriPath, 133 | query: Parameters, 134 | fragment: Option 135 | } 136 | 137 | impl HyperHandler for ServerInstance { 138 | fn handle<'a, 'b>(&'a self, request: hyper::server::request::Request<'a, 'b>, writer: hyper::server::response::Response<'a>) { 139 | let ( 140 | request_addr, 141 | request_method, 142 | mut request_headers, 143 | request_uri, 144 | request_version, 145 | request_reader 146 | ) = request.deconstruct(); 147 | 148 | let force_close = if let Some(ref keep_alive) = self.keep_alive { 149 | self.threads_in_use.load(Ordering::SeqCst) + keep_alive.free_threads > self.threads 150 | } else { 151 | false 152 | }; 153 | 154 | let mut response = Response::new(writer, &self.response_filters, &self.global, force_close); 155 | response.headers_mut().set(Date(HttpDate(time::now_utc()))); 156 | response.headers_mut().set(ContentType(self.content_type.clone())); 157 | response.headers_mut().set(hyper::header::Server(self.server.clone())); 158 | 159 | let path_components = match request_uri { 160 | RequestUri::AbsoluteUri(url) => Some(parse_url(&url)), 161 | RequestUri::AbsolutePath(path) => Some(parse_path(&path)), 162 | RequestUri::Star => { 163 | Some(ParsedUri { 164 | host: None, 165 | uri_path: UriPath::Asterisk, 166 | query: Parameters::new(), 167 | fragment: None 168 | }) 169 | }, 170 | _ => None 171 | }; 172 | 173 | match path_components { 174 | Some(ParsedUri{ host, uri_path, query, fragment }) => { 175 | if let Some((name, port)) = host { 176 | request_headers.set(::header::Host { 177 | hostname: name, 178 | port: port 179 | }); 180 | } 181 | 182 | let body = context::body::BodyReader::from_reader(request_reader, &request_headers); 183 | 184 | let mut context = Context { 185 | headers: request_headers, 186 | http_version: request_version, 187 | method: request_method, 188 | address: request_addr, 189 | uri_path: uri_path, 190 | hyperlinks: vec![], 191 | variables: Parameters::new(), 192 | query: query.into(), 193 | fragment: fragment, 194 | global: &self.global, 195 | body: body 196 | }; 197 | 198 | let mut filter_storage = AnyMap::new(); 199 | 200 | match self.modify_context(&mut filter_storage, &mut context) { 201 | ContextAction::Next => { 202 | *response.filter_storage_mut() = filter_storage; 203 | 204 | let path = context.uri_path.clone(); 205 | if let Some(path) = path.as_path() { 206 | let result = self.handlers.handle_request(Environment { 207 | context: context, 208 | response: response, 209 | route_state: (&path[..]).into(), 210 | }); 211 | 212 | if let Err(mut environment) = result { 213 | if environment.response.status() == StatusCode::Ok { 214 | environment.response.set_status(StatusCode::NotFound); 215 | } 216 | } 217 | } 218 | }, 219 | ContextAction::Abort(status) => { 220 | *response.filter_storage_mut() = filter_storage; 221 | response.set_status(status); 222 | } 223 | } 224 | }, 225 | None => { 226 | response.set_status(StatusCode::BadRequest); 227 | } 228 | } 229 | } 230 | 231 | fn on_connection_start(&self) { 232 | self.threads_in_use.fetch_add(1, Ordering::SeqCst); 233 | } 234 | 235 | fn on_connection_end(&self) { 236 | self.threads_in_use.fetch_sub(1, Ordering::SeqCst); 237 | } 238 | } 239 | 240 | fn parse_path(path: &str) -> ParsedUri { 241 | match path.find('?') { 242 | Some(index) => { 243 | let (query, fragment) = parse_fragment(&path[index+1..]); 244 | 245 | let mut path: Vec<_> = percent_decode(path[..index].as_bytes()).collect(); 246 | if path.is_empty() { 247 | path.push(b'/'); 248 | } 249 | 250 | ParsedUri { 251 | host: None, 252 | uri_path: UriPath::Path(path.into()), 253 | query: utils::parse_parameters(query.as_bytes()), 254 | fragment: fragment.map(|f| percent_decode(f.as_bytes()).collect::>().into()), 255 | } 256 | }, 257 | None => { 258 | let (path, fragment) = parse_fragment(&path); 259 | 260 | let mut path: Vec<_> = percent_decode(path.as_bytes()).collect(); 261 | if path.is_empty() { 262 | path.push(b'/'); 263 | } 264 | 265 | ParsedUri { 266 | host: None, 267 | uri_path: UriPath::Path(path.into()), 268 | query: Parameters::new(), 269 | fragment: fragment.map(|f| percent_decode(f.as_bytes()).collect::>().into()) 270 | } 271 | } 272 | } 273 | } 274 | 275 | fn parse_fragment(path: &str) -> (&str, Option<&str>) { 276 | match path.find('#') { 277 | Some(index) => (&path[..index], Some(&path[index+1..])), 278 | None => (path, None) 279 | } 280 | } 281 | 282 | fn parse_url(url: &Url) -> ParsedUri { 283 | let mut path = vec![]; 284 | path.extend(percent_decode(url.path().as_bytes())); 285 | 286 | let query = url.query_pairs() 287 | .map(|(k, v)| (k.into_owned(), v.into_owned())) 288 | .collect(); 289 | 290 | let host = url.host_str().map(|host| (host.into(), url.port())); 291 | 292 | ParsedUri { 293 | host: host, 294 | uri_path: UriPath::Path(path.into()), 295 | query: query, 296 | fragment: url.fragment().as_ref().map(|f| percent_decode(f.as_bytes()).collect::>().into()) 297 | } 298 | } 299 | 300 | 301 | #[test] 302 | fn parse_path_parts() { 303 | let with = "this".to_owned().into(); 304 | let and = "that".to_owned().into(); 305 | let ParsedUri { uri_path, query, fragment, .. } = parse_path("/path/to/something?with=this&and=that#lol"); 306 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 307 | assert_eq!(query.get_raw("with"), Some(&with)); 308 | assert_eq!(query.get_raw("and"), Some(&and)); 309 | assert_eq!(fragment, Some("lol".to_owned().into())); 310 | } 311 | 312 | #[test] 313 | fn parse_strange_path() { 314 | let with = "this".to_owned().into(); 315 | let and = "what?".to_owned().into(); 316 | let ParsedUri { uri_path, query, fragment, .. } = parse_path("/path/to/something?with=this&and=what?#"); 317 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 318 | assert_eq!(query.get_raw("with"), Some(&with)); 319 | assert_eq!(query.get_raw("and"), Some(&and)); 320 | assert_eq!(fragment, Some(String::new().into())); 321 | } 322 | 323 | #[test] 324 | fn parse_missing_path_parts() { 325 | let with = "this".to_owned().into(); 326 | let and = "that".to_owned().into(); 327 | let ParsedUri { uri_path, query, fragment, .. } = parse_path("/path/to/something?with=this&and=that"); 328 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 329 | assert_eq!(query.get_raw("with"), Some(&with)); 330 | assert_eq!(query.get_raw("and"), Some(&and)); 331 | assert_eq!(fragment, None); 332 | 333 | 334 | let ParsedUri { uri_path, query, fragment, .. } = parse_path("/path/to/something#lol"); 335 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 336 | assert_eq!(query.len(), 0); 337 | assert_eq!(fragment, Some("lol".to_owned().into())); 338 | 339 | 340 | let ParsedUri { uri_path, query, fragment, .. } = parse_path("?with=this&and=that#lol"); 341 | assert_eq!(uri_path.as_path(), Some("/".into())); 342 | assert_eq!(query.get_raw("with"), Some(&with)); 343 | assert_eq!(query.get_raw("and"), Some(&and)); 344 | assert_eq!(fragment, Some("lol".to_owned().into())); 345 | } 346 | 347 | 348 | #[test] 349 | fn parse_url_parts() { 350 | let with = "this".to_owned().into(); 351 | let and = "that".to_owned().into(); 352 | let url = Url::parse("http://example.com/path/to/something?with=this&and=that#lol").unwrap(); 353 | let ParsedUri { uri_path, query, fragment, .. } = parse_url(&url); 354 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 355 | assert_eq!(query.get_raw("with"), Some(&with)); 356 | assert_eq!(query.get_raw("and"), Some(&and)); 357 | assert_eq!(fragment, Some("lol".to_owned().into())); 358 | } 359 | 360 | #[test] 361 | fn parse_strange_url() { 362 | let with = "this".to_owned().into(); 363 | let and = "what?".to_owned().into(); 364 | let url = Url::parse("http://example.com/path/to/something?with=this&and=what?#").unwrap(); 365 | let ParsedUri { uri_path, query, fragment, .. } = parse_url(&url); 366 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 367 | assert_eq!(query.get_raw("with"), Some(&with)); 368 | assert_eq!(query.get_raw("and"), Some(&and)); 369 | assert_eq!(fragment, Some(String::new().into())); 370 | } 371 | 372 | #[test] 373 | fn parse_missing_url_parts() { 374 | let with = "this".to_owned().into(); 375 | let and = "that".to_owned().into(); 376 | let url = Url::parse("http://example.com/path/to/something?with=this&and=that").unwrap(); 377 | let ParsedUri { uri_path, query, fragment, .. } = parse_url(&url); 378 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 379 | assert_eq!(query.get_raw("with"), Some(&with)); 380 | assert_eq!(query.get_raw("and"), Some(&and)); 381 | assert_eq!(fragment, None); 382 | 383 | 384 | let url = Url::parse("http://example.com/path/to/something#lol").unwrap(); 385 | let ParsedUri { uri_path, query, fragment, .. } = parse_url(&url); 386 | assert_eq!(uri_path.as_path(), Some("/path/to/something".into())); 387 | assert_eq!(query.len(), 0); 388 | assert_eq!(fragment, Some("lol".to_owned().into())); 389 | 390 | 391 | let url = Url::parse("http://example.com?with=this&and=that#lol").unwrap(); 392 | let ParsedUri { uri_path, query, fragment, .. } = parse_url(&url); 393 | assert_eq!(uri_path.as_path(), Some("/".into())); 394 | assert_eq!(query.get_raw("with"), Some(&with)); 395 | assert_eq!(query.get_raw("and"), Some(&and)); 396 | assert_eq!(fragment, Some("lol".to_owned().into())); 397 | } 398 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | //!Server configuration and instance. 2 | 3 | use std::borrow::ToOwned; 4 | 5 | use hyper; 6 | use hyper::mime::Mime; 7 | 8 | pub use hyper::server::Listening; 9 | 10 | use filter::{ContextFilter, ResponseFilter}; 11 | use handler::HandleRequest; 12 | use net::SslServer; 13 | 14 | use HttpResult; 15 | 16 | pub use self::instance::ServerInstance; 17 | pub use self::config::{Host, Global, KeepAlive}; 18 | 19 | mod instance; 20 | mod config; 21 | 22 | ///Used to set up and run a server. 23 | /// 24 | ///```no_run 25 | ///# use std::error::Error; 26 | ///# use rustful::{Server, Handler, Context, Response}; 27 | ///# #[derive(Default)] 28 | ///# struct R; 29 | ///# impl Handler for R { 30 | ///# fn handle(&self, _context: Context, _response: Response) {} 31 | ///# } 32 | ///# let router = R; 33 | ///let server_result = Server { 34 | /// host: 8080.into(), 35 | /// handlers: router, 36 | /// ..Server::default() 37 | ///}.run(); 38 | /// 39 | ///match server_result { 40 | /// Ok(_server) => {}, 41 | /// Err(e) => println!("could not start server: {}", e.description()) 42 | ///} 43 | ///``` 44 | pub struct Server { 45 | ///One or several response handlers. 46 | pub handlers: R, 47 | 48 | ///The host address and port where the server will listen for requests. 49 | ///Default is `0.0.0.0:80`. 50 | pub host: Host, 51 | 52 | ///The number of threads to be used in the server thread pool. The default 53 | ///(`None`) will cause the server to optimistically use the formula 54 | ///`(num_cores * 5) / 4`. 55 | pub threads: Option, 56 | 57 | ///The server's `keep-alive` policy. Setting this to `Some(...)` will 58 | ///allow `keep-alive` connections with a timeout, and keeping it as `None` 59 | ///will force connections to close after each request. Default is `None`. 60 | pub keep_alive: Option, 61 | 62 | ///The content of the server header. Default is `"rustful"`. 63 | pub server: String, 64 | 65 | ///The default media type. Default is `text/plain, charset: UTF-8`. 66 | pub content_type: Mime, 67 | 68 | ///Globally accessible data. 69 | pub global: Global, 70 | 71 | ///The context filter stack. 72 | pub context_filters: Vec>, 73 | 74 | ///The response filter stack. 75 | pub response_filters: Vec> 76 | } 77 | 78 | impl Server { 79 | ///Set up a new standard server. This can be useful when `handlers` 80 | ///doesn't implement `Default`: 81 | /// 82 | ///```no_run 83 | ///# use std::error::Error; 84 | ///# use rustful::{Server, Handler, Context, Response}; 85 | ///let handler = |context: Context, response: Response| { 86 | /// //... 87 | ///}; 88 | /// 89 | ///let server_result = Server { 90 | /// host: 8080.into(), 91 | /// ..Server::new(handler) 92 | ///}; 93 | ///``` 94 | pub fn new(handlers: R) -> Server { 95 | Server { 96 | handlers: handlers, 97 | host: 80.into(), 98 | threads: None, 99 | keep_alive: None, 100 | server: "rustful".to_owned(), 101 | content_type: Mime( 102 | hyper::mime::TopLevel::Text, 103 | hyper::mime::SubLevel::Html, 104 | vec![(hyper::mime::Attr::Charset, hyper::mime::Value::Utf8)] 105 | ), 106 | global: Global::default(), 107 | context_filters: Vec::new(), 108 | response_filters: Vec::new(), 109 | } 110 | } 111 | 112 | ///Start the server. 113 | pub fn run(self) -> HttpResult { 114 | self.build().run() 115 | } 116 | 117 | ///Start the server with SSL. 118 | pub fn run_https(self, ssl: S) -> HttpResult { 119 | self.build().run_https(ssl) 120 | } 121 | 122 | ///Build a runnable instance of the server. 123 | pub fn build(self) -> ServerInstance { 124 | ServerInstance::new(self) 125 | } 126 | } 127 | 128 | impl Default for Server { 129 | fn default() -> Server { 130 | Server::new(R::default()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use url::percent_encoding::percent_decode; 3 | use context::Parameters; 4 | 5 | pub fn parse_parameters(source: &[u8]) -> Parameters { 6 | let mut parameters = Parameters::new(); 7 | let source: Vec = source.iter() 8 | .map(|&e| if e == b'+' { b' ' } else { e }) 9 | .collect(); 10 | 11 | for parameter in source.split(|&e| e == b'&') { 12 | let mut parts = parameter.split(|&e| e == b'='); 13 | 14 | match (parts.next(), parts.next()) { 15 | (Some(name), Some(value)) => { 16 | let name: Vec<_> = percent_decode(name).collect(); 17 | let value: Vec<_> = percent_decode(value).collect(); 18 | parameters.insert(name, value); 19 | }, 20 | (Some(name), None) => { 21 | let name: Vec<_> = percent_decode(name).collect(); 22 | parameters.insert(name, String::new()); 23 | }, 24 | _ => {} 25 | } 26 | } 27 | 28 | parameters 29 | } 30 | 31 | ///Extension trait for byte vectors. 32 | pub trait BytesExt { 33 | ///Copy a number of bytes to the vector. 34 | fn push_bytes(&mut self, bytes: &[u8]); 35 | } 36 | 37 | impl BytesExt for Vec { 38 | fn push_bytes(&mut self, bytes: &[u8]) { 39 | self.write_all(bytes).unwrap(); 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod test { 45 | use std::borrow::ToOwned; 46 | use super::parse_parameters; 47 | 48 | #[test] 49 | fn parsing_parameters() { 50 | let parameters = parse_parameters(b"a=1&aa=2&ab=202"); 51 | let a = "1".to_owned().into(); 52 | let aa = "2".to_owned().into(); 53 | let ab = "202".to_owned().into(); 54 | assert_eq!(parameters.get_raw("a"), Some(&a)); 55 | assert_eq!(parameters.get_raw("aa"), Some(&aa)); 56 | assert_eq!(parameters.get_raw("ab"), Some(&ab)); 57 | } 58 | 59 | #[test] 60 | fn parsing_parameters_with_plus() { 61 | let parameters = parse_parameters(b"a=1&aa=2+%2B+extra+meat&ab=202+fifth+avenue"); 62 | let a = "1".to_owned().into(); 63 | let aa = "2 + extra meat".to_owned().into(); 64 | let ab = "202 fifth avenue".to_owned().into(); 65 | assert_eq!(parameters.get_raw("a"), Some(&a)); 66 | assert_eq!(parameters.get_raw("aa"), Some(&aa)); 67 | assert_eq!(parameters.get_raw("ab"), Some(&ab)); 68 | } 69 | 70 | #[test] 71 | fn parsing_strange_parameters() { 72 | let parameters = parse_parameters(b"a=1=2&=2&ab="); 73 | let a = "1".to_owned().into(); 74 | let aa = "2".to_owned().into(); 75 | let ab = "".to_owned().into(); 76 | assert_eq!(parameters.get_raw("a"), Some(&a)); 77 | assert_eq!(parameters.get_raw(""), Some(&aa)); 78 | assert_eq!(parameters.get_raw("ab"), Some(&ab)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" == "" ]; then 4 | echo "Usage: version.sh X.Y.Z" 5 | exit 1 6 | fi 7 | 8 | current_version="$(cargo read-manifest | sed 's/.*"version":"\([^"]\+\)".*/\1/g')" 9 | current_short_version="$(echo "$current_version" | sed 's/\([^.]\+\.[^.]\+\)\..*/\1/g')" 10 | new_short_version="$(echo "$1" | sed 's/\([^.]\+\.[^.]\+\)\..*/\1/g')" 11 | 12 | echo "updating from $current_version to $1" 13 | 14 | sed -i 's/version = "'$current_version'" #automatically updated/version = "'$1'" #automatically updated/' Cargo.toml 15 | sed -i 's/rustful = "'$current_short_version'"/rustful = "'$new_short_version'"/' README.md 16 | 17 | bash scripts/changelog.sh --------------------------------------------------------------------------------