├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── checklist.md ├── rustfmt.toml └── src ├── attachment.rs ├── database.rs ├── error.rs ├── lib.rs ├── nok.rs ├── path.rs ├── revision.rs ├── root.rs └── testing ├── fake_server.rs └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | script: cargo test --verbose --no-run && cargo test --verbose --lib 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CouchDB-rs Change Log 2 | 3 | ## v0.6.1 (unreleased) 4 | 5 | No changes yet! 6 | 7 | ## v0.6.0 (2017-07-17) 8 | 9 | This release is a total rewrite owing to a change in project goals. The 10 | library now less ambitiously provides a set of passive types and does 11 | _not_ provide an HTTP client. 12 | 13 | Here's what the library supports so far: 14 | 15 | * There's an `Attachment` type for intelligently working with document 16 | attachments. 17 | * There's a suite of path-related types (in the `path` submodule) for 18 | specifying the locations of CouchDB resources in a type-safe way. 19 | * There are a few other types for deserializing specific CouchDB JSON 20 | objects. For example, the `Nok` type can capture information from a 21 | CouchDB error response. 22 | 23 | Also, all project dependencies are now up-to-date. 24 | 25 | ## v0.5.2 (2017-05-26) 26 | 27 | This release fixes compiler warnings that are soon to become hard 28 | errors. See issue [#57][issue_57] for more information. 29 | 30 | ## v0.5.1 (2016-02-12) 31 | 32 | This release extends the crate's coverage of the CouchDB API, deprecates 33 | a few poorly named things, and improves documentation. 34 | 35 | ### Deprecated 36 | 37 | * The method `Client::post_to_database` is deprecated. Use 38 | `Client::post_database` instead. 39 | * The type `PostToDatabase` is deprecated. Use `PostDatabase` instead. 40 | 41 | ### New 42 | 43 | * The `GetChanges` action type is new and allows applications to 44 | get database changes via the `/db/_changes` resource. 45 | * The `GetRoot` action type is new and allows applications to get the 46 | CouchDB root resource (`/`), which includes the server's version 47 | information. 48 | * There's now support for getting documents at a specific revision via 49 | the `?rev` query parameter—i.e., `GET /db/doc?rev=`. 50 | * This release adds limited support for getting embedded attachments via 51 | the `GetDocument` action. 52 | * The `Document` type now contains a `deleted` field for signifying 53 | whether the document has been deleted. 54 | * The `action` module's documentation now contains a feature table 55 | showing, in detail, the crate's coverage of the CouchDB API. 56 | 57 | ## v0.5.0 (2016-01-17) 58 | 59 | This release makes a few API changes to continue the library's progress 60 | towards optimal type-safety and convenience. 61 | 62 | ### Breaking changes 63 | 64 | * The `Document` type has been refactored to make it easier to use. 65 | * The `Document` type is no longer a generic type, nor is the 66 | `content` field publicly accessible. Applications now access 67 | document content via a new `into_content` method, which does the 68 | JSON-decoding. See issue [#28][issue_28] for more information. 69 | * The `revision` field has been renamed to `rev`, which more closely 70 | matches the CouchDB name. 71 | * The `Document` type implements `serde::Deserialize` instead of a 72 | custom `from_reader` deserialization method. This should _not_ 73 | affect applications. 74 | * The `Document` type no longer implements these traits: `Eq`, 75 | `Hash`, `Ord`, and `PartialOrd`. 76 | * Throughout the project, the term “command” has been replaced with 77 | “action”. The only API change is that the 78 | `command` module is now named the `action` module. This should _not_ 79 | affect applications. See issue [#32][issue_32] for more information. 80 | * The `PostToDatabase` action now returns `(DocumentId, Revision)`, not 81 | `(Revision, DocumentId)`. 82 | * The following types now have at least one private field and can no 83 | longer be directly constructed by applications: 84 | * `Database`, 85 | * `Design`, 86 | * `ErrorResponse`, 87 | * `ViewFunction`, 88 | * `ViewResult`, and 89 | * `ViewRow`. 90 | * The `DeleteDocument` action now returns the revision of the deleted 91 | document. Previously the action returned nothing. 92 | * The `Server` type has been moved/renamed to `testing::FakeServer`. 93 | 94 | ### New 95 | 96 | * New `ViewFunctionBuilder` type for constructing a `ViewFunction` 97 | instance. 98 | * New `Revision::update_number` method for getting the _update number_ 99 | part of a revision. 100 | 101 | ### Additional notes 102 | 103 | * The project is now dual-licensed under Apache-2.0 and MIT. See issue 104 | [#31][issue_31] for more information. 105 | * Actions are now tested as unit tests _and_ integration tests. 106 | Previously, actions were tested only as integration tests. 107 | Unit-testing now provides good test coverage without having the 108 | CouchDB server installed on the local machine. 109 | * The project now has support for Travis CI. 110 | 111 | ## v0.4.0 (2016-01-03) 112 | 113 | This release introduces several breaking changes to improve type-safety 114 | and ease-of-use, as well as to fix inconsistencies between the crate's 115 | API and the CouchDB API. 116 | 117 | ### Breaking changes 118 | 119 | * The _path_ types of v0.3.x (e.g., `DocumentPath`, etc.) are now split 120 | into _path_, _id_, and _name_ types (e.g., `DocumentPath`, 121 | `DocumentId`, and `DocumentName`, etc.). Client commands use path 122 | types as input; id and name types are used everywhere else to match 123 | what the CouchDB API uses. 124 | * Paths now must begin with a slash (e.g., `/db/docid` vs the 125 | `db/docid` format of v0.3.x). 126 | * Path types now implement `std::str::FromStr` instead of 127 | `From`. This means string-to-path conversions now may 128 | fail. 129 | * The `Revision` type now fully understands CouchDB revisions. 130 | * The `Revision` type now implements `std::str::FromStr` instead of 131 | `From<&str>` and `From`. This means string-to-revision 132 | conversion now may fail. 133 | * The `Revision` type no longer implements `AsRef`. 134 | * Revisions now compare as numbers, not strings, to match what the 135 | CouchDB server does. 136 | * The `Error` enum has been refactored to be simpler. 137 | * Many error variants documented in v0.3.x are now hidden or 138 | removed. The remaining variants are either CouchDB response errors 139 | or are for path-parsing. 140 | * All CouchDB response error values are now wrapped in an `Option` 141 | to reflect how the CouchDB server returns no detailed error 142 | information for HEAD requests. 143 | * All non-hidden error variant values are now tuples, not structs. 144 | * The `InvalidRequest` error variant has been renamed to 145 | `BadRequest`. The new name matches HTTP status code 400 of the 146 | same name. 147 | 148 | ### Fixes 149 | 150 | * When getting a document, the client now ignores any `_attachments` 151 | field in the CouchDB response. Previously, the client included the 152 | attachment info in the document content. 153 | * The client no longer tries to decode the server's response as JSON 154 | when the client receives an "unauthorized" error as a result of 155 | executing a client command to HEAD a document. 156 | 157 | ### Additional notes 158 | 159 | * Test coverage has expanded, and test cases have been broken out into 160 | smaller cases. Consequently, there are now more than 200 additional 161 | test cases than in the v0.3.1 release. 162 | * The source code has been reorganized to be more hierarchical. CouchDB 163 | types, path types, and client commands now reside within distinct 164 | submodules. 165 | 166 | ## v0.3.1 (2015-12-21) 167 | 168 | This release expands the crate's coverage of the CouchDB API. 169 | 170 | ### New 171 | 172 | * There's a new client command to POST to a database. 173 | * The `Revision` type now implements `serde::Serialize` and 174 | `serde::Deserialize`. 175 | 176 | ## v0.3.0 (2015-12-12) 177 | 178 | This release overhauls the crate's API to provide stronger type-safety 179 | and to be more Rust-idiomatic. 180 | 181 | ### Breaking changes 182 | 183 | * There are new types for specifying databases, documents, and views. 184 | * All raw-string path parameters have been replaced with new _path_ 185 | types: `DatabasePath`, `DocumentPath`, and `ViewPath`. The 186 | signatures of all client commands have changed, as well as the 187 | `Document` and `ViewRow` types. 188 | * There's a new `DocumentId` type that combines a document name with 189 | its type (i.e., _normal_ document vs _design_ document vs _local_ 190 | document). 191 | * All client commands specific to design documents (e.g., 192 | `get_design_document`) have been removed. Design documents are now 193 | accessible via generic document commands (e.g., `get_document`). 194 | * The `ViewResult` struct now wraps its `total_rows` and `offset` fields 195 | in an `Option`. 196 | * The underlying type for `ViewFunctionMap` is now `HashMap`, not 197 | `BTreeMap`. 198 | * The `Command` trait is now private. 199 | * Crate dependencies now specify explicit version ranges instead of `*`. 200 | 201 | ### Fixes 202 | 203 | * All JSON-decoding errors are now reported as the `Decode` error 204 | variant. Previously, some decoding errors were reported as a hidden 205 | variant. 206 | * The `Revision` type now compares as case-insensitive, matching CouchDB 207 | semantics. 208 | * A bug has been fixed that caused CPU spin on Windows in the `Server` 209 | type. 210 | 211 | ### New 212 | 213 | * The `Database` type now includes all fields returned by the CouchDB 214 | server as a result of a client command to GET a database. 215 | * There's a new `DesignBuilder` type to make it easier to construct 216 | `Design` instances. 217 | * The `Clone`, `Hash`, `Eq`, `PartialEq`, `Ord`, and `PartialOrd` traits 218 | have been implemented for all types where appropriate. 219 | 220 | ## v0.2.0 (2015-10-17) 221 | 222 | ### Breaking changes 223 | 224 | * Client command-construction methods (e.g., `put_document`, 225 | `get_database`, etc.) now bind the lifetime of the returned command to 226 | the lifetimes of all `&str` parameters. 227 | * The client command to GET a design document now strips `"_design/"` 228 | from the resulting document id. 229 | 230 | ### Additional notes 231 | 232 | * The integration test has been split into separate test cases, one for 233 | each CouchDB command. 234 | * Some support has been added for running tests on Windows. See issue 235 | #8. 236 | 237 | ## v0.1.0 (2015-09-21) 238 | 239 | ### Breaking changes 240 | 241 | * The `Revision` type now implements the `AsRef` trait instead of 242 | implementing the `as_str` method. 243 | * Client commands that have a revision parameter now borrow the 244 | `Revision` argument instead of taking ownership. This resolves issue 245 | #1. 246 | * Disallow construction of a `Revision` from an arbitrary string. 247 | * The `ServerErrorResponse` type has been renamed to `ErrorResponse`, 248 | which is now used consistently for reporting CouchDB server errors. 249 | * The `DesignDocument` type has been renamed to `Design`. 250 | * There's a new `IntoUrl` trait that aliases `hyper::IntoUrl`. 251 | 252 | ### Fixes 253 | 254 | * The `views` field of the `Design` struct is now public. 255 | 256 | ### New 257 | 258 | * There's a new `ViewFunctionMap` collection type. 259 | 260 | ## v0.0.1 (2015-09-07) 261 | 262 | This release adds and improves API doc comments. 263 | 264 | ## v0.0.0 (2015-09-05) 265 | 266 | This is the first release. It provides support for client commands to 267 | manipulate databases (HEAD, GET, PUT, and DELETE), to manipulate 268 | documents (HEAD, GET, PUT, and DELETE), and to execute views (GET). 269 | 270 | [issue_28]: https://github.com/couchdb-rs/couchdb/issues/28 "Issue #28" 271 | [issue_31]: https://github.com/couchdb-rs/couchdb/issues/31 "Issue #31" 272 | [issue_32]: https://github.com/couchdb-rs/couchdb/issues/32 "Issue #32" 273 | [issue_57]: https://github.com/couchdb-rs/couchdb/issues/57 "Issue #57" 274 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "couchdb" 3 | version = "0.6.1-master" 4 | authors = ["Craig M. Brandenburg "] 5 | license = "MIT/Apache-2.0" 6 | description = "The couchdb library provides types for working with CouchDB." 7 | repository = "https://github.com/couchdb-rs/couchdb" 8 | documentation = "https://couchdb-rs.github.io/couchdb/doc/v0.6.0/couchdb/" 9 | keywords = ["couch", "couchdb", "database", "nosql"] 10 | 11 | [dependencies] 12 | base64 = "0.6.0" 13 | mime = "0.3.2" 14 | regex = "0.2.2" 15 | serde = "1.0" 16 | serde_derive = "1.0" 17 | tempdir = "0.3.5" 18 | url = "1.5" 19 | uuid = { version = "0.5.1", features = ["serde"] } 20 | 21 | [dev-dependencies] 22 | reqwest = "0.7.1" 23 | serde_json = "1.0" 24 | -------------------------------------------------------------------------------- /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) [2015] [Craig M. Brandenburg ] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CouchDB 2 | 3 | [![Build Status](https://travis-ci.org/couchdb-rs/couchdb.svg?branch=master)](https://travis-ci.org/couchdb-rs/couchdb) 4 | 5 | --- 6 | 7 | **This project is reborn!** 8 | 9 | As of its v0.6.0 release, the `couchdb` crate has new life as a toolkit 10 | instead of providing a full-blown client. 11 | 12 | In a nutshell, the `couchdb` crate now provides passive, 13 | “building-block” types for working with CouchDB in Rust. Applications 14 | may use as few or as many of these types as makes the most sense. Actual 15 | HTTP communication with a CouchDB server is now accomplished by some 16 | other means, such as [hyper][hyper_crate] or [reqwest][reqwest_crate]. 17 | 18 | ## Project roadmap 19 | 20 | The latest release is **v0.6.0**, which was released **2017-07-17**. 21 | 22 | * [v0.6.0 change log](https://github.com/couchdb-rs/couchdb/blob/v0.6.0/CHANGELOG.md). 23 | * [v0.6.0 documentation](https://couchdb-rs.github.io/couchdb/doc/v0.6.0/couchdb/index.html). 24 | * [v0.6.0 issues](https://github.com/couchdb-rs/couchdb/issues?q=milestone%3Av0.6.0). 25 | * [v0.6.0 crates.io page](https://crates.io/crates/couchdb/0.6.0). 26 | 27 | The next release is expected to be **v0.6.1** and has no schedule. 28 | 29 | * [master change log](https://github.com/couchdb-rs/couchdb/blob/master/CHANGELOG.md). 30 | * [v0.6.1 issues](https://github.com/couchdb-rs/couchdb/issues?q=milestone%3Av0.6.1). 31 | 32 | ## License 33 | 34 | CouchDB-rs is licensed under either of: 35 | 36 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 37 | http://www.apache.org/licenses/LICENSE-2.0), or 38 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 39 | http://opensource.org/licenses/MIT). 40 | 41 | ## Feedback 42 | 43 | Do you find this crate useful? Not useful? [Please send 44 | feedback!](mailto:c.m.brandenburg@gmail.com) 45 | 46 | [chill-rs]: https://github.com/chill-rs/chill 47 | [hyper_crate]: https://crates.io/crates/hyper 48 | [reqwest_crate]: https://crates.io/crates/reqwest 49 | [rethinking_couchdb_in_rust]: https://cmbrandenburg.github.io/post/2016-02-23-rethinking_couchdb_in_rust/ 50 | -------------------------------------------------------------------------------- /checklist.md: -------------------------------------------------------------------------------- 1 | # CouchDB-rs Release Checklist 2 | 3 | 1. Resolve any `FIXME` comments in the code. 4 | 5 | 1. Ensure the `couchdb` crate builds and runs using the latest 6 | dependencies. 7 | 8 | $ cargo update && 9 | cargo test && 10 | cargo test --release 11 | 12 | If any errors occur then fix them! 13 | 14 | 1. Create a temporary Git branch for the release. 15 | 16 | $ git checkout -b release_prep 17 | 18 | 1. Ensure packaging succeeds. 19 | 20 | $ cargo package 21 | 22 | If any errors occur then fix them! 23 | 24 | 1. Update project files. 25 | 26 | 1. Edit `Cargo.toml` to declare the correct version for this 27 | crate. 28 | 29 | 1. E.g., remove the `-master` suffix. 30 | 31 | 1. Ensure the documentation link is correct. 32 | 33 | 1. Edit `CHANGELOG.md` to add the date for the new release and 34 | remove the “unreleased” adjective. Ensure the change log is 35 | up-to-date, clear, and well formatted. 36 | 37 | 1. Edit `README.md` to update all references to the latest release 38 | and next release. 39 | 40 | 1. Ensure there are no untracked files in the working directory. 41 | 42 | 1. Commit changes. 43 | 44 | $ git commit 45 | 46 | 1. Build and publish Rust documentation for the new version. 47 | 48 | 1. Build. 49 | 50 | $ cargo clean && 51 | cargo doc --no-deps && 52 | ver=$(grep '^version' Cargo.toml | sed -E -e 's/.*\"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/') && 53 | test -n "$ver" && 54 | git checkout gh-pages && 55 | cp -R target/doc doc/v$ver && 56 | git add doc/v$ver 57 | 58 | 1. Review `doc/v$ver/couchdb/index.html`. 59 | 60 | 1. Publish. 61 | 62 | $ test -n "$ver" && 63 | git commit -m "Add v$ver documentation" && 64 | git push origin && 65 | git checkout release_prep 66 | 67 | 1. Merge updates into master. 68 | 69 | $ git checkout master && 70 | git merge release_prep && 71 | git branch -d release_prep 72 | 73 | 1. Publish the crate. 74 | 75 | $ cargo publish 76 | 77 | 1. Create Git tag. 78 | 79 | $ test -n "$ver" && 80 | git tag -a v$ver -m "Release of v$ver" && 81 | git push --tags 82 | 83 | 1. Prep for new work. 84 | 85 | 1. Edit `Cargo.toml` to increment the version, adding the `-master` 86 | suffix. 87 | 88 | 1. Edit `CHANGELOG.md` to add the new “unreleased” section for the 89 | next version. 90 | 91 | 1. Commit changes 92 | 93 | $ git commit 94 | 95 | 1. Close the issue milestone for the new release: 96 | https://github.com/couchdb-rs/couchdb/milestones. 97 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | newline_style = "Unix" 3 | reorder_imports = true 4 | reorder_imported_names = true 5 | single_line_if_else_max_width = 80 6 | wrap_match_arms = false 7 | -------------------------------------------------------------------------------- /src/attachment.rs: -------------------------------------------------------------------------------- 1 | //! The `attachment` module provides types for working with CouchDB document 2 | //! attachments. 3 | 4 | use {Error, base64, serde, std}; 5 | use mime::Mime; 6 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 7 | use std::str::FromStr; 8 | 9 | /// `Attachment` is a state-aware representation of a CouchDB document 10 | /// attachment. 11 | /// 12 | /// # Summary 13 | /// 14 | /// * `Attachment` maintains state about whether it already exists on the server 15 | /// (i.e., _originates from the server_) or not (i.e., _originates from the 16 | /// client_). 17 | /// 18 | /// * A CouchDB attachment may be stubbed, meaning it has no content but instead 19 | /// is a placeholder for attachment content that already exists on the server. 20 | /// 21 | /// * An `Attachment` instance deserialized from JSON is server-originating. 22 | /// 23 | /// * An `Attachment` instance constructed from content (e.g., via the 24 | /// `Attachment::new` method) is client-originating. 25 | /// 26 | /// * When serialized to JSON, a server-originating `Attachment` instance emits 27 | /// a stub object—regardless whether the `Attachment` instance is a stub. 28 | /// 29 | /// * When serialized to JSON, a client-originating `Attachment` instance emits 30 | /// a non-stub object that uses base64-encoding to encapsulate its content. 31 | /// 32 | /// * `Attachment` supports conversion into a stub, which is useful when either: 33 | /// 34 | /// * Updating a document but not making changes to its existing 35 | /// attachments, or, 36 | /// * Uploading attachments via multipart-encoding. 37 | /// 38 | /// # Remarks 39 | /// 40 | /// CouchDB document attachments are versatile but tricky. Generally speaking, 41 | /// there are several things the application must get right: 42 | /// 43 | /// * When updating a document on the server, the client must send existing 44 | /// attachments—either stubbed or containing full content—otherwise the server 45 | /// will delete missing attachments as part of the document update. 46 | /// 47 | /// * When enclosing attachment content directly in JSON, the content must be 48 | /// base64-encoded. 49 | /// 50 | /// * To prevent sending redundant data to the server, the application 51 | /// serializes unmodified attachments as stubs (via `"stub": true` within the 52 | /// attachment object). 53 | /// 54 | /// * When using multipart-encoding in lieu of base64-encoding, the application 55 | /// serializes attachments into yet another form (via `"follows": true` within 56 | /// the attachment object). 57 | /// 58 | /// # TODO 59 | /// 60 | /// * Add a means for applications to construct server-originating attachments 61 | /// from multipart data. 62 | /// 63 | #[derive(Clone, Debug, Eq, PartialEq)] 64 | pub struct Attachment { 65 | content_type: Mime, 66 | inner: Inner, 67 | } 68 | 69 | #[derive(Clone, Debug, Eq, PartialEq)] 70 | enum Inner { 71 | ServerOrigin { 72 | content: Content, 73 | digest: Digest, 74 | encoding: Option, 75 | revpos: u64, 76 | }, 77 | ClientOrigin { content: Vec }, 78 | Follows { content_length: u64 }, 79 | } 80 | 81 | #[derive(Clone, Debug, Eq, PartialEq)] 82 | enum Content { 83 | WithBytes(Vec), 84 | WithLength(u64), 85 | } 86 | 87 | /// `Digest` is a hashed sum of an attachment's content. 88 | #[derive(Clone, Debug, Eq, PartialEq)] 89 | pub enum Digest { 90 | #[doc(hidden)] 91 | Md5 { value: Vec }, 92 | #[doc(hidden)] 93 | Other { name: String, value: Vec }, 94 | } 95 | 96 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 97 | enum EncodingCodec { 98 | Gzip, 99 | Other(String), 100 | } 101 | 102 | /// `Encoding` contains information about the compression the CouchDB server 103 | /// uses to store an attachment's content. 104 | #[derive(Clone, Debug, Eq, PartialEq)] 105 | pub struct Encoding { 106 | length: u64, 107 | codec: EncodingCodec, 108 | } 109 | 110 | impl Attachment { 111 | /// Constructs a new attachment. 112 | /// 113 | /// The newly constructed `Attachment` is internally marked as having 114 | /// originated from the client and therefore, when serialized as JSON, will 115 | /// include all content as a base64-encoded string (as opposed to being 116 | /// stubbed out). This may incur significant overhead when sent to the 117 | /// CouchDB server within an enclosed document because base64-encoding uses 118 | /// four encoded bytes to represent every three encoded bytes. 119 | /// 120 | /// One way to reduce base64 overhead is to stub the attachment and instead 121 | /// use multipart-encoding when uploading the document. See the [CouchDB 122 | /// documentation](http://docs.couchdb.org/en/2.0.0/api/document/common.html#attachments) 123 | /// for details. 124 | /// 125 | pub fn new(content_type: Mime, content: Vec) -> Self { 126 | Attachment { 127 | content_type: content_type, 128 | inner: Inner::ClientOrigin { content: content }, 129 | } 130 | } 131 | 132 | /// Returns whether the attachment originates from the server. 133 | pub fn is_server_origin(&self) -> bool { 134 | match self.inner { 135 | Inner::ServerOrigin { .. } => true, 136 | Inner::ClientOrigin { .. } => false, 137 | Inner::Follows { .. } => false, 138 | } 139 | } 140 | 141 | /// Returns whether the attachment originates from the client. 142 | pub fn is_client_origin(&self) -> bool { 143 | match self.inner { 144 | Inner::ServerOrigin { .. } => false, 145 | Inner::ClientOrigin { .. } => true, 146 | Inner::Follows { .. } => false, 147 | } 148 | } 149 | 150 | /// Borrows the attachment's content MIME type. 151 | pub fn content_type(&self) -> &Mime { 152 | &self.content_type 153 | } 154 | 155 | /// Borrows the attachment's content, if available. 156 | /// 157 | /// Content is available if and only if: 158 | /// 159 | /// * The attachment originates from the client, or, 160 | /// * The attachment originates from the server and is not a stub. 161 | /// 162 | pub fn content(&self) -> Option<&[u8]> { 163 | match self.inner { 164 | Inner::ServerOrigin { content: Content::WithBytes(ref bytes), .. } => Some(bytes), 165 | Inner::ServerOrigin { content: Content::WithLength(_), .. } => None, 166 | Inner::ClientOrigin { ref content } => Some(content), 167 | Inner::Follows { .. } => None, 168 | } 169 | } 170 | 171 | /// Returns the size of the attachment's content, in bytes. 172 | pub fn content_length(&self) -> u64 { 173 | match self.inner { 174 | Inner::ServerOrigin { content: Content::WithBytes(ref bytes), .. } => bytes.len() as u64, 175 | Inner::ServerOrigin { content: Content::WithLength(length), .. } => length, 176 | Inner::ClientOrigin { ref content } => content.len() as u64, 177 | Inner::Follows { content_length } => content_length, 178 | } 179 | } 180 | 181 | /// Constructs a stubbed copy of the attachment. 182 | /// 183 | /// A stubbed attachment contains no content, instead marking itself as a 184 | /// stub and relying on the CouchDB server to already have the content if 185 | /// the attachment is sent to the server within its enclosing document. 186 | /// 187 | /// Hence, only an attachment that originates from the server can be 188 | /// stubbed. Otherwise, content would be lost, which this method prevents by 189 | /// instead returning `None` if the attachment originates from the client. 190 | /// 191 | /// **Note:** The stub retains all other information about the attachment, 192 | /// such as content type and digest. 193 | /// 194 | pub fn to_stub(&self) -> Option { 195 | match self.inner { 196 | Inner::ServerOrigin { 197 | ref content, 198 | ref digest, 199 | ref encoding, 200 | ref revpos, 201 | } => { 202 | Some(Attachment { 203 | content_type: self.content_type.clone(), 204 | inner: Inner::ServerOrigin { 205 | content: content.to_length_only(), 206 | digest: digest.clone(), 207 | encoding: encoding.clone(), 208 | revpos: revpos.clone(), 209 | }, 210 | }) 211 | } 212 | _ => None, 213 | } 214 | } 215 | 216 | /// Constructs a stubbed copy of the attachment suitable for 217 | /// multipart-encoding. 218 | /// 219 | /// The returned attachment loses all information about the attachment 220 | /// except for its content type and content length. The intention is for the 221 | /// application to: 222 | /// 223 | /// 1. Serialize the attachment stub within an enclosed document, as JSON, 224 | /// and, 225 | /// 226 | /// 2. Send the attachment content as multipart data, within the same HTTP 227 | /// request. 228 | /// 229 | /// See the [CouchDB 230 | /// documentation](http://docs.couchdb.org/en/2.0.0/api/document/common.html#creating-multiple-attachments) 231 | /// for details. 232 | /// 233 | /// # Example 234 | /// 235 | /// ```rust 236 | /// extern crate couchdb; 237 | /// extern crate mime; 238 | /// extern crate serde_json; 239 | /// 240 | /// let att = couchdb::Attachment::new( 241 | /// mime::TEXT_PLAIN, 242 | /// Vec::from(b"Lorem ipsum dolor sit amet".as_ref()) 243 | /// ).to_multipart_stub(); 244 | /// 245 | /// let encoded = serde_json::to_vec(&att).unwrap(); 246 | /// 247 | /// # let decoded = serde_json::from_slice::(&encoded) 248 | /// # .unwrap(); 249 | /// # 250 | /// # let expected = serde_json::Value::Object( 251 | /// # vec![ 252 | /// # (String::from("content_type"), serde_json::Value::String(String::from("text/plain"))), 253 | /// # (String::from("follows"), serde_json::Value::Bool(true)), 254 | /// # (String::from("length"), serde_json::Value::Number(serde_json::Number::from(26))), 255 | /// # ].into_iter().collect::>() 256 | /// # ); 257 | /// # 258 | /// # assert_eq!(decoded, expected); 259 | /// # 260 | /// // encoded: 261 | /// // 262 | /// // { 263 | /// // "content_type": "text/plain", 264 | /// // "follows": true, 265 | /// // "length": 26 266 | /// // } 267 | /// ``` 268 | /// 269 | pub fn to_multipart_stub(&self) -> Attachment { 270 | Attachment { 271 | content_type: self.content_type.clone(), 272 | inner: Inner::Follows { content_length: self.content_length() }, 273 | } 274 | } 275 | 276 | /// Borrows the attachment's digest, if available. 277 | /// 278 | /// An attachment's digest is available if and only if it originates from 279 | /// the server. 280 | /// 281 | pub fn digest(&self) -> Option<&Digest> { 282 | match self.inner { 283 | Inner::ServerOrigin { ref digest, .. } => Some(digest), 284 | Inner::ClientOrigin { .. } => None, 285 | Inner::Follows { .. } => None, 286 | } 287 | } 288 | 289 | /// Returns the attachment's encoding information, if available. 290 | pub fn encoding(&self) -> Option<&Encoding> { 291 | match self.inner { 292 | Inner::ServerOrigin { ref encoding, .. } => encoding.as_ref().clone(), 293 | Inner::ClientOrigin { .. } => None, 294 | Inner::Follows { .. } => None, 295 | } 296 | } 297 | 298 | /// Returns the attachment's revision sequence number—i.e., the `revpos` 299 | /// attachment field. 300 | pub fn revision_sequence(&self) -> Option { 301 | match self.inner { 302 | Inner::ServerOrigin { revpos, .. } => Some(revpos), 303 | Inner::ClientOrigin { .. } => None, 304 | Inner::Follows { .. } => None, 305 | } 306 | } 307 | } 308 | 309 | impl<'a> Deserialize<'a> for Attachment { 310 | fn deserialize(deserializer: D) -> Result 311 | where 312 | D: Deserializer<'a>, 313 | { 314 | #[derive(Deserialize)] 315 | struct T { 316 | content_type: SerializableMime, 317 | data: Option, 318 | digest: Digest, 319 | encoded_length: Option, 320 | encoding: Option, 321 | length: Option, 322 | revpos: u64, 323 | // stub: Option, // unused 324 | } 325 | 326 | let x = T::deserialize(deserializer)?; 327 | 328 | let encoding = match (x.encoding, x.encoded_length) { 329 | (Some(codec), Some(length)) => Some(Encoding { 330 | codec: EncodingCodec::from(codec), 331 | length: length, 332 | }), 333 | (None, None) => None, 334 | _ => return Err(serde::de::Error::invalid_value( 335 | serde::de::Unexpected::Map, 336 | &Expectation( 337 | "a JSON object with complete CouchDB attachment encoding info OR no such info", 338 | ), 339 | )), 340 | }; 341 | 342 | let inner = if let Some(SerializableBase64(bytes)) = x.data { 343 | Inner::ServerOrigin { 344 | content: Content::WithBytes(bytes), 345 | digest: x.digest, 346 | encoding: encoding, 347 | revpos: x.revpos, 348 | } 349 | } else if let Some(content_length) = x.length { 350 | Inner::ServerOrigin { 351 | content: Content::WithLength(content_length), 352 | digest: x.digest, 353 | encoding: encoding, 354 | revpos: x.revpos, 355 | } 356 | } else { 357 | return Err(serde::de::Error::invalid_value( 358 | serde::de::Unexpected::Map, 359 | &Expectation( 360 | "a JSON object with CouchDB attachment content OR content length", 361 | ), 362 | )); 363 | }; 364 | 365 | Ok(Attachment { 366 | content_type: x.content_type.0, 367 | inner: inner, 368 | }) 369 | } 370 | } 371 | 372 | impl Serialize for Attachment { 373 | fn serialize(&self, serializer: S) -> Result 374 | where 375 | S: Serializer, 376 | { 377 | #[derive(Debug, Default, Deserialize, Serialize)] 378 | struct T { 379 | content_type: String, 380 | #[serde(skip_serializing_if = "Option::is_none")] 381 | data: Option, 382 | #[serde(skip_serializing_if = "Option::is_none")] 383 | stub: Option, 384 | #[serde(skip_serializing_if = "Option::is_none")] 385 | follows: Option, 386 | #[serde(skip_serializing_if = "Option::is_none")] 387 | length: Option, 388 | } 389 | 390 | let mut x = T::default(); 391 | x.content_type = self.content_type.to_string(); 392 | 393 | match self.inner { 394 | Inner::ServerOrigin { .. } => { 395 | x.stub = Some(true); 396 | } 397 | Inner::ClientOrigin { ref content } => { 398 | x.data = Some(base64::encode(content)); 399 | } 400 | Inner::Follows { content_length } => { 401 | x.follows = Some(true); 402 | x.length = Some(content_length); 403 | } 404 | }; 405 | 406 | x.serialize(serializer) 407 | } 408 | } 409 | 410 | struct Expectation(&'static str); 411 | 412 | impl serde::de::Expected for Expectation { 413 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 414 | f.write_str(self.0) 415 | } 416 | } 417 | 418 | struct SerializableBase64(Vec); 419 | 420 | impl<'a> Deserialize<'a> for SerializableBase64 { 421 | fn deserialize(deserializer: D) -> Result 422 | where 423 | D: Deserializer<'a>, 424 | { 425 | let s = String::deserialize(deserializer)?; 426 | 427 | let v = base64::decode(&s).map_err(|_| { 428 | serde::de::Error::invalid_value( 429 | serde::de::Unexpected::Str(&s), 430 | &Expectation("a base64-encoded string containing CouchDB attachment data"), 431 | ) 432 | })?; 433 | 434 | Ok(SerializableBase64(v)) 435 | } 436 | } 437 | 438 | impl<'a> Deserialize<'a> for Digest { 439 | fn deserialize(deserializer: D) -> Result 440 | where 441 | D: Deserializer<'a>, 442 | { 443 | let s = String::deserialize(deserializer)?; 444 | 445 | let v = Digest::from_str(&s).map_err(|_| { 446 | serde::de::Error::invalid_value( 447 | serde::de::Unexpected::Str(&s), 448 | &Expectation("a CouchDB attachment digest"), 449 | ) 450 | })?; 451 | 452 | Ok(v) 453 | } 454 | } 455 | 456 | impl Content { 457 | /// Returns a length-only copy of the content. 458 | pub fn to_length_only(&self) -> Content { 459 | match *self { 460 | Content::WithBytes(ref bytes) => Content::WithLength(bytes.len() as u64), 461 | Content::WithLength(length) => Content::WithLength(length), 462 | } 463 | } 464 | } 465 | 466 | impl Digest { 467 | /// Borrows the encoded digest value. 468 | /// 469 | /// For example, with an MD5 digest, the value is the 16-byte MD5 sum of the 470 | /// attachment's content. 471 | /// 472 | pub fn bytes(&self) -> &[u8] { 473 | match *self { 474 | Digest::Md5 { ref value } => value, 475 | Digest::Other { ref value, .. } => value, 476 | } 477 | } 478 | 479 | /// Returns whether the digest is MD5-encoded. 480 | pub fn is_md5(&self) -> bool { 481 | match *self { 482 | Digest::Md5 { .. } => true, 483 | _ => false, 484 | } 485 | } 486 | } 487 | 488 | impl FromStr for Digest { 489 | type Err = Error; 490 | fn from_str(s: &str) -> Result { 491 | 492 | let mut iter = s.splitn(2, '-'); 493 | let name = iter.next().unwrap(); 494 | let value = iter.next().ok_or(Error::BadDigest)?; 495 | let value = base64::decode(&value).map_err(|_| Error::BadDigest)?; 496 | 497 | Ok(match name { 498 | "md5" => Digest::Md5 { value: value }, 499 | _ => Digest::Other { 500 | name: String::from(name), 501 | value: value, 502 | }, 503 | }) 504 | } 505 | } 506 | 507 | impl Encoding { 508 | /// Returns the size of the attachment's compressed content, in bytes. 509 | pub fn length(&self) -> u64 { 510 | self.length 511 | } 512 | 513 | /// Returns whether the compression codec is gzip. 514 | pub fn is_gzip(&self) -> bool { 515 | self.codec == EncodingCodec::Gzip 516 | } 517 | } 518 | 519 | impl From for EncodingCodec { 520 | fn from(s: String) -> Self { 521 | match s.as_str() { 522 | "gzip" => EncodingCodec::Gzip, 523 | _ => EncodingCodec::Other(s), 524 | } 525 | } 526 | } 527 | 528 | struct SerializableMime(Mime); 529 | 530 | impl<'a> Deserialize<'a> for SerializableMime { 531 | fn deserialize(deserializer: D) -> Result 532 | where 533 | D: Deserializer<'a>, 534 | { 535 | let s = String::deserialize(deserializer)?; 536 | 537 | let v = Mime::from_str(&s).map_err(|_| { 538 | serde::de::Error::invalid_value( 539 | serde::de::Unexpected::Str(&s), 540 | &Expectation("a string specifying a MIME type"), 541 | ) 542 | })?; 543 | 544 | Ok(SerializableMime(v)) 545 | } 546 | } 547 | 548 | #[cfg(test)] 549 | mod tests { 550 | use super::*; 551 | use {mime, serde_json}; 552 | 553 | #[test] 554 | fn attachment_deserializes_as_stub() { 555 | 556 | let source = r#"{ 557 | "content_type": "text/plain", 558 | "digest": "md5-Ids41vtv725jyrN7iUvMcQ==", 559 | "length": 1872, 560 | "revpos": 4, 561 | "stub": true 562 | }"#; 563 | 564 | let expected = Attachment { 565 | content_type: mime::TEXT_PLAIN, 566 | inner: Inner::ServerOrigin { 567 | content: Content::WithLength(1872), 568 | digest: Digest::Md5 { 569 | value: Vec::from( 570 | b"\x21\xdb\x38\xd6\ 571 | \xfb\x6f\xef\x6e\ 572 | \x63\xca\xb3\x7b\ 573 | \x89\x4b\xcc\x71" 574 | .as_ref(), 575 | ), 576 | }, 577 | encoding: None, 578 | revpos: 4, 579 | }, 580 | }; 581 | 582 | let got: Attachment = serde_json::from_str(source).unwrap(); 583 | assert_eq!(got, expected); 584 | } 585 | 586 | #[test] 587 | fn attachment_deserializes_with_data() { 588 | 589 | let source = r#"{ 590 | "content_type": "image/gif", 591 | "data": "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", 592 | "digest": "md5-2JdGiI2i2VELZKnwMers1Q==", 593 | "revpos": 2 594 | }"#; 595 | 596 | let expected = Attachment { 597 | content_type: mime::IMAGE_GIF, 598 | inner: Inner::ServerOrigin { 599 | content: Content::WithBytes(Vec::from( 600 | b"\x47\x49\x46\x38\ 601 | \x39\x61\x01\x00\ 602 | \x01\x00\x80\x00\ 603 | \x00\x00\x00\x00\ 604 | \xff\xff\xff\x21\ 605 | \xf9\x04\x01\x00\ 606 | \x00\x00\x00\x2c\ 607 | \x00\x00\x00\x00\ 608 | \x01\x00\x01\x00\ 609 | \x00\x02\x01\x44\ 610 | \x00\x3b" 611 | .as_ref(), 612 | )), 613 | digest: Digest::Md5 { 614 | value: Vec::from( 615 | b"\xd8\x97\x46\x88\ 616 | \x8d\xa2\xd9\x51\ 617 | \x0b\x64\xa9\xf0\ 618 | \x31\xea\xec\xd5" 619 | .as_ref(), 620 | ), 621 | }, 622 | encoding: None, 623 | revpos: 2, 624 | }, 625 | }; 626 | 627 | let got: Attachment = serde_json::from_str(source).unwrap(); 628 | assert_eq!(got, expected); 629 | } 630 | 631 | #[test] 632 | fn attachment_deserializes_with_encoding_info() { 633 | 634 | let source = r#"{ 635 | "content_type": "text/plain", 636 | "digest": "md5-Ids41vtv725jyrN7iUvMcQ==", 637 | "encoded_length": 693, 638 | "encoding": "gzip", 639 | "length": 1872, 640 | "revpos": 4, 641 | "stub": true 642 | }"#; 643 | 644 | let expected = Attachment { 645 | content_type: mime::TEXT_PLAIN, 646 | inner: Inner::ServerOrigin { 647 | content: Content::WithLength(1872), 648 | digest: Digest::Md5 { 649 | value: Vec::from( 650 | b"\x21\xdb\x38\xd6\ 651 | \xfb\x6f\xef\x6e\ 652 | \x63\xca\xb3\x7b\ 653 | \x89\x4b\xcc\x71" 654 | .as_ref(), 655 | ), 656 | }, 657 | encoding: Some(Encoding { 658 | length: 693, 659 | codec: EncodingCodec::Gzip, 660 | }), 661 | revpos: 4, 662 | }, 663 | }; 664 | 665 | let got: Attachment = serde_json::from_str(source).unwrap(); 666 | assert_eq!(got, expected); 667 | } 668 | 669 | #[test] 670 | fn client_origin_attachment_serializes_with_content() { 671 | 672 | let source = Attachment::new( 673 | mime::TEXT_PLAIN, 674 | Vec::from(b"Lorem ipsum dolor sit amet".as_ref()), 675 | ); 676 | 677 | let encoded = serde_json::to_vec(&source).unwrap(); 678 | let decoded: serde_json::Value = serde_json::from_slice(&encoded).unwrap(); 679 | 680 | let expected = json!({ 681 | "content_type": "text/plain", 682 | "data": "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ=", 683 | }); 684 | 685 | assert_eq!(decoded, expected); 686 | } 687 | 688 | #[test] 689 | fn server_origin_attachment_serializes_as_stub() { 690 | 691 | let source = Attachment { 692 | content_type: mime::TEXT_PLAIN, 693 | inner: Inner::ServerOrigin { 694 | content: Content::WithLength(1872), 695 | digest: Digest::Md5 { 696 | value: Vec::from( 697 | b"\x21\xdb\x38\xd6\ 698 | \xfb\x6f\xef\x6e\ 699 | \x63\xca\xb3\x7b\ 700 | \x89\x4b\xcc\x71" 701 | .as_ref(), 702 | ), 703 | }, 704 | encoding: Some(Encoding { 705 | length: 693, 706 | codec: EncodingCodec::Gzip, 707 | }), 708 | revpos: 4, 709 | }, 710 | }; 711 | 712 | let encoded = serde_json::to_vec(&source).unwrap(); 713 | let decoded: serde_json::Value = serde_json::from_slice(&encoded).unwrap(); 714 | 715 | let expected = json!({ 716 | "content_type": "text/plain", 717 | "stub": true, 718 | }); 719 | 720 | assert_eq!(decoded, expected); 721 | } 722 | } 723 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use {DatabaseName, serde, std}; 2 | use serde::Deserializer; 3 | use std::marker::PhantomData; 4 | 5 | /// `Database` contains the content of a database resource. 6 | /// 7 | /// # Summary 8 | /// 9 | /// * `Database` has public members instead of accessor methods because there 10 | /// are no invariants restricting the data. 11 | /// 12 | /// * `Database` implements `Deserialize`. 13 | /// 14 | /// # Remarks 15 | /// 16 | /// An application may obtain a database resource by sending an HTTP request to 17 | /// GET `/{db}`. 18 | /// 19 | /// # Compatibility 20 | /// 21 | /// `Database` contains a dummy private member in order to prevent applications 22 | /// from directly constructing a `Database` instance. This allows new fields to 23 | /// be added to `Database` in future releases without it being a breaking 24 | /// change. 25 | /// 26 | #[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Deserialize)] 27 | pub struct Database { 28 | pub committed_update_seq: u64, 29 | pub compact_running: bool, 30 | pub db_name: DatabaseName, 31 | pub disk_format_version: i32, 32 | pub data_size: u64, 33 | pub disk_size: u64, 34 | pub doc_count: u64, 35 | pub doc_del_count: u64, 36 | 37 | #[serde(deserialize_with = "deserialize_instance_start_time")] 38 | pub instance_start_time: u64, 39 | 40 | pub purge_seq: u64, 41 | pub update_seq: u64, 42 | 43 | #[serde(default = "PhantomData::default")] 44 | _private_guard: PhantomData<()>, 45 | } 46 | 47 | fn deserialize_instance_start_time<'a, D: Deserializer<'a>>(deserializer: D) -> Result { 48 | 49 | struct Visitor; 50 | 51 | impl<'b> serde::de::Visitor<'b> for Visitor { 52 | type Value = u64; 53 | fn expecting(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 54 | f.write_str("a string specifying the CouchDB start time") 55 | } 56 | 57 | fn visit_str(self, s: &str) -> Result { 58 | u64::from_str_radix(s, 10).map_err(|_| E::invalid_value(serde::de::Unexpected::Str(s), &self)) 59 | } 60 | } 61 | 62 | deserializer.deserialize_str(Visitor) 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | use serde_json; 69 | use std::marker::PhantomData; 70 | 71 | #[test] 72 | fn database_deserializes_ok() { 73 | 74 | let source = r#"{ 75 | "committed_update_seq": 292786, 76 | "compact_running": false, 77 | "data_size": 65031503, 78 | "db_name": "receipts", 79 | "disk_format_version": 6, 80 | "disk_size": 137433211, 81 | "doc_count": 6146, 82 | "doc_del_count": 64637, 83 | "instance_start_time": "1376269325408900", 84 | "purge_seq": 0, 85 | "update_seq": 292786 86 | }"#; 87 | 88 | let expected = Database { 89 | committed_update_seq: 292786, 90 | compact_running: false, 91 | data_size: 65031503, 92 | db_name: DatabaseName::from("receipts"), 93 | disk_format_version: 6, 94 | disk_size: 137433211, 95 | doc_count: 6146, 96 | doc_del_count: 64637, 97 | instance_start_time: 1376269325408900, 98 | purge_seq: 0, 99 | update_seq: 292786, 100 | _private_guard: PhantomData, 101 | }; 102 | 103 | let got: Database = serde_json::from_str(source).unwrap(); 104 | assert_eq!(got, expected); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::borrow::Cow; 3 | 4 | /// `Error` is the principal type of the `couchdb` crate. 5 | #[derive(Debug)] 6 | pub enum Error { 7 | BadDesignDocumentId, 8 | 9 | #[doc(hidden)] 10 | BadDigest, 11 | 12 | #[doc(hidden)] 13 | BadPath { what: &'static str }, 14 | 15 | BadRevision, 16 | 17 | #[doc(hidden)] 18 | Io { 19 | what: Cow<'static, str>, 20 | cause: std::io::Error, 21 | }, 22 | } 23 | 24 | impl Error { 25 | #[doc(hidden)] 26 | pub fn bad_path(what: &'static str) -> Self { 27 | Error::BadPath { what: what } 28 | } 29 | } 30 | 31 | impl std::fmt::Display for Error { 32 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 33 | let d = std::error::Error::description(self); 34 | match *self { 35 | Error::BadPath { what } => write!(f, "{}: {}", d, what), 36 | Error::Io { ref cause, .. } => write!(f, "{}: {}", d, cause), 37 | _ => f.write_str(d), 38 | } 39 | } 40 | } 41 | 42 | impl std::error::Error for Error { 43 | fn description(&self) -> &str { 44 | match *self { 45 | Error::BadDesignDocumentId => "The string is not a valid CouchDB design document id", 46 | Error::BadDigest => "The string is not a valid CouchDB attachment digest", 47 | Error::BadPath { .. } => "The CouchDB path is not valid", 48 | Error::BadRevision => "The string is not a valid CouchDB document revision", 49 | Error::Io { ref what, .. } => what.as_ref(), 50 | } 51 | } 52 | 53 | fn cause(&self) -> Option<&std::error::Error> { 54 | match *self { 55 | Error::Io { ref cause, .. } => Some(cause), 56 | _ => None, 57 | } 58 | } 59 | } 60 | 61 | impl>> From<(T, std::io::Error)> for Error { 62 | fn from((what, cause): (T, std::io::Error)) -> Self { 63 | Error::Io { 64 | what: what.into(), 65 | cause: cause, 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The `couchdb` library provides types for working with CouchDB. 2 | //! 3 | //! # Summary 4 | //! 5 | //! * The `couchdb` library is not a CouchDB client. Rather, it makes it easier 6 | //! for applications to communicate with a CouchDB server using existing HTTP 7 | //! client libraries (such as [hyper](https://crates.io/crates/hyper) and 8 | //! [reqwest](https://crates.io/crates/reqwest)). 9 | //! 10 | //! * The `couchdb` library is a toolkit, not a framework. Applications may opt 11 | //! in to using as much or as little of the library as makes the most sense. 12 | //! 13 | //! # Prerequisites 14 | //! 15 | //! * The application programmer is familiar with CouchDB and its API. 16 | //! 17 | //! Though the `couchdb` library aims to be easy to use, it does not aim to 18 | //! teach programmers about CouchDB or how to use the CouchDB API. For more 19 | //! information about CouchDB, consult its 20 | //! [documentation](http://docs.couchdb.org/en/2.0.0/index.html#). 21 | //! 22 | //! # Remarks 23 | //! 24 | //! The CouchDB API, like most HTTP interfaces, uses a lot of stringly types and 25 | //! requires client applications to do a lot of text-parsing and 26 | //! text-formatting. The `couchdb` library makes working with these stringly 27 | //! types easier. 28 | //! 29 | //! In earlier versions, the `couchdb` library provided a fledgling CouchDB 30 | //! client for communicating with a CouchDB server, but now the library is 31 | //! purely a passive collection of types, as well as testing tools, that's 32 | //! intended to be used in conjunction with other HTTP libraries, such as 33 | //! [hyper](https://crates.io/crates/hyper) or 34 | //! [reqwest](https://crates.io/crates/reqwest). 35 | 36 | extern crate base64; 37 | extern crate mime; 38 | extern crate regex; 39 | extern crate serde; 40 | #[macro_use] 41 | extern crate serde_derive; 42 | #[cfg(test)] 43 | #[macro_use] 44 | extern crate serde_json; 45 | extern crate tempdir; 46 | extern crate url; 47 | extern crate uuid; 48 | 49 | pub mod attachment; 50 | pub mod path; 51 | pub mod testing; 52 | 53 | mod database; 54 | mod error; 55 | mod nok; 56 | mod revision; 57 | mod root; 58 | 59 | pub use attachment::Attachment; 60 | pub use database::Database; 61 | pub use error::Error; 62 | pub use nok::Nok; 63 | pub use path::*; 64 | pub use revision::Revision; 65 | pub use root::{Root, Vendor, Version}; 66 | -------------------------------------------------------------------------------- /src/nok.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | /// `Nok` contains the content of an error response from the CouchDB server. 4 | /// 5 | /// # Summary 6 | /// 7 | /// * `Nok` has public members instead of accessor methods because there are no 8 | /// invariants restricting the data. 9 | /// 10 | /// * `Nok` implements `Deserialize`. 11 | /// 12 | /// # Remarks 13 | /// 14 | /// When the CouchDB server responds with a 4xx- or 5xx status code, the 15 | /// response usually has a body containing a JSON object with an “error” string 16 | /// and a “reason” string. For example: 17 | /// 18 | /// ```text 19 | /// { 20 | /// "error": "file_exists", 21 | /// "reason": "The database could not be created, the file already exists." 22 | /// } 23 | /// ``` 24 | /// 25 | /// The `Nok` type contains the information from the response body. 26 | /// 27 | /// ``` 28 | /// extern crate couchdb; 29 | /// extern crate serde_json; 30 | /// 31 | /// # let body = br#"{ 32 | /// # "error": "file_exists", 33 | /// # "reason": "The database could not be created, the file already exists." 34 | /// # }"#; 35 | /// # 36 | /// let nok: couchdb::Nok = serde_json::from_slice(body).unwrap(); 37 | /// 38 | /// assert_eq!(nok.error, "file_exists"); 39 | /// assert_eq!(nok.reason, 40 | /// "The database could not be created, the file already exists."); 41 | /// ``` 42 | /// 43 | /// # Compatibility 44 | /// 45 | /// `Nok` contains a dummy private member in order to prevent applications from 46 | /// directly constructing a `Nok` instance. This allows new fields to be added 47 | /// to `Nok` in future releases without it being a breaking change. 48 | /// 49 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)] 50 | pub struct Nok { 51 | pub error: String, 52 | pub reason: String, 53 | 54 | #[serde(default = "PhantomData::default")] 55 | _private_guard: PhantomData<()>, 56 | } 57 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | //! The `path` module provides types for identifying databases, documents, etc. 2 | //! 3 | //! # Summary 4 | //! 5 | //! * The `path` module provides a suite of types for names, ids, and paths—all 6 | //! of which are used to specify the location of CouchDB resources, such as 7 | //! databases, documents, and views. 8 | //! 9 | //! * Both **names** and **ids** are percent-decoded, but whereas a name 10 | //! comprises exactly one path segment, an id may comprise more. 11 | //! 12 | //! * Both names and ids are useful for constructing query parameters, headers, 13 | //! and capturing CouchDB response data. 14 | //! 15 | //! * A **path** is a full percent-encoded URL path and is most useful when 16 | //! constructing a URL for an HTTP request. It implements neither `Serialize` 17 | //! nor `Deserialize`. 18 | //! 19 | //! # Remarks 20 | //! 21 | //! The CouchDB API uses strings for specifying the locations of resources, and 22 | //! using these strings “in the raw” can be error-prone. The `path` module 23 | //! provides safer alternatives by way of stronger types. 24 | //! 25 | //! There are two chief kinds of errors that stronger types help eliminate: 26 | //! 27 | //! * **Incorrect percent-encoding.** For example, document names and attachment 28 | //! names may contain slashes (`/`) and other non-standard characters, and 29 | //! neglect to percent-encode these characters can cause obscure bugs. 30 | //! 31 | //! * **Type mismatches.** For example, mistakenly using a database path to 32 | //! delete a document could cause massive data loss. 33 | //! 34 | //! Note, however, that the `path` module merely makes these types available. It 35 | //! is up to the application programmer to make use of them. 36 | //! 37 | //! # Example 38 | //! 39 | //! ```rust 40 | //! extern crate couchdb; 41 | //! 42 | //! // Construct view path: '/alpha/_design/bravo/_view/charlie delta': 43 | //! 44 | //! let view_path = couchdb::DatabaseName::new("alpha") 45 | //! .with_design_document_id(couchdb::DesignDocumentName::new("bravo")) 46 | //! .with_view_name("charlie delta"); 47 | //! 48 | //! // Paths are percent-encoded and thus well suited for building URLs. 49 | //! 50 | //! let s = view_path.to_string(); 51 | //! assert_eq!(s, "/alpha/_design/bravo/_view/charlie%20delta"); 52 | //! 53 | //! // Alternatively, paths can be parsed from string. 54 | //! 55 | //! let v2 = couchdb::ViewPath::parse(&s).unwrap(); 56 | //! assert_eq!(view_path, v2); 57 | //! ``` 58 | 59 | use {Error, serde, std}; 60 | use serde::Deserialize; 61 | use std::borrow::Cow; 62 | use std::fmt::Display; 63 | use std::str::FromStr; 64 | 65 | const DESIGN_PREFIX: &str = "_design"; 66 | const LOCAL_PREFIX: &str = "_local"; 67 | const VIEW_PREFIX: &str = "_view"; 68 | 69 | static DOCUMENT_PREFIXES: &[&str] = &[DESIGN_PREFIX, LOCAL_PREFIX]; 70 | 71 | trait PathEncodable { 72 | fn encode_path_to(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error>; 73 | } 74 | 75 | fn percent_encode_segment(segment: &str, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 76 | use url::percent_encoding; 77 | f.write_str("/")?; 78 | percent_encoding::percent_encode( 79 | segment.as_bytes(), 80 | percent_encoding::PATH_SEGMENT_ENCODE_SET, 81 | ).fmt(f) 82 | } 83 | 84 | fn percent_decode<'a>(x: &'a str) -> Result, Error> { 85 | use url::percent_encoding; 86 | percent_encoding::percent_decode(x.as_bytes()) 87 | .decode_utf8() 88 | .map_err(|_| { 89 | Error::bad_path("Path is invalid UTF-8 after percent-decoding") 90 | }) 91 | } 92 | 93 | // PathDecoder is a utility for parsing a path string into its constituent 94 | // segments while providing consistent error-reporting. 95 | // 96 | // E.g., the string "/alpha/bravo" may be decoded into two parts, "alpha" and 97 | // "bravo". 98 | // 99 | // E.g., the string "/alpha%20bravo" may be decoded into one parts, "alpha 100 | // bravo". 101 | // 102 | #[derive(Clone, Debug, PartialEq)] 103 | struct PathDecoder<'a> { 104 | cursor: &'a str, 105 | } 106 | 107 | trait PathDecodable { 108 | fn path_decode(s: String) -> Self; 109 | } 110 | 111 | impl> PathDecodable for T { 112 | fn path_decode(s: String) -> Self { 113 | Self::from(s) 114 | } 115 | } 116 | 117 | const E_EMPTY_SEGMENT: &str = "Path has an segment"; 118 | const E_NO_LEADING_SLASH: &str = "Path does not begin with a slash"; 119 | const E_TOO_FEW_SEGMENTS: &str = "Path has too few segments"; 120 | const E_TOO_MANY_SEGMENTS: &str = "Path has too many segments"; 121 | const E_TRAILING_SLASH: &str = "Path ends with a slash"; 122 | const E_UNEXPECTED_SEGMENT: &str = "Path contains unexpected segment"; 123 | 124 | impl<'a> PathDecoder<'a> { 125 | pub fn begin(cursor: &'a str) -> Result { 126 | 127 | if !cursor.starts_with('/') { 128 | return Err(Error::bad_path(E_NO_LEADING_SLASH)); 129 | } 130 | 131 | Ok(PathDecoder { cursor: cursor }) 132 | } 133 | 134 | pub fn end(self) -> Result<(), Error> { 135 | match self.cursor { 136 | "" => Ok(()), 137 | "/" => Err(Error::bad_path(E_TRAILING_SLASH)), 138 | _ => Err(Error::bad_path(E_TOO_MANY_SEGMENTS)), 139 | } 140 | } 141 | 142 | fn prep(&self) -> Result<&'a str, Error> { 143 | if self.cursor.is_empty() { 144 | return Err(Error::bad_path(E_TOO_FEW_SEGMENTS)); 145 | } 146 | 147 | debug_assert!(self.cursor.starts_with('/')); 148 | let after_slash = &self.cursor['/'.len_utf8()..]; 149 | 150 | if after_slash.is_empty() { 151 | return Err(Error::bad_path(E_TOO_FEW_SEGMENTS)); 152 | } 153 | 154 | Ok(after_slash) 155 | } 156 | 157 | pub fn decode_exact(&mut self, key: &str) -> Result<(), Error> { 158 | 159 | let p = self.prep()?; 160 | 161 | let slash = p.find('/').unwrap_or(p.len()); 162 | if slash == 0 { 163 | return Err(Error::bad_path(E_EMPTY_SEGMENT)); 164 | } 165 | 166 | if &p[..slash] != key { 167 | return Err(Error::bad_path(E_UNEXPECTED_SEGMENT)); 168 | } 169 | 170 | self.cursor = &p[slash..]; 171 | 172 | Ok(()) 173 | } 174 | 175 | pub fn decode_segment(&mut self) -> Result { 176 | 177 | // TODO: We could use From> instead of From to 178 | // eliminate a temporary memory allocation when no percent decoding 179 | // takes place. 180 | 181 | let p = self.prep()?; 182 | 183 | let slash = p.find('/').unwrap_or(p.len()); 184 | if slash == 0 { 185 | return Err(Error::bad_path(E_EMPTY_SEGMENT)); 186 | } 187 | 188 | let segment = percent_decode(&p[..slash])?; 189 | self.cursor = &p[slash..]; 190 | 191 | Ok(T::path_decode(segment.into_owned())) 192 | } 193 | 194 | pub fn decode_with_prefix(&mut self, prefix: &str) -> Result { 195 | // TODO: We could use From> instead of From to 196 | // eliminate a temporary memory allocation when no percent decoding 197 | // takes place. 198 | 199 | let p = self.prep()?; 200 | 201 | let slash = p.find('/').unwrap_or(p.len()); 202 | if slash + 1 >= p.len() { 203 | return Err(Error::bad_path(E_TOO_FEW_SEGMENTS)); 204 | } 205 | 206 | if &p[..slash] != prefix { 207 | return Err(Error::bad_path(E_UNEXPECTED_SEGMENT)); 208 | } 209 | 210 | let p = &p[slash + 1..]; 211 | 212 | let slash = p.find('/').unwrap_or(p.len()); 213 | if slash == 0 { 214 | return Err(Error::bad_path(E_EMPTY_SEGMENT)); 215 | } 216 | 217 | let segment = percent_decode(&p[..slash])?; 218 | self.cursor = &p[slash..]; 219 | 220 | Ok(T::path_decode(format!("{}/{}", prefix, segment))) 221 | } 222 | 223 | pub fn decode_with_optional_prefix(&mut self, prefixes: I) -> Result 224 | where 225 | I: IntoIterator, 226 | S: AsRef, 227 | T: PathDecodable, 228 | { 229 | // TODO: We could use From> instead of From to 230 | // eliminate a temporary memory allocation when no percent decoding 231 | // takes place. 232 | 233 | let p = self.prep()?; 234 | let slash = p.find('/').unwrap_or(p.len()); 235 | 236 | for prefix in prefixes.into_iter() { 237 | if &p[..slash] != prefix.as_ref() { 238 | continue; 239 | } 240 | 241 | if slash + 1 >= p.len() { 242 | return Err(Error::bad_path(E_TOO_FEW_SEGMENTS)); 243 | } 244 | 245 | let p = &p[slash + 1..]; 246 | 247 | let slash = p.find('/').unwrap_or(p.len()); 248 | if slash == 0 { 249 | return Err(Error::bad_path(E_EMPTY_SEGMENT)); 250 | } 251 | 252 | let segment = percent_decode(&p[..slash])?; 253 | self.cursor = &p[slash..]; 254 | 255 | return Ok(T::path_decode(format!("{}/{}", prefix.as_ref(), segment))); 256 | } 257 | 258 | self.decode_segment() 259 | } 260 | } 261 | 262 | macro_rules! define_name_type { 263 | ($type_name:ident, 264 | $param_name:ident, 265 | #[$doc_description:meta], 266 | #[$doc_content:meta]) => 267 | { 268 | #[$doc_content] 269 | /// 270 | /// For more information about path-related types, see the [module-level 271 | /// documentation](index.html). 272 | /// 273 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 274 | pub struct $type_name(String); 275 | 276 | impl $type_name { 277 | /// Constructs a new 278 | #[$doc_description] 279 | /// name. 280 | pub fn new>(s: T) -> Self { 281 | $type_name(s.into()) 282 | } 283 | 284 | /// Converts the 285 | #[$doc_description] 286 | /// name into a string. 287 | pub fn into_string(self) -> String { 288 | self.0 289 | } 290 | } 291 | 292 | impl PathEncodable for $type_name { 293 | fn encode_path_to(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 294 | percent_encode_segment(&self.0, f) 295 | } 296 | } 297 | 298 | impl AsRef for $type_name { 299 | fn as_ref(&self) -> &str { 300 | self.0.as_ref() 301 | } 302 | } 303 | 304 | impl From<$type_name> for String { 305 | fn from($param_name: $type_name) -> Self { 306 | String::from($param_name.0) 307 | } 308 | } 309 | 310 | impl Display for $type_name { 311 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 312 | self.0.fmt(f) 313 | } 314 | } 315 | 316 | impl<'a> From<&'a str> for $type_name { 317 | fn from(s: &'a str) -> Self { 318 | $type_name(String::from(s)) 319 | } 320 | } 321 | 322 | impl From for $type_name { 323 | fn from(s: String) -> Self { 324 | $type_name(s) 325 | } 326 | } 327 | 328 | }; 329 | } 330 | 331 | define_name_type!(DatabaseName, db_name, #[doc="database"], 332 | #[doc="`DatabaseName` is a single URL path segment that specifies the name of a 333 | database. 334 | 335 | For example, given the document path `/db/_design/doc`, the database name is 336 | `db`."]); 337 | 338 | impl DatabaseName { 339 | /// Joins the database name with a document id to construct a document path. 340 | pub fn with_document_id>(self, doc_id: T) -> DocumentPath { 341 | DocumentPath { 342 | db_name: self, 343 | doc_id: doc_id.into(), 344 | } 345 | } 346 | 347 | /// Joins the database name with a design document id to construct a design 348 | /// document path. 349 | pub fn with_design_document_id>(self, ddoc_id: T) -> DesignDocumentPath { 350 | DesignDocumentPath { 351 | db_name: self, 352 | ddoc_id: ddoc_id.into(), 353 | } 354 | } 355 | 356 | /// Converts the database name into a database path. 357 | pub fn into_database_path(self) -> DatabasePath { 358 | DatabasePath { db_name: self } 359 | } 360 | } 361 | 362 | define_name_type!(NormalDocumentName, doc_name, #[doc="normal document"], 363 | #[doc="`NormalDocumentName` is a single URL path segment that specifies the name 364 | of a document that is neither a design document nor a local document. 365 | 366 | For example, given the document path `/db/doc`, the document name is `doc`."]); 367 | 368 | define_name_type!(DesignDocumentName, ddoc_name, #[doc="design document"], 369 | #[doc="`DesignDocumentName` is a single URL path segment that specifies the name 370 | of a design document. 371 | 372 | For example, given the design document path `/db/_design/doc`, the document name 373 | is `doc`."]); 374 | 375 | define_name_type!(LocalDocumentName, ldoc_name, #[doc="local document"], 376 | #[doc="`LocalDocumentName` is a single URL path segment that specifies the name 377 | of a local document. 378 | 379 | For example, given the local document path `/db/_local/doc`, the document name 380 | is `doc`."]); 381 | 382 | define_name_type!(AttachmentName, att_name, #[doc="attachment"], 383 | #[doc="`AttachmentName` is a single URL path segment that specifies the name of 384 | an attachment. 385 | 386 | For example, given the attachment path `/db/doc/att`, the attachment name is 387 | `att`."]); 388 | 389 | define_name_type!(ViewName, view_name, #[doc="view"], #[doc="`ViewName` is a 390 | single URL path segment that specifies the name of a view. 391 | 392 | For example, given the view path `/db/_design/doc/_view/view`, the view name is 393 | `view`."]); 394 | 395 | /// `DocumentId` comprises one or more URL path segments that, together, 396 | /// identify a document. 397 | /// 398 | /// For example, given the document path `/db/_design/doc`, the document id is 399 | /// `_design/doc`. 400 | /// 401 | /// For more information about path-related types, see the [module-level 402 | /// documentation](index.html). 403 | /// 404 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 405 | pub struct DocumentId(String); 406 | 407 | impl DocumentId { 408 | /// Constructs a new document id. 409 | pub fn new>(s: T) -> Self { 410 | DocumentId(s.into()) 411 | } 412 | 413 | /// Converts the document id into a string. 414 | pub fn into_string(self) -> String { 415 | self.0 416 | } 417 | 418 | /// Returns whether the document id specifies a normal document—i.e., 419 | /// neither a design document nor a local document. 420 | pub fn is_normal(&self) -> bool { 421 | self.split_prefix().0.is_none() 422 | } 423 | 424 | /// Tries to convert the document id into a normal document name. 425 | /// 426 | /// The conversion fails if and only if the document id does not specify a 427 | /// normal document. In other words, the conversion succeeds if and only if 428 | /// the document id begins with neither the `_design/` prefix nor the 429 | /// `_local` prefix. 430 | /// 431 | pub fn into_normal_document_name(self) -> Result { 432 | if let (None, base) = self.split_prefix() { 433 | return Ok(NormalDocumentName::from(String::from(base))); 434 | } 435 | Err(self) 436 | } 437 | 438 | /// Returns whether the document id specifies a design document—i.e., the 439 | /// document begins with the `_design/` prefix. 440 | pub fn is_design(&self) -> bool { 441 | DocumentId::has_given_prefix(&self.0, DESIGN_PREFIX) 442 | } 443 | 444 | /// Tries to convert the document id into a design document name. 445 | pub fn into_design_document_name(self) -> Result { 446 | match self.split_prefix() { 447 | (Some(prefix), base) if prefix == DESIGN_PREFIX => return Ok(DesignDocumentName::from(String::from(base))), 448 | _ => {} 449 | } 450 | Err(self) 451 | } 452 | 453 | /// Tries to converts the document id into a design document id. 454 | /// 455 | /// The conversion fails if and only if the document id does not specify a 456 | /// design document. In other words, the conversion succeeds if and only if 457 | /// the document id begins with the `_design/` prefix. 458 | /// 459 | pub fn into_design_document_id(self) -> Result { 460 | if self.is_design() { Ok(DesignDocumentId(self)) } else { Err(self) } 461 | } 462 | 463 | /// Returns whether the document id specifies a local document—i.e., the 464 | /// document begins with the `_local/` prefix. 465 | pub fn is_local(&self) -> bool { 466 | DocumentId::has_given_prefix(&self.0, LOCAL_PREFIX) 467 | } 468 | 469 | /// Tries to convert the document id into a local document name. 470 | /// 471 | /// The conversion fails if and only if the document id does not specify a 472 | /// local document. In other words, the conversion succeeds if and only if 473 | /// the document id begins with the `_local/` prefix. 474 | /// 475 | pub fn into_local_document_name(self) -> Result { 476 | match self.split_prefix() { 477 | (Some(prefix), base) if prefix == LOCAL_PREFIX => return Ok(LocalDocumentName::from(String::from(base))), 478 | _ => {} 479 | } 480 | Err(self) 481 | } 482 | 483 | fn has_given_prefix(s: &str, prefix: &str) -> bool { 484 | s.starts_with(prefix) && s[prefix.len()..].starts_with('/') 485 | } 486 | 487 | fn split_prefix(&self) -> (Option<&str>, &str) { 488 | for &prefix in DOCUMENT_PREFIXES.iter() { 489 | if DocumentId::has_given_prefix(&self.0, prefix) { 490 | return (Some(prefix), &self.0[prefix.len() + 1..]); 491 | } 492 | } 493 | (None, &self.0) 494 | } 495 | } 496 | 497 | impl PathEncodable for DocumentId { 498 | fn encode_path_to(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 499 | let (prefix, base) = self.split_prefix(); 500 | if let Some(prefix) = prefix { 501 | percent_encode_segment(prefix, f)?; 502 | } 503 | percent_encode_segment(base, f) 504 | } 505 | } 506 | 507 | impl AsRef for DocumentId { 508 | fn as_ref(&self) -> &str { 509 | self.0.as_ref() 510 | } 511 | } 512 | 513 | impl From for String { 514 | fn from(doc_id: DocumentId) -> Self { 515 | String::from(doc_id.0) 516 | } 517 | } 518 | 519 | impl Display for DocumentId { 520 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 521 | self.0.fmt(f) 522 | } 523 | } 524 | 525 | impl<'a> From<&'a str> for DocumentId { 526 | fn from(s: &'a str) -> Self { 527 | DocumentId(String::from(s)) 528 | } 529 | } 530 | 531 | impl From for DocumentId { 532 | fn from(s: String) -> Self { 533 | DocumentId(s) 534 | } 535 | } 536 | 537 | impl From for DocumentId { 538 | fn from(ddoc_id: DesignDocumentId) -> Self { 539 | ddoc_id.0 540 | } 541 | } 542 | 543 | impl From for DocumentId { 544 | fn from(doc_name: NormalDocumentName) -> Self { 545 | DocumentId(doc_name.0) 546 | } 547 | } 548 | 549 | impl From for DocumentId { 550 | fn from(ddoc_name: DesignDocumentName) -> Self { 551 | DocumentId(format!("{}/{}", DESIGN_PREFIX, ddoc_name.0)) 552 | } 553 | } 554 | 555 | impl From for DocumentId { 556 | fn from(ldoc_name: LocalDocumentName) -> Self { 557 | DocumentId(format!("{}/{}", LOCAL_PREFIX, ldoc_name.0)) 558 | } 559 | } 560 | 561 | /// `DesignDocumentId` comprises URL path segments that, together, identify a 562 | /// design document. 563 | /// 564 | /// For example, given the document path `/db/_design/doc`, the design document 565 | /// id is `_design/doc`. 566 | /// 567 | /// `DesignDocumentId` is a special form of 568 | /// [`DocumentId`](struct.DocumentId.html). All design document ids are document 569 | /// ids, but not all document ids are design document ids. 570 | /// 571 | /// For more information about path-related types, see the [module-level 572 | /// documentation](index.html). 573 | /// 574 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 575 | pub struct DesignDocumentId(DocumentId); 576 | 577 | impl DesignDocumentId { 578 | /// Tries to construct a design document id from a string. 579 | pub fn parse(s: &str) -> Result { 580 | DesignDocumentId::from_str(s) 581 | } 582 | 583 | /// Converts the design document id into a `String`. 584 | pub fn into_string(self) -> String { 585 | self.0.into_string() 586 | } 587 | 588 | fn validate(s: &str) -> Result<(), Error> { 589 | if s.len() <= DESIGN_PREFIX.len() + '/'.len_utf8() || !s.starts_with(DESIGN_PREFIX) || 590 | !s[DESIGN_PREFIX.len()..].starts_with('/') 591 | { 592 | return Err(Error::BadDesignDocumentId); 593 | } 594 | Ok(()) 595 | } 596 | 597 | /// Converts the design document id into a general document id. 598 | pub fn into_document_id(self) -> DocumentId { 599 | self.0 600 | } 601 | 602 | /// Converts the design document id into a design document name. 603 | pub fn into_design_document_name(self) -> DesignDocumentName { 604 | self.into_document_id().into_design_document_name().unwrap() 605 | } 606 | } 607 | 608 | impl PathEncodable for DesignDocumentId { 609 | fn encode_path_to(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 610 | self.0.encode_path_to(f) 611 | } 612 | } 613 | 614 | impl AsRef for DesignDocumentId { 615 | fn as_ref(&self) -> &str { 616 | self.0.as_ref() 617 | } 618 | } 619 | 620 | impl From for String { 621 | fn from(ddoc_id: DesignDocumentId) -> Self { 622 | String::from(ddoc_id.0) 623 | } 624 | } 625 | 626 | impl Display for DesignDocumentId { 627 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 628 | self.0.fmt(f) 629 | } 630 | } 631 | 632 | impl FromStr for DesignDocumentId { 633 | type Err = Error; 634 | fn from_str(s: &str) -> Result { 635 | DesignDocumentId::validate(s)?; 636 | Ok(DesignDocumentId(DocumentId::from(s))) 637 | } 638 | } 639 | 640 | impl From for DesignDocumentId { 641 | fn from(ddoc_name: DesignDocumentName) -> Self { 642 | DesignDocumentId(DocumentId::new(format!("{}/{}", DESIGN_PREFIX, ddoc_name))) 643 | } 644 | } 645 | 646 | impl<'a> Deserialize<'a> for DesignDocumentId { 647 | fn deserialize(deserializer: D) -> Result 648 | where 649 | D: serde::Deserializer<'a>, 650 | { 651 | struct Visitor; 652 | 653 | impl<'b> serde::de::Visitor<'b> for Visitor { 654 | type Value = DesignDocumentId; 655 | 656 | fn expecting(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 657 | write!(f, "a string specifying a CouchDB design document id") 658 | } 659 | 660 | fn visit_str(self, source: &str) -> Result 661 | where 662 | E: serde::de::Error, 663 | { 664 | if !DocumentId::has_given_prefix(source, DESIGN_PREFIX) { 665 | return Err(E::invalid_value(serde::de::Unexpected::Str(source), &self)); 666 | } 667 | Ok(DesignDocumentId(DocumentId::from(source))) 668 | } 669 | 670 | fn visit_string(self, source: String) -> Result 671 | where 672 | E: serde::de::Error, 673 | { 674 | if !DocumentId::has_given_prefix(&source, DESIGN_PREFIX) { 675 | return Err(E::invalid_value(serde::de::Unexpected::Str(&source), &self)); 676 | } 677 | Ok(DesignDocumentId(DocumentId::from(source))) 678 | } 679 | } 680 | 681 | deserializer.deserialize_string(Visitor) 682 | } 683 | } 684 | 685 | impl PathDecodable for DesignDocumentId { 686 | fn path_decode(s: String) -> Self { 687 | debug_assert!(DocumentId::has_given_prefix(&s, DESIGN_PREFIX)); 688 | DesignDocumentId(DocumentId::from(s)) 689 | } 690 | } 691 | 692 | /// `ViewId` comprises URL path segments that combine a design document name and 693 | /// a view name. 694 | /// 695 | /// For example, given the view path `/db/_design/doc/_view/view`, the view id 696 | /// is `doc/view`. 697 | /// 698 | /// An application can use `ViewId` when specifying a view filter by, for 699 | /// example, sending an HTTP request to `GET 700 | /// /{db}/_changes?filter=_view&view=doc/view`, where `doc/view` is a view id. 701 | /// 702 | /// **NOTE:** As of version 1.6.1, the CouchDB server does not support view 703 | /// filters where either the design document name or view name contain a slash 704 | /// character (`/`). As such, `ViewId` makes no attempt to correctly 705 | /// percent-encode the names. 706 | /// 707 | /// For more information about path-related types, see the [module-level 708 | /// documentation](index.html). 709 | /// 710 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 711 | pub struct ViewId(String); 712 | 713 | impl ViewId { 714 | /// Constructs a view id from a design document name and a view name. 715 | pub fn new(ddoc_name: T, view_name: U) -> Self 716 | where 717 | T: Into, 718 | U: Into, 719 | { 720 | ViewId(format!("{}/{}", ddoc_name.into(), view_name.into())) 721 | } 722 | 723 | /// Converts the view id into a `String`. 724 | pub fn into_string(self) -> String { 725 | self.0 726 | } 727 | } 728 | 729 | impl AsRef for ViewId { 730 | fn as_ref(&self) -> &str { 731 | self.0.as_ref() 732 | } 733 | } 734 | 735 | impl From for String { 736 | fn from(view_id: ViewId) -> Self { 737 | String::from(view_id.0) 738 | } 739 | } 740 | 741 | impl Display for ViewId { 742 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 743 | self.0.fmt(f) 744 | } 745 | } 746 | 747 | /// `DatabasePath` is the full URL path of a database. 748 | /// 749 | /// For more information about path-related types, see the [module-level 750 | /// documentation](index.html). 751 | /// 752 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] 753 | pub struct DatabasePath { 754 | db_name: DatabaseName, 755 | } 756 | 757 | impl DatabasePath { 758 | /// Tries to construct a database path from a string. 759 | pub fn parse(s: &str) -> Result { 760 | DatabasePath::from_str(s) 761 | } 762 | 763 | /// Borrows the path's database name. 764 | pub fn database_name(&self) -> &DatabaseName { 765 | &self.db_name 766 | } 767 | 768 | /// Joins the path with a document id to construct a document path. 769 | pub fn with_document_id>(self, doc_id: T) -> DocumentPath { 770 | DocumentPath { 771 | db_name: self.db_name, 772 | doc_id: doc_id.into(), 773 | } 774 | } 775 | 776 | /// Joins the path with a design document id to construct a design document 777 | /// path. 778 | pub fn with_design_document_id>(self, ddoc_id: T) -> DesignDocumentPath { 779 | DesignDocumentPath { 780 | db_name: self.db_name, 781 | ddoc_id: ddoc_id.into(), 782 | } 783 | } 784 | } 785 | 786 | impl FromStr for DatabasePath { 787 | type Err = Error; 788 | fn from_str(s: &str) -> Result { 789 | let mut p = PathDecoder::begin(s)?; 790 | let db_name = p.decode_segment()?; 791 | p.end()?; 792 | Ok(DatabasePath { db_name: db_name }) 793 | } 794 | } 795 | 796 | impl Display for DatabasePath { 797 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 798 | self.db_name.encode_path_to(f)?; 799 | Ok(()) 800 | } 801 | } 802 | 803 | /// `DocumentPath` is the full URL path of a document. 804 | /// 805 | /// For more information about path-related types, see the [module-level 806 | /// documentation](index.html). 807 | /// 808 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] 809 | pub struct DocumentPath { 810 | db_name: DatabaseName, 811 | doc_id: DocumentId, 812 | } 813 | 814 | impl DocumentPath { 815 | /// Tries to construct a document path from a string. 816 | pub fn parse(s: &str) -> Result { 817 | DocumentPath::from_str(s) 818 | } 819 | 820 | /// Borrows the path's database name. 821 | pub fn database_name(&self) -> &DatabaseName { 822 | &self.db_name 823 | } 824 | 825 | /// Borrows the path's document id. 826 | pub fn document_id(&self) -> &DocumentId { 827 | &self.doc_id 828 | } 829 | 830 | /// Joins the path with an attachment name to construct an attachment path. 831 | pub fn with_attachment_name>(self, att_name: T) -> AttachmentPath { 832 | AttachmentPath { 833 | db_name: self.db_name, 834 | doc_id: self.doc_id, 835 | att_name: att_name.into(), 836 | } 837 | } 838 | } 839 | 840 | impl FromStr for DocumentPath { 841 | type Err = Error; 842 | fn from_str(s: &str) -> Result { 843 | let mut p = PathDecoder::begin(s)?; 844 | let db_name = p.decode_segment()?; 845 | let doc_id = p.decode_with_optional_prefix(DOCUMENT_PREFIXES)?; 846 | p.end()?; 847 | Ok(DocumentPath { 848 | db_name: db_name, 849 | doc_id: doc_id, 850 | }) 851 | } 852 | } 853 | 854 | impl Display for DocumentPath { 855 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 856 | self.db_name.encode_path_to(f)?; 857 | self.doc_id.encode_path_to(f)?; 858 | Ok(()) 859 | } 860 | } 861 | 862 | /// `DesignDocumentPath` is the full URL path of a design document. 863 | /// 864 | /// For more information about path-related types, see the [module-level 865 | /// documentation](index.html). 866 | /// 867 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] 868 | pub struct DesignDocumentPath { 869 | db_name: DatabaseName, 870 | ddoc_id: DesignDocumentId, 871 | } 872 | 873 | impl DesignDocumentPath { 874 | /// Tries to construct a design database path from a string. 875 | pub fn parse(s: &str) -> Result { 876 | DesignDocumentPath::from_str(s) 877 | } 878 | 879 | /// Borrows this design document path's database name. 880 | pub fn database_name(&self) -> &DatabaseName { 881 | &self.db_name 882 | } 883 | 884 | /// Borrows this design document path's design document id. 885 | pub fn design_document_id(&self) -> &DesignDocumentId { 886 | &self.ddoc_id 887 | } 888 | 889 | /// Joins the path with an attachment name to construct an attachment path. 890 | pub fn with_attachment_name>(self, att_name: T) -> AttachmentPath { 891 | AttachmentPath { 892 | db_name: self.db_name, 893 | doc_id: self.ddoc_id.into_document_id(), 894 | att_name: att_name.into(), 895 | } 896 | } 897 | 898 | /// Joins the path with a view name to construct a view path. 899 | pub fn with_view_name>(self, view_name: T) -> ViewPath { 900 | ViewPath { 901 | db_name: self.db_name, 902 | ddoc_id: self.ddoc_id, 903 | view_name: view_name.into(), 904 | } 905 | } 906 | } 907 | 908 | impl FromStr for DesignDocumentPath { 909 | type Err = Error; 910 | fn from_str(s: &str) -> Result { 911 | let mut p = PathDecoder::begin(s)?; 912 | let db_name = p.decode_segment()?; 913 | let ddoc_id = p.decode_with_prefix(DESIGN_PREFIX)?; 914 | p.end()?; 915 | Ok(DesignDocumentPath { 916 | db_name: db_name, 917 | ddoc_id: ddoc_id, 918 | }) 919 | } 920 | } 921 | 922 | impl Display for DesignDocumentPath { 923 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 924 | self.db_name.encode_path_to(f)?; 925 | self.ddoc_id.encode_path_to(f)?; 926 | Ok(()) 927 | } 928 | } 929 | 930 | /// `AttachmentPath` is the full URL path of an attachment. 931 | /// 932 | /// For more information about path-related types, see the [module-level 933 | /// documentation](index.html). 934 | /// 935 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] 936 | pub struct AttachmentPath { 937 | db_name: DatabaseName, 938 | doc_id: DocumentId, 939 | att_name: AttachmentName, 940 | } 941 | 942 | impl AttachmentPath { 943 | /// Tries to construct an attachment path from a string. 944 | pub fn parse(s: &str) -> Result { 945 | AttachmentPath::from_str(s) 946 | } 947 | 948 | /// Borrows the path's database name. 949 | pub fn database_name(&self) -> &DatabaseName { 950 | &self.db_name 951 | } 952 | 953 | /// Borrows the path's document id. 954 | pub fn document_id(&self) -> &DocumentId { 955 | &self.doc_id 956 | } 957 | 958 | /// Borrows the path's attachment name. 959 | pub fn attachment_name(&self) -> &AttachmentName { 960 | &self.att_name 961 | } 962 | } 963 | 964 | impl FromStr for AttachmentPath { 965 | type Err = Error; 966 | fn from_str(s: &str) -> Result { 967 | let mut p = PathDecoder::begin(s)?; 968 | let db_name = p.decode_segment()?; 969 | let doc_id = p.decode_with_optional_prefix(DOCUMENT_PREFIXES)?; 970 | let att_name = p.decode_segment()?; 971 | p.end()?; 972 | Ok(AttachmentPath { 973 | db_name: db_name, 974 | doc_id: doc_id, 975 | att_name: att_name, 976 | }) 977 | } 978 | } 979 | 980 | impl Display for AttachmentPath { 981 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 982 | self.db_name.encode_path_to(f)?; 983 | self.doc_id.encode_path_to(f)?; 984 | self.att_name.encode_path_to(f)?; 985 | Ok(()) 986 | } 987 | } 988 | 989 | /// `ViewPath` is the full URL path of a view. 990 | /// 991 | /// For more information about path-related types, see the [module-level 992 | /// documentation](index.html). 993 | /// 994 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] 995 | pub struct ViewPath { 996 | db_name: DatabaseName, 997 | ddoc_id: DesignDocumentId, 998 | view_name: ViewName, 999 | } 1000 | 1001 | impl ViewPath { 1002 | /// Tries to construct a view path from a string. 1003 | pub fn parse(s: &str) -> Result { 1004 | ViewPath::from_str(s) 1005 | } 1006 | 1007 | /// Borrows the path's database name. 1008 | pub fn database_name(&self) -> &DatabaseName { 1009 | &self.db_name 1010 | } 1011 | 1012 | /// Borrows the path's design document id. 1013 | pub fn design_document_id(&self) -> &DesignDocumentId { 1014 | &self.ddoc_id 1015 | } 1016 | 1017 | /// Borrows the path's view name. 1018 | pub fn view_name(&self) -> &ViewName { 1019 | &self.view_name 1020 | } 1021 | } 1022 | 1023 | impl FromStr for ViewPath { 1024 | type Err = Error; 1025 | fn from_str(s: &str) -> Result { 1026 | let mut p = PathDecoder::begin(s)?; 1027 | let db_name = p.decode_segment()?; 1028 | let ddoc_id = p.decode_with_prefix(DESIGN_PREFIX)?; 1029 | p.decode_exact(VIEW_PREFIX)?; 1030 | let view_name = p.decode_segment()?; 1031 | p.end()?; 1032 | Ok(ViewPath { 1033 | db_name: db_name, 1034 | ddoc_id: ddoc_id, 1035 | view_name: view_name, 1036 | }) 1037 | } 1038 | } 1039 | 1040 | impl Display for ViewPath { 1041 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 1042 | self.db_name.encode_path_to(f)?; 1043 | self.ddoc_id.encode_path_to(f)?; 1044 | percent_encode_segment(VIEW_PREFIX, f)?; 1045 | self.view_name.encode_path_to(f)?; 1046 | Ok(()) 1047 | } 1048 | } 1049 | #[cfg(test)] 1050 | mod tests { 1051 | use super::*; 1052 | use {serde_json, std}; 1053 | 1054 | define_name_type!(TestName, test_name, #[doc=""], #[doc=""]); 1055 | 1056 | #[test] 1057 | fn path_decoding_must_begin_with_leading_slash() { 1058 | PathDecoder::begin("/").unwrap(); 1059 | PathDecoder::begin("").unwrap_err().to_string().contains( 1060 | E_NO_LEADING_SLASH, 1061 | ); 1062 | PathDecoder::begin("alpha") 1063 | .unwrap_err() 1064 | .to_string() 1065 | .contains(E_NO_LEADING_SLASH); 1066 | PathDecoder::begin("alpha/bravo") 1067 | .unwrap_err() 1068 | .to_string() 1069 | .contains(E_NO_LEADING_SLASH); 1070 | } 1071 | 1072 | #[test] 1073 | fn path_decoding_must_end_with_empty_string() { 1074 | 1075 | let mut p = PathDecoder::begin("/alpha").unwrap(); 1076 | assert_eq!(p.decode_segment::().unwrap(), "alpha"); 1077 | p.end().unwrap(); 1078 | 1079 | let p = PathDecoder::begin("/").unwrap(); 1080 | assert!(p.end().unwrap_err().to_string().contains(E_TRAILING_SLASH)); 1081 | 1082 | let p = PathDecoder::begin("//").unwrap(); 1083 | assert!(p.end().unwrap_err().to_string().contains( 1084 | E_TOO_MANY_SEGMENTS, 1085 | )); 1086 | 1087 | let p = PathDecoder::begin("/alpha").unwrap(); 1088 | assert!(p.end().unwrap_err().to_string().contains( 1089 | E_TOO_MANY_SEGMENTS, 1090 | )); 1091 | } 1092 | 1093 | #[test] 1094 | fn path_decoding_enforces_nonemptiness_for_segments() { 1095 | let mut p = PathDecoder::begin("/alpha//bravo").unwrap(); 1096 | assert_eq!(p.decode_segment::().unwrap(), "alpha"); 1097 | assert!( 1098 | p.decode_segment::() 1099 | .unwrap_err() 1100 | .to_string() 1101 | .contains(E_EMPTY_SEGMENT) 1102 | ); 1103 | 1104 | let mut p = PathDecoder::begin("/alpha//bravo").unwrap(); 1105 | assert!( 1106 | p.decode_with_prefix::("alpha") 1107 | .unwrap_err() 1108 | .to_string() 1109 | .contains(E_EMPTY_SEGMENT) 1110 | ); 1111 | 1112 | let mut p = PathDecoder::begin("/alpha//bravo").unwrap(); 1113 | assert_eq!(p.decode_segment::().unwrap(), "alpha"); 1114 | assert!( 1115 | p.decode_with_optional_prefix::(&["charlie"]) 1116 | .unwrap_err() 1117 | .to_string() 1118 | .contains(E_EMPTY_SEGMENT) 1119 | ); 1120 | 1121 | println!("CHECK go time"); 1122 | let mut p = PathDecoder::begin("/alpha//bravo").unwrap(); 1123 | assert!( 1124 | p.decode_with_optional_prefix::(&["alpha"]) 1125 | .unwrap_err() 1126 | .to_string() 1127 | .contains(E_EMPTY_SEGMENT) 1128 | ); 1129 | } 1130 | 1131 | #[test] 1132 | fn path_decoding_fails_on_a_path_having_too_few_segments() { 1133 | let mut p = PathDecoder::begin("/alpha").unwrap(); 1134 | assert_eq!(p.decode_segment::().unwrap(), "alpha"); 1135 | assert!( 1136 | p.decode_segment::() 1137 | .unwrap_err() 1138 | .to_string() 1139 | .contains(E_TOO_FEW_SEGMENTS) 1140 | ); 1141 | 1142 | let mut p = PathDecoder::begin("/alpha").unwrap(); 1143 | assert_eq!(p.decode_segment::().unwrap(), "alpha"); 1144 | assert!( 1145 | p.decode_with_prefix::("bravo") 1146 | .unwrap_err() 1147 | .to_string() 1148 | .contains(E_TOO_FEW_SEGMENTS) 1149 | ); 1150 | 1151 | // I.e., once we find the prefix in the input string, we're committed to 1152 | // decoding with that prefix and will not fall back to not using the 1153 | // prefix. 1154 | // 1155 | // This helps enforce additional strictness so that don't, say, end up 1156 | // with a non-design document named "_design" but instead yield an 1157 | // error. 1158 | 1159 | let mut p = PathDecoder::begin("/alpha").unwrap(); 1160 | assert_eq!(p.decode_segment::().unwrap(), "alpha"); 1161 | assert!( 1162 | p.decode_with_optional_prefix::(&["alpha"]) 1163 | .unwrap_err() 1164 | .to_string() 1165 | .contains(E_TOO_FEW_SEGMENTS) 1166 | ); 1167 | } 1168 | 1169 | #[test] 1170 | fn path_decoding_fails_on_an_unexpected_segment() { 1171 | let mut p = PathDecoder::begin("/alpha/bravo").unwrap(); 1172 | assert!( 1173 | p.decode_with_prefix::("bravo") 1174 | .unwrap_err() 1175 | .to_string() 1176 | .contains(E_UNEXPECTED_SEGMENT) 1177 | ); 1178 | 1179 | let mut p = PathDecoder::begin("/alpha/bravo").unwrap(); 1180 | assert!(p.decode_exact("bravo").unwrap_err().to_string().contains( 1181 | E_UNEXPECTED_SEGMENT, 1182 | )); 1183 | } 1184 | 1185 | #[test] 1186 | fn path_decoding_succeeds_on_a_prefix() { 1187 | let mut p = PathDecoder::begin("/alpha/bravo/charlie").unwrap(); 1188 | assert_eq!( 1189 | p.decode_with_prefix::("alpha").unwrap(), 1190 | "alpha/bravo" 1191 | ); 1192 | assert_eq!(p.decode_segment::().unwrap(), "charlie"); 1193 | p.end().unwrap(); 1194 | } 1195 | 1196 | #[test] 1197 | fn path_decoding_succeeds_on_an_optional_prefix() { 1198 | let mut p = PathDecoder::begin("/alpha/bravo/charlie").unwrap(); 1199 | assert_eq!( 1200 | p.decode_with_optional_prefix::(&["alpha"]) 1201 | .unwrap(), 1202 | "alpha/bravo" 1203 | ); 1204 | assert_eq!(p.decode_segment::().unwrap(), "charlie"); 1205 | p.end().unwrap(); 1206 | 1207 | let mut p = PathDecoder::begin("/bravo/charlie").unwrap(); 1208 | assert_eq!( 1209 | p.decode_with_optional_prefix::(&["alpha", "bravo"]) 1210 | .unwrap(), 1211 | "bravo/charlie" 1212 | ); 1213 | p.end().unwrap(); 1214 | 1215 | let mut p = PathDecoder::begin("/bravo").unwrap(); 1216 | assert_eq!( 1217 | p.decode_with_optional_prefix::(&["alpha"]) 1218 | .unwrap(), 1219 | "bravo" 1220 | ); 1221 | p.end().unwrap(); 1222 | } 1223 | 1224 | #[test] 1225 | fn path_decoding_percent_decodes() { 1226 | let mut p = PathDecoder::begin("/alpha%20bravo%2fcharlie").unwrap(); 1227 | assert_eq!(p.decode_segment::().unwrap(), "alpha bravo/charlie"); 1228 | p.end().unwrap(); 1229 | } 1230 | 1231 | fn encode_path(x: &T) -> String { 1232 | struct PathEncoder<'a, T: PathEncodable + 'a>(&'a T); 1233 | impl<'a, T: PathEncodable> std::fmt::Display for PathEncoder<'a, T> { 1234 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 1235 | self.0.encode_path_to(f) 1236 | } 1237 | } 1238 | 1239 | // Ensure that the percent-encodings are uppercase, e.g., "%2F" not 1240 | // "%2f". 1241 | 1242 | let encoded = PathEncoder(x).to_string(); 1243 | let mut iter = encoded.split('%'); 1244 | let first = iter.next().unwrap(); 1245 | 1246 | iter.map(|s| { 1247 | // str has no split_mut! 1248 | let mut a = s[..2].to_uppercase(); 1249 | a.push_str(&s[2..]); 1250 | a 1251 | }).fold(String::from(first), |mut a, b| { 1252 | a.push('%'); 1253 | a.push_str(&b); 1254 | a 1255 | }) 1256 | } 1257 | 1258 | #[test] 1259 | fn name_type_percent_encodes_self() { 1260 | assert_eq!( 1261 | encode_path(&TestName::new("alpha/bravo?charlie")), 1262 | "/alpha%2Fbravo%3Fcharlie" 1263 | ); 1264 | } 1265 | 1266 | #[test] 1267 | fn document_id_distinguishes_by_document_type() { 1268 | let doc_id = DocumentId::new("alpha"); 1269 | assert!(doc_id.is_normal()); 1270 | assert!(!doc_id.is_design()); 1271 | assert!(!doc_id.is_local()); 1272 | 1273 | let doc_id = DocumentId::new("_design/alpha"); 1274 | assert!(!doc_id.is_normal()); 1275 | assert!(doc_id.is_design()); 1276 | assert!(!doc_id.is_local()); 1277 | 1278 | let doc_id = DocumentId::new("_local/alpha"); 1279 | assert!(!doc_id.is_normal()); 1280 | assert!(!doc_id.is_design()); 1281 | assert!(doc_id.is_local()); 1282 | } 1283 | 1284 | #[test] 1285 | fn document_id_converts_into_normal_document_name() { 1286 | assert_eq!( 1287 | DocumentId::new("alpha").into_normal_document_name(), 1288 | Ok(NormalDocumentName::new("alpha")) 1289 | ); 1290 | assert_eq!( 1291 | DocumentId::new("alpha/bravo?charlie").into_normal_document_name(), 1292 | Ok(NormalDocumentName::new("alpha/bravo?charlie")) 1293 | ); 1294 | assert_eq!( 1295 | DocumentId::new("_design/alpha").into_normal_document_name(), 1296 | Err(DocumentId::new("_design/alpha")) 1297 | ); 1298 | assert_eq!( 1299 | DocumentId::new("_local/alpha").into_normal_document_name(), 1300 | Err(DocumentId::new("_local/alpha")) 1301 | ); 1302 | } 1303 | 1304 | #[test] 1305 | fn document_id_converts_into_design_document_name() { 1306 | assert_eq!( 1307 | DocumentId::new("alpha").into_design_document_name(), 1308 | Err(DocumentId::new("alpha")) 1309 | ); 1310 | assert_eq!( 1311 | DocumentId::new("_design/alpha").into_design_document_name(), 1312 | Ok(DesignDocumentName::new("alpha")) 1313 | ); 1314 | assert_eq!( 1315 | DocumentId::new("_design/alpha/bravo?charlie").into_design_document_name(), 1316 | Ok(DesignDocumentName::new("alpha/bravo?charlie")) 1317 | ); 1318 | assert_eq!( 1319 | DocumentId::new("_local/alpha").into_design_document_name(), 1320 | Err(DocumentId::new("_local/alpha")) 1321 | ); 1322 | } 1323 | 1324 | #[test] 1325 | fn document_id_converts_into_local_document_name() { 1326 | assert_eq!( 1327 | DocumentId::new("alpha").into_local_document_name(), 1328 | Err(DocumentId::new("alpha")) 1329 | ); 1330 | assert_eq!( 1331 | DocumentId::new("_design/alpha").into_local_document_name(), 1332 | Err(DocumentId::new("_design/alpha")) 1333 | ); 1334 | assert_eq!( 1335 | DocumentId::new("_local/alpha").into_local_document_name(), 1336 | Ok(LocalDocumentName::new("alpha")) 1337 | ); 1338 | assert_eq!( 1339 | DocumentId::new("_local/alpha/bravo?charlie").into_local_document_name(), 1340 | Ok(LocalDocumentName::new("alpha/bravo?charlie")) 1341 | ); 1342 | } 1343 | 1344 | #[test] 1345 | fn document_id_converts_from_document_name() { 1346 | assert_eq!( 1347 | DocumentId::from(NormalDocumentName::new("alpha")), 1348 | DocumentId::new("alpha") 1349 | ); 1350 | assert_eq!( 1351 | DocumentId::from(DesignDocumentName::new("alpha")), 1352 | DocumentId::new("_design/alpha") 1353 | ); 1354 | assert_eq!( 1355 | DocumentId::from(LocalDocumentName::new("alpha")), 1356 | DocumentId::new("_local/alpha") 1357 | ); 1358 | } 1359 | 1360 | #[test] 1361 | fn document_id_percent_encodes_self() { 1362 | assert_eq!( 1363 | encode_path(&DocumentId::new("alpha/bravo?charlie")), 1364 | "/alpha%2Fbravo%3Fcharlie" 1365 | ); 1366 | assert_eq!( 1367 | encode_path(&DocumentId::new("_design/alpha/bravo?charlie")), 1368 | "/_design/alpha%2Fbravo%3Fcharlie" 1369 | ); 1370 | assert_eq!( 1371 | encode_path(&DocumentId::new("_local/alpha/bravo?charlie")), 1372 | "/_local/alpha%2Fbravo%3Fcharlie" 1373 | ); 1374 | } 1375 | 1376 | #[test] 1377 | fn design_document_id_converts_into_document_id() { 1378 | assert_eq!( 1379 | DesignDocumentId::parse("_design/alpha") 1380 | .unwrap() 1381 | .into_document_id(), 1382 | DocumentId::new("_design/alpha") 1383 | ) 1384 | } 1385 | 1386 | #[test] 1387 | fn design_document_id_converts_into_design_document_name() { 1388 | assert_eq!( 1389 | DesignDocumentId::parse("_design/alpha") 1390 | .unwrap() 1391 | .into_design_document_name(), 1392 | DesignDocumentName::new("alpha") 1393 | ) 1394 | } 1395 | 1396 | #[test] 1397 | fn design_document_id_converts_from_design_document_name() { 1398 | assert_eq!( 1399 | DesignDocumentId::from(DesignDocumentName::new("alpha")), 1400 | DesignDocumentId::parse("_design/alpha").unwrap() 1401 | ); 1402 | } 1403 | 1404 | #[test] 1405 | fn design_document_id_deserialization_enforces_design_prefix() { 1406 | 1407 | let source = r#""_design/alpha""#; 1408 | let got: DesignDocumentId = serde_json::from_str(&source).unwrap(); 1409 | let expected = DesignDocumentId(DocumentId::new("_design/alpha")); 1410 | assert_eq!(got, expected); 1411 | 1412 | let source = r#""alpha""#; 1413 | match serde_json::from_str::(&source) { 1414 | Err(ref e) if e.is_data() => {} 1415 | x => panic!("Got unexpected result {:?}", x), 1416 | } 1417 | } 1418 | 1419 | #[test] 1420 | fn database_path_percent_encodes_itself() { 1421 | let got = DatabaseName::new("alpha/bravo?charlie") 1422 | .into_database_path() 1423 | .to_string(); 1424 | let expected = "/alpha%2Fbravo%3Fcharlie"; 1425 | assert_eq!(got, expected); 1426 | } 1427 | 1428 | #[test] 1429 | fn database_path_decodes_str() { 1430 | let got = DatabasePath::from_str("/alpha%2Fbravo%3Fcharlie").unwrap(); 1431 | let expected = DatabaseName::new("alpha/bravo?charlie").into_database_path(); 1432 | assert_eq!(got, expected); 1433 | } 1434 | 1435 | #[test] 1436 | fn document_path_percent_encodes_itself() { 1437 | let got = DatabaseName::new("alpha/bravo?charlie") 1438 | .with_document_id("delta/echo?foxtrot") 1439 | .to_string(); 1440 | let expected = "/alpha%2Fbravo%3Fcharlie/delta%2Fecho%3Ffoxtrot"; 1441 | assert_eq!(got, expected); 1442 | 1443 | let got = DatabaseName::new("alpha/bravo?charlie") 1444 | .with_document_id(DesignDocumentName::new("delta/echo?foxtrot")) 1445 | .to_string(); 1446 | let expected = "/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot"; 1447 | assert_eq!(got, expected); 1448 | 1449 | let got = DatabaseName::new("alpha/bravo?charlie") 1450 | .with_document_id(LocalDocumentName::new("delta/echo?foxtrot")) 1451 | .to_string(); 1452 | let expected = "/alpha%2Fbravo%3Fcharlie/_local/delta%2Fecho%3Ffoxtrot"; 1453 | assert_eq!(got, expected); 1454 | } 1455 | 1456 | #[test] 1457 | fn document_path_decodes_str() { 1458 | let got = DocumentPath::from_str("/alpha%2Fbravo%3Fcharlie/delta%2Fecho%3Ffoxtrot").unwrap(); 1459 | let expected = DatabaseName::new("alpha/bravo?charlie").with_document_id("delta/echo?foxtrot"); 1460 | assert_eq!(got, expected); 1461 | 1462 | let got = DocumentPath::from_str("/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot").unwrap(); 1463 | let expected = 1464 | DatabaseName::new("alpha/bravo?charlie").with_document_id(DesignDocumentName::new("delta/echo?foxtrot")); 1465 | assert_eq!(got, expected); 1466 | 1467 | let got = DocumentPath::from_str("/alpha%2Fbravo%3Fcharlie/_local/delta%2Fecho%3Ffoxtrot").unwrap(); 1468 | let expected = 1469 | DatabaseName::new("alpha/bravo?charlie").with_document_id(LocalDocumentName::new("delta/echo?foxtrot")); 1470 | assert_eq!(got, expected); 1471 | } 1472 | 1473 | #[test] 1474 | fn design_document_path_percent_encodes_itself() { 1475 | let got = DatabaseName::new("alpha/bravo?charlie") 1476 | .with_document_id(DesignDocumentName::new("delta/echo?foxtrot")) 1477 | .to_string(); 1478 | let expected = "/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot"; 1479 | assert_eq!(got, expected); 1480 | } 1481 | 1482 | #[test] 1483 | fn design_document_path_decodes_str() { 1484 | let got = DesignDocumentPath::from_str("/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot").unwrap(); 1485 | let expected = DatabaseName::new("alpha/bravo?charlie") 1486 | .with_design_document_id(DesignDocumentName::new("delta/echo?foxtrot")); 1487 | assert_eq!(got, expected); 1488 | } 1489 | 1490 | #[test] 1491 | fn attachment_path_percent_encodes_itself() { 1492 | let got = DatabaseName::new("alpha/bravo?charlie") 1493 | .with_document_id("delta/echo?foxtrot") 1494 | .with_attachment_name("golf") 1495 | .to_string(); 1496 | let expected = "/alpha%2Fbravo%3Fcharlie/delta%2Fecho%3Ffoxtrot/golf"; 1497 | assert_eq!(got, expected); 1498 | 1499 | let got = DatabaseName::new("alpha/bravo?charlie") 1500 | .with_document_id(DesignDocumentName::new("delta/echo?foxtrot")) 1501 | .with_attachment_name("golf") 1502 | .to_string(); 1503 | let expected = "/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot/golf"; 1504 | assert_eq!(got, expected); 1505 | 1506 | let got = DatabaseName::new("alpha/bravo?charlie") 1507 | .with_document_id(LocalDocumentName::new("delta/echo?foxtrot")) 1508 | .with_attachment_name("golf") 1509 | .to_string(); 1510 | let expected = "/alpha%2Fbravo%3Fcharlie/_local/delta%2Fecho%3Ffoxtrot/golf"; 1511 | assert_eq!(got, expected); 1512 | } 1513 | 1514 | #[test] 1515 | fn attachment_path_decodes_str() { 1516 | let got = AttachmentPath::from_str("/alpha%2Fbravo%3Fcharlie/delta%2Fecho%3Ffoxtrot/golf").unwrap(); 1517 | let expected = DatabaseName::new("alpha/bravo?charlie") 1518 | .with_document_id("delta/echo?foxtrot") 1519 | .with_attachment_name("golf"); 1520 | assert_eq!(got, expected); 1521 | 1522 | let got = AttachmentPath::from_str( 1523 | "/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot/golf", 1524 | ).unwrap(); 1525 | let expected = DatabaseName::new("alpha/bravo?charlie") 1526 | .with_document_id(DesignDocumentName::new("delta/echo?foxtrot")) 1527 | .with_attachment_name("golf"); 1528 | assert_eq!(got, expected); 1529 | 1530 | let got = AttachmentPath::from_str( 1531 | "/alpha%2Fbravo%3Fcharlie/_local/delta%2Fecho%3Ffoxtrot/golf", 1532 | ).unwrap(); 1533 | let expected = DatabaseName::new("alpha/bravo?charlie") 1534 | .with_document_id(LocalDocumentName::new("delta/echo?foxtrot")) 1535 | .with_attachment_name("golf"); 1536 | assert_eq!(got, expected); 1537 | } 1538 | 1539 | #[test] 1540 | fn view_path_percent_encodes_itself() { 1541 | let got = DatabaseName::new("alpha/bravo?charlie") 1542 | .with_design_document_id(DesignDocumentName::new("delta/echo?foxtrot")) 1543 | .with_view_name("golf") 1544 | .to_string(); 1545 | let expected = "/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot/_view/golf"; 1546 | assert_eq!(got, expected); 1547 | } 1548 | 1549 | #[test] 1550 | fn view_path_decodes_str() { 1551 | let got = ViewPath::from_str( 1552 | "/alpha%2Fbravo%3Fcharlie/_design/delta%2Fecho%3Ffoxtrot/_view/golf", 1553 | ).unwrap(); 1554 | let expected = DatabaseName::new("alpha/bravo?charlie") 1555 | .with_design_document_id(DesignDocumentName::new("delta/echo?foxtrot")) 1556 | .with_view_name("golf"); 1557 | assert_eq!(got, expected); 1558 | } 1559 | } 1560 | -------------------------------------------------------------------------------- /src/revision.rs: -------------------------------------------------------------------------------- 1 | use {Error, serde, std}; 2 | use uuid::Uuid; 3 | 4 | /// `Revision` contains a document revision. 5 | /// 6 | /// # Summary 7 | /// 8 | /// * `Revision` stores a revision, which is a string that looks like 9 | /// `1-9c65296036141e575d32ba9c034dd3ee`. 10 | /// 11 | /// * `Revision` can be parsed from a string via `FromStr` or the 12 | /// `Revision::parse` method. 13 | /// 14 | /// * `Revision` implements `Deserialize` and `Serialize`. 15 | /// 16 | /// # Remarks 17 | /// 18 | /// A CouchDB document revision comprises a **sequence number** and an **MD5 19 | /// digest**. The sequence number (usually) starts at `1` when the document is 20 | /// created and increments by one each time the document is updated. The digest 21 | /// is a hash of the document content, which the CouchDB server uses to detect 22 | /// conflicts. 23 | /// 24 | /// # Example 25 | /// 26 | /// ``` 27 | /// extern crate couchdb; 28 | /// 29 | /// let rev = couchdb::Revision::parse("42-1234567890abcdef1234567890abcdef") 30 | /// .unwrap(); 31 | /// 32 | /// assert_eq!(rev.to_string(), "42-1234567890abcdef1234567890abcdef"); 33 | /// ``` 34 | /// 35 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 36 | pub struct Revision { 37 | sequence_number: u64, 38 | digest: Uuid, 39 | } 40 | 41 | impl Revision { 42 | /// Constructs a new `Revision` from the given string. 43 | /// 44 | /// The string must be of the form `42-1234567890abcdef1234567890abcdef`. 45 | /// 46 | pub fn parse(s: &str) -> Result { 47 | use std::str::FromStr; 48 | Revision::from_str(s) 49 | } 50 | 51 | /// Returns the sequence number part of the revision. 52 | /// 53 | /// The sequence number is the `42` part of the revision 54 | /// `42-1234567890abcdef1234567890abcdef`. 55 | /// 56 | pub fn sequence_number(&self) -> u64 { 57 | self.sequence_number 58 | } 59 | } 60 | 61 | impl std::fmt::Display for Revision { 62 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 63 | write!(f, "{}-{}", self.sequence_number, self.digest.simple()) 64 | } 65 | } 66 | 67 | impl std::str::FromStr for Revision { 68 | type Err = Error; 69 | fn from_str(s: &str) -> Result { 70 | 71 | let mut parts = s.splitn(2, '-'); 72 | 73 | let sequence_number_str = parts.next().ok_or(Error::BadRevision)?; 74 | let sequence_number = u64::from_str_radix(sequence_number_str, 10).map_err(|_| { 75 | Error::BadRevision 76 | })?; 77 | 78 | if sequence_number == 0 { 79 | return Err(Error::BadRevision); 80 | } 81 | 82 | let digest_str = parts.next().ok_or(Error::BadRevision)?; 83 | let digest = Uuid::parse_str(digest_str).map_err(|_| Error::BadRevision)?; 84 | 85 | if digest_str.chars().any(|c| !c.is_digit(16)) { 86 | return Err(Error::BadRevision); 87 | } 88 | 89 | Ok(Revision { 90 | sequence_number: sequence_number, 91 | digest: digest, 92 | }) 93 | } 94 | } 95 | 96 | impl From for String { 97 | fn from(revision: Revision) -> Self { 98 | revision.to_string() 99 | } 100 | } 101 | 102 | impl serde::Serialize for Revision { 103 | fn serialize(&self, serializer: S) -> Result 104 | where 105 | S: serde::Serializer, 106 | { 107 | let s = self.to_string(); 108 | serializer.serialize_str(&s) 109 | } 110 | } 111 | 112 | impl<'de> serde::Deserialize<'de> for Revision { 113 | fn deserialize(deserializer: D) -> Result 114 | where 115 | D: serde::Deserializer<'de>, 116 | { 117 | struct Visitor; 118 | 119 | impl<'de> serde::de::Visitor<'de> for Visitor { 120 | type Value = Revision; 121 | 122 | fn expecting(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 123 | write!(f, "a string specifying a CouchDB document revision") 124 | } 125 | 126 | fn visit_str(self, v: &str) -> Result 127 | where 128 | E: serde::de::Error, 129 | { 130 | Revision::parse(v).map_err(|_e| E::invalid_value(serde::de::Unexpected::Str(v), &self)) 131 | } 132 | } 133 | 134 | deserializer.deserialize_str(Visitor) 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | 141 | use super::*; 142 | use serde_json; 143 | 144 | #[test] 145 | fn parse_ok() { 146 | let expected = Revision { 147 | sequence_number: 42, 148 | digest: "1234567890abcdeffedcba0987654321".parse().unwrap(), 149 | }; 150 | let got = Revision::parse("42-1234567890abcdeffedcba0987654321").unwrap(); 151 | assert_eq!(expected, got); 152 | } 153 | 154 | #[test] 155 | fn parse_nok() { 156 | Revision::parse("bad_revision").unwrap_err(); 157 | } 158 | 159 | #[test] 160 | fn sequence_number() { 161 | let rev = Revision::parse("999-1234567890abcdef1234567890abcdef").unwrap(); 162 | assert_eq!(999, rev.sequence_number()); 163 | } 164 | 165 | #[test] 166 | fn display() { 167 | let expected = "42-1234567890abcdeffedcba0987654321"; 168 | let source = Revision { 169 | sequence_number: 42, 170 | digest: "1234567890abcdeffedcba0987654321".parse().unwrap(), 171 | }; 172 | let got = format!("{}", source); 173 | assert_eq!(expected, got); 174 | } 175 | 176 | #[test] 177 | fn from_str_ok() { 178 | use std::str::FromStr; 179 | let expected = Revision { 180 | sequence_number: 42, 181 | digest: "1234567890abcdeffedcba0987654321".parse().unwrap(), 182 | }; 183 | let got = Revision::from_str("42-1234567890abcdeffedcba0987654321").unwrap(); 184 | assert_eq!(expected, got); 185 | } 186 | 187 | #[test] 188 | fn from_str_nok() { 189 | macro_rules! expect_error { 190 | ($input: expr) => { 191 | match Revision::from_str($input) { 192 | Err(Error::RevisionParse{..}) => (), 193 | x => panic!("Got unexpected result {:?}", x), 194 | } 195 | } 196 | } 197 | 198 | use std::str::FromStr; 199 | 200 | Revision::from_str("12345678123456781234567812345678").unwrap_err(); 201 | Revision::from_str("-12345678123456781234567812345678").unwrap_err(); 202 | Revision::from_str("1-").unwrap_err(); 203 | Revision::from_str("1-1234567890abcdef1234567890abcdef-").unwrap_err(); 204 | Revision::from_str("-42-12345678123456781234567812345678").unwrap_err(); 205 | Revision::from_str("18446744073709551616-12345678123456781234567812345678").unwrap_err(); // overflow 206 | Revision::from_str("0-12345678123456781234567812345678").unwrap_err(); // zero sequence_number not allowed 207 | Revision::from_str("1-z2345678123456781234567812345678").unwrap_err(); 208 | Revision::from_str("1-1234567812345678123456781234567").unwrap_err(); 209 | Revision::from_str("bad_revision_blah_blah_blah").unwrap_err(); 210 | } 211 | 212 | #[test] 213 | fn string_from_revision() { 214 | let expected = "42-1234567890abcdeffedcba0987654321"; 215 | let source = Revision { 216 | sequence_number: 42, 217 | digest: "1234567890abcdeffedcba0987654321".parse().unwrap(), 218 | }; 219 | let got = format!("{}", source); 220 | assert_eq!(expected, got); 221 | } 222 | 223 | #[test] 224 | fn eq_same() { 225 | let r1 = Revision::parse("1-1234567890abcdef1234567890abcdef").unwrap(); 226 | let r2 = Revision::parse("1-1234567890abcdef1234567890abcdef").unwrap(); 227 | assert!(r1 == r2); 228 | } 229 | 230 | #[test] 231 | fn eq_different_numbers() { 232 | let r1 = Revision::parse("1-1234567890abcdef1234567890abcdef").unwrap(); 233 | let r2 = Revision::parse("7-1234567890abcdef1234567890abcdef").unwrap(); 234 | assert!(r1 != r2); 235 | } 236 | 237 | #[test] 238 | fn eq_different_digests() { 239 | let r1 = Revision::parse("1-1234567890abcdef1234567890abcdef").unwrap(); 240 | let r2 = Revision::parse("1-9999567890abcdef1234567890abcdef").unwrap(); 241 | assert!(r1 != r2); 242 | } 243 | 244 | #[test] 245 | fn eq_case_insensitive() { 246 | let r1 = Revision::parse("1-1234567890abcdef1234567890ABCDEF").unwrap(); 247 | let r2 = Revision::parse("1-1234567890ABCDEf1234567890abcdef").unwrap(); 248 | assert!(r1 == r2); 249 | } 250 | 251 | #[test] 252 | fn serialization_and_deserialization_ok() { 253 | let source = r#""42-1234567890abcdeffedcba0987654321""#; 254 | let expected = Revision::parse("42-1234567890abcdeffedcba0987654321").unwrap(); 255 | let got: Revision = serde_json::from_str(source).unwrap(); 256 | assert_eq!(got, expected); 257 | } 258 | 259 | #[test] 260 | fn deserialization_enforces_revision_validity() { 261 | let source = r#""obviously bad revision""#; 262 | match serde_json::from_str::(source) { 263 | Err(ref e) if e.is_data() => {} 264 | x => panic!("Got unexpected result {:?}", x), 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/root.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::marker::PhantomData; 3 | use uuid::Uuid; 4 | 5 | /// `Root` contains the content of a CouchDB server's root resource. 6 | /// 7 | /// # Summary 8 | /// 9 | /// * `Root` has public members instead of accessor methods because there are no 10 | /// invariants restricting the data. 11 | /// 12 | /// * `Root` implements `Deserialize`. 13 | /// 14 | /// # Remarks 15 | /// 16 | /// An application may obtain a CouchDB server's root resource by sending an 17 | /// HTTP request to GET `/`. 18 | /// 19 | /// # Compatibility 20 | /// 21 | /// `Root` contains a dummy private member in order to prevent applications from 22 | /// directly constructing a `Root` instance. This allows new fields to be added 23 | /// to `Root` in future releases without it being a breaking change. 24 | /// 25 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq)] 26 | pub struct Root { 27 | pub couchdb: String, 28 | pub uuid: Uuid, 29 | pub vendor: Vendor, 30 | pub version: Version, 31 | 32 | #[serde(default = "PhantomData::default")] 33 | _private_guard: PhantomData<()>, 34 | } 35 | 36 | /// `Vendor` contains information about a CouchDB server vendor. 37 | /// 38 | /// # Summary 39 | /// 40 | /// * `Vendor` has public members instead of accessor methods because there are 41 | /// no invariants restricting the data. 42 | /// 43 | /// * `Vendor` implements `Deserialize`. 44 | /// 45 | /// # Remarks 46 | /// 47 | /// `Vendor` is normally part of a [`Root`](struct.Root.html) instance. 48 | /// 49 | /// # Compatibility 50 | /// 51 | /// `Vendor` contains a dummy private member in order to prevent applications 52 | /// from directly constructing a `Vendor` instance. This allows new fields to be 53 | /// added to `Vendor` in future releases without it being a breaking change. 54 | /// 55 | /// 56 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq)] 57 | pub struct Vendor { 58 | pub name: String, 59 | pub version: Version, 60 | 61 | #[serde(default = "PhantomData::default")] 62 | _private_guard: PhantomData<()>, 63 | } 64 | 65 | /// `Version` is a string specifying a version. 66 | /// 67 | /// # Summary 68 | /// 69 | /// * `Version` thinly wraps a string but may be parsed into its major, minor, 70 | /// and patch numbers. 71 | /// 72 | /// * `Version` implements `Deserialize`. 73 | /// 74 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq)] 75 | pub struct Version(String); 76 | 77 | impl std::fmt::Display for Version { 78 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 79 | f.write_str(&self.0) 80 | } 81 | } 82 | 83 | impl AsRef for Version { 84 | fn as_ref(&self) -> &str { 85 | &self.0 86 | } 87 | } 88 | 89 | impl From for String { 90 | fn from(v: Version) -> Self { 91 | v.0 92 | } 93 | } 94 | 95 | impl From for Version { 96 | fn from(s: String) -> Self { 97 | Version(s) 98 | } 99 | } 100 | 101 | impl<'a> From<&'a str> for Version { 102 | fn from(s: &'a str) -> Self { 103 | Version(String::from(s)) 104 | } 105 | } 106 | 107 | impl Version { 108 | /// Tries to obtain the major, minor, and patch numbers from the version 109 | /// string. 110 | pub fn triple(&self) -> Option<(u64, u64, u64)> { 111 | 112 | const BASE: u32 = 10; 113 | 114 | let parts = self.0 115 | .split(|c: char| !c.is_digit(BASE)) 116 | .map(|s| { 117 | u64::from_str_radix(s, BASE).map(|x| Some(x)).unwrap_or( 118 | None, 119 | ) 120 | }) 121 | .take(3) 122 | .collect::>(); 123 | 124 | if parts.len() < 3 || parts.iter().any(|&x| x.is_none()) { 125 | return None; 126 | } 127 | 128 | Some((parts[0].unwrap(), parts[1].unwrap(), parts[2].unwrap())) 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | use serde_json; 136 | use std::marker::PhantomData; 137 | 138 | #[test] 139 | fn version_parses_triple() { 140 | assert_eq!(Version::from("1.6.1").triple(), Some((1, 6, 1))); 141 | 142 | // E.g., the Homebrew vendor appends an extra number onto their version. 143 | assert_eq!(Version::from("1.6.1_1").triple(), Some((1, 6, 1))); 144 | 145 | assert_eq!(Version::from("obviously_bad").triple(), None); 146 | } 147 | 148 | #[test] 149 | fn root_deserializes_ok() { 150 | 151 | let source = r#"{ 152 | "couchdb": "Welcome", 153 | "uuid": "0762dcce5f0d7f6f79157f852186f149", 154 | "version": "1.6.1", 155 | "vendor": { 156 | "name": "Homebrew", 157 | "version": "1.6.1_9" 158 | } 159 | }"#; 160 | 161 | let expected = Root { 162 | couchdb: String::from("Welcome"), 163 | uuid: Uuid::parse_str("0762dcce5f0d7f6f79157f852186f149").unwrap(), 164 | vendor: Vendor { 165 | name: String::from("Homebrew"), 166 | version: Version::from("1.6.1_9"), 167 | _private_guard: PhantomData, 168 | }, 169 | version: Version::from("1.6.1"), 170 | _private_guard: PhantomData, 171 | }; 172 | 173 | let got: Root = serde_json::from_str(source).unwrap(); 174 | assert_eq!(got, expected); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/testing/fake_server.rs: -------------------------------------------------------------------------------- 1 | use {Error, regex, std, tempdir}; 2 | 3 | // RAII wrapper for a child process that kills the process when dropped. 4 | struct AutoKillProcess(std::process::Child); 5 | 6 | impl Drop for AutoKillProcess { 7 | fn drop(&mut self) { 8 | let AutoKillProcess(ref mut process) = *self; 9 | process.kill().unwrap(); 10 | process.wait().unwrap(); 11 | } 12 | } 13 | 14 | /// `FakeServer` manages a CouchDB server process for application testing. 15 | /// 16 | /// # Summary 17 | /// 18 | /// * `FakeServer` is an RAII-wrapper for an external CouchDB server process 19 | /// useful for testing. 20 | /// 21 | /// * The external CouchDB process's underlying storage persists to the system's 22 | /// default temporary directory (e.g., `/tmp`) and is deleted when the 23 | /// `FakeServer` instance drops. 24 | /// 25 | /// # Remarks 26 | /// 27 | /// `FakeServer` is a fake, not a mock, meaning an application may use it to 28 | /// send HTTP requests to a real CouchDB server and receive real responses. 29 | /// Consequently, this means that CouchDB must be installed on the local machine 30 | /// in order to use `FakeServer`. 31 | /// 32 | /// The CouchDB server will open an unused port on the local machine. The 33 | /// application may obtain the server's exact address via the `FakeServer::url` 34 | /// method. 35 | /// 36 | /// The CouchDB server remains up and running for the lifetime of the 37 | /// `FakeServer` instance. When the instance drops, the server shuts down and 38 | /// all of its data are deleted. 39 | /// 40 | /// # Example 41 | /// 42 | /// ```rust 43 | /// extern crate couchdb; 44 | /// extern crate reqwest; 45 | /// 46 | /// let server_url = { 47 | /// let server = match couchdb::testing::FakeServer::new() { 48 | /// Ok(x) => x, 49 | /// Err(e) => { 50 | /// println!("Is CouchDB installed locally? ({})", e); 51 | /// return; 52 | /// } 53 | /// }; 54 | /// 55 | /// let mut response = reqwest::get(server.url()).unwrap(); 56 | /// assert!(response.status().is_success()); 57 | /// 58 | /// let root: couchdb::Root = response.json().unwrap(); 59 | /// println!("CouchDB welcome message: {}", root.couchdb); 60 | /// 61 | /// server.url().to_string() 62 | /// 63 | /// // Server shuts down when `server` goes out of scope. 64 | /// }; 65 | /// 66 | /// // The server is now shut down, so the client request fails. 67 | /// reqwest::get(&server_url).unwrap_err(); 68 | /// ``` 69 | /// 70 | pub struct FakeServer { 71 | // Rust drops structure fields in forward order, not reverse order. The 72 | // child process must exit before we remove the temporary directory. 73 | _process: AutoKillProcess, 74 | _tmp_root: tempdir::TempDir, 75 | url: String, 76 | } 77 | 78 | impl FakeServer { 79 | /// Spawns a CouchDB server process for testing. 80 | pub fn new() -> Result { 81 | 82 | let tmp_root = try!(tempdir::TempDir::new("couchdb_test").map_err(|e| { 83 | Error::from(( 84 | "Failed to create temporary directory for CouchDB server", 85 | e, 86 | )) 87 | })); 88 | 89 | { 90 | use std::io::Write; 91 | let path = tmp_root.path().join("couchdb.conf"); 92 | let mut f = try!(std::fs::File::create(&path).map_err(|e| { 93 | Error::from(("Failed to open CouchDB server configuration file", e)) 94 | })); 95 | try!( 96 | f.write_all( 97 | b"[couchdb]\n\ 98 | database_dir = var\n\ 99 | uri_file = couchdb.uri\n\ 100 | view_index_dir = view\n\ 101 | \n\ 102 | [log]\n\ 103 | file = couchdb.log\n\ 104 | \n\ 105 | [httpd]\n\ 106 | port = 0\n\ 107 | ", 108 | ).map_err(|e| { 109 | Error::from(("Failed to write CouchDB server configuration file", e)) 110 | }) 111 | ); 112 | } 113 | 114 | let child = try!(new_test_server_command(&tmp_root).spawn().map_err(|e| { 115 | Error::from(("Failed to spawn CouchDB server process", e)) 116 | })); 117 | let mut process = AutoKillProcess(child); 118 | 119 | let (tx, rx) = std::sync::mpsc::channel(); 120 | let mut process_out; 121 | { 122 | let AutoKillProcess(ref mut process) = process; 123 | let stdout = std::mem::replace(&mut process.stdout, None).unwrap(); 124 | process_out = std::io::BufReader::new(stdout); 125 | } 126 | 127 | let t = std::thread::spawn(move || { 128 | 129 | let re = regex::Regex::new(r"Apache CouchDB has started on (http.*)").unwrap(); 130 | let mut line = String::new(); 131 | 132 | loop { 133 | use std::io::BufRead; 134 | line.clear(); 135 | process_out.read_line(&mut line).unwrap(); 136 | let line = line.trim_right(); 137 | match re.captures(line) { 138 | None => (), 139 | Some(caps) => { 140 | tx.send(caps.get(1).unwrap().as_str().to_owned()).unwrap(); 141 | 142 | // TODO: Instead of breaking out of the loop, continue 143 | // looking for URL updates due to `POST /_restart`. 144 | 145 | break; 146 | } 147 | } 148 | } 149 | 150 | // Drain stdout. 151 | loop { 152 | use std::io::BufRead; 153 | line.clear(); 154 | process_out.read_line(&mut line).unwrap(); 155 | if line.is_empty() { 156 | break; 157 | } 158 | } 159 | }); 160 | 161 | // Wait for the CouchDB server to start its HTTP service. 162 | let url = try!(rx.recv().map_err(|e| { 163 | t.join().unwrap_err(); 164 | Error::from(( 165 | "Failed to obtain URL from CouchDB server", 166 | std::io::Error::new(std::io::ErrorKind::Other, e), 167 | )) 168 | })); 169 | 170 | Ok(FakeServer { 171 | _process: process, 172 | _tmp_root: tmp_root, 173 | url: url, 174 | }) 175 | } 176 | 177 | /// Returns the CouchDB server's URL. 178 | pub fn url(&self) -> &str { 179 | &self.url 180 | } 181 | } 182 | 183 | #[cfg(any(windows))] 184 | fn new_test_server_command(tmp_root: &tempdir::TempDir) -> std::process::Command { 185 | 186 | // Getting a one-shot CouchDB server running on Windows is tricky: 187 | // http://stackoverflow.com/questions/11812365/how-to-use-a-custom-couch-ini-on-windows 188 | // 189 | // TODO: Support CouchDB being installed in a non-default directory. 190 | 191 | let couchdb_dir = "c:/program files (x86)/apache software foundation/couchdb"; 192 | 193 | let erl = format!("{}/bin/erl", couchdb_dir); 194 | let default_ini = format!("{}/etc/couchdb/default.ini", couchdb_dir); 195 | let local_ini = format!("{}/etc/couchdb/local.ini", couchdb_dir); 196 | 197 | let mut c = std::process::Command::new(erl); 198 | c.arg("-couch_ini"); 199 | c.arg(default_ini); 200 | c.arg(local_ini); 201 | c.arg("couchdb.conf"); 202 | c.arg("-s"); 203 | c.arg("couch"); 204 | c.current_dir(tmp_root.path()); 205 | c.stdout(std::process::Stdio::piped()); 206 | c 207 | } 208 | 209 | #[cfg(any(not(windows)))] 210 | fn new_test_server_command(tmp_root: &tempdir::TempDir) -> std::process::Command { 211 | let mut c = std::process::Command::new("couchdb"); 212 | c.arg("-a"); 213 | c.arg("couchdb.conf"); 214 | c.current_dir(tmp_root.path()); 215 | c.stdout(std::process::Stdio::piped()); 216 | c 217 | } 218 | -------------------------------------------------------------------------------- /src/testing/mod.rs: -------------------------------------------------------------------------------- 1 | //! The `testing` module provides tools for applications to test their use of 2 | //! CouchDB. 3 | 4 | mod fake_server; 5 | 6 | pub use self::fake_server::FakeServer; 7 | --------------------------------------------------------------------------------