├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rb ├── doc ├── CNAME ├── _templates │ └── page.template ├── css │ ├── hack.css │ ├── highlight.css │ └── site.css └── prism.js ├── lazers-changes-stream ├── .gitignore ├── Cargo.toml ├── examples │ └── simple_parsing.rs ├── src │ ├── changes_stream.md │ ├── lib.md │ └── types │ │ ├── change.md │ │ ├── changes_lines.md │ │ ├── document.md │ │ ├── last_seq.md │ │ ├── mod.md │ │ └── revision.md └── tango-build.rs ├── lazers-hyper-client ├── Cargo.toml ├── src │ ├── lib.md │ └── types │ │ ├── database_info.md │ │ ├── document_created.md │ │ ├── error.md │ │ └── mod.md ├── tango-build.rs └── tests │ └── tests.rs ├── lazers-liblazers ├── Cargo.toml ├── build.rs ├── src │ └── lib.md └── tests │ └── test.c ├── lazers-replicator ├── Cargo.toml ├── src │ ├── documents.md │ ├── errors.md │ ├── find_common_ancestry.md │ ├── get_peers_information.md │ ├── lib.md │ ├── utils.md │ └── verify_peers.md ├── tango-build.rs └── tests │ └── http_test.rs ├── lazers-traits ├── Cargo.toml ├── src │ ├── decorations.md │ ├── lib.md │ ├── prelude.md │ └── result.md └── tango-build.rs ├── rustfmt.toml └── update_page.sh /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | */src/**/*rs 4 | **/*.stamp 5 | doc 6 | **/*.bk 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | services: couchdb 3 | sudo: false 4 | rust: 5 | - 1.15.0 6 | - nightly 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | env: 11 | - PROJECT=lazers-traits 12 | - PROJECT=lazers-hyper-client 13 | - PROJECT=lazers-changes-stream 14 | - PROJECT=lazers-replicator 15 | - PROJECT=lazers-liblazers 16 | script: cd $PROJECT; cargo test --verbose 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Getting started 4 | 5 | If you don't have it installed already, install Rust and CouchDB: 6 | 7 | * [Rust](https://doc.rust-lang.org/book/getting-started.html) 8 | * [CouchDB](http://couchdb.apache.org/) 9 | 10 | Make sure CouchDB is running. 11 | 12 | Clone [the repository](https://github.com/skade/lazers). 13 | 14 | Navigate the subdirectory (e.g. `lazers-hyper-client`) you want to work in and type: 15 | 16 | ```bash 17 | $ cargo build 18 | $ cargo test 19 | ``` 20 | 21 | If projects rely on each other (e.g. lazers-hyper-client depends on lazers-traits), the depending project will be built automatically. 22 | 23 | That is all, you are ready to go! 24 | 25 | ## State of the project 26 | 27 | The project is currently experimental. Expect API changes. 28 | 29 | ## Opportunities 30 | 31 | The project is currently experimental. Think something is not a good idea? Change it! 32 | 33 | ## Do I need to know Rust? 34 | 35 | No, but you should obviously bring interest. I'll happily walk you through the issues you might experience on the way to your first patch. 36 | 37 | ## Do I need to know CouchDB? 38 | 39 | No. CouchDB is well-documented and currently, most of the work is in implementing the public interface. 40 | 41 | ## Expected quality of patches 42 | 43 | I'm fine with anything that compiles. Even if you break the tests, make sure you submit the patch, we can get started from there. 44 | 45 | ## Using Tango 46 | 47 | I'm rather convinced that I want to stick to tango. Here's how to work with it: 48 | 49 | Tango is working in a bidirectional fashion. Whenever you run `cargo build`, it generates Rust sourcecode from the markdown input. You can then edit _any of both files_, as long as you keep the other untouched. On the next `cargo build`, Tango will update the unchanged file. So you can work with whatever file you want. 50 | 51 | Just make sure you later commit the right one. 52 | 53 | The benefit of tango is the extensive long-form documentation it provides. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["lazers-changes-stream", "lazers-traits", "lazers-replicator", "lazers-liblazers", "lazers-hyper-client"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laze RS 2 | 3 | What could become a CouchDB toolkit in Rust. 4 | 5 | Uses literate programming through [tango](https://github.com/pnkfelix/tango) and tries hard to build nice APIs. 6 | 7 | [It is pronouced "Laze RS"](https://en.wiktionary.org/wiki/laze). 8 | 9 | See [CONTRIBUTING](http://laze.rs/contributing.html) for a contribution guide. 10 | 11 | ## Project state 12 | 13 | This is currently mostly API experiments. I'm searching for help 14 | with many of them. 15 | 16 | Contributors welcome! 17 | 18 | Find a list of issues [here](https://github.com/skade/lazers/issues). 19 | 20 | ## Notable specialties 21 | 22 | ### Use of Tango 23 | 24 | Tango is used to provide richer documentation by using literate programming to make writing descriptions the default. 25 | 26 | ### Documentation-driven development 27 | 28 | Features, as experimental as they may be, should never be committed without extensive documentation. 29 | 30 | This hasn't been followed through in the past and has been fixed, but there might be spots. These spots shouldn't happen again. 31 | 32 | ### Use of decorated results 33 | 34 | Decorated results allow a form of result chaining that hides error handling until the very last step. 35 | 36 | See [yakshav.es/decorating-results/](http://yakshav.es/decorating-results/) about how decorated results work. 37 | 38 | ## Current Setup 39 | 40 | The project currently consists of the following crates: 41 | 42 | * [lazers-traits](http://laze.rs/lazers-traits/src/lib/): A set of traits without implementations that describe the general interface towards a CouchDB(-like) database. It defines error and result types and interactions for writing and reading documents. 43 | 44 | * [lazers-hyper-client](http://laze.rs/lazers-hyper-client/src/lib/): A client implementing lazers-traits for a remote CouchDB using hyper. Currently also serves as an implementation example for the interface described in lazers-traits. 45 | 46 | * [lazers-changes-stream](http://laze.rs/lazers-changes-stream/src/lib/): An implementation of the CouchDB changes stream protocol. It is generic over any `Read` interface, so it depends on no http client. 47 | 48 | * [lazers-replicator](http://laze.rs/lazers-replicator/src/lib/): A (currently unfinished) implementation of the CouchDB replication protocol. Important features of the base libraries were missing, but it is still around as a placeholder or as a way to start. 49 | 50 | * [lazers-liblazers](http://laze.rs/lazers-liblazers/src/lib/): A C interface to the library. Still figuring a good way to do this. This interface is important, see [Long-Term Goals] 51 | 52 | ## Notably missing 53 | 54 | * A second implementation of the lazers-traits interface for a local database 55 | * A good way to compile the md source documents into a doc site 56 | * A switch to tokio/futures-rs. I think this is the... future, so at some point the whole interface would need to be switched over. 57 | 58 | ## Long-term goals 59 | 60 | * Provide a way to use Laze RS on iOS and Android similar to CouchBase lite. 61 | * Provide bindings towards several programming languages like Ruby/Python 62 | 63 | ## Constraints 64 | 65 | The library itself uses plain Serde for serialisation and deserialisation, this means some boilerplate work is required. 66 | 67 | ## Credits 68 | 69 | Hat tip to https://lobste.rs/u/gsquire for the final name idea. 70 | -------------------------------------------------------------------------------- /build.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'fileutils' 3 | 4 | Dir["lazers*/**/*md"].each do |source| 5 | dir, _ = Pathname.new(source).split 6 | name = Pathname.new(source).basename(".md") 7 | 8 | FileUtils.mkdir_p "doc/#{dir}/#{name}" 9 | system("pandoc", 10 | source, 11 | "--smart", 12 | "--template", 13 | "doc/_templates/page.template", 14 | "-s", 15 | "-o", 16 | "doc/#{dir}/#{name}/index.html") 17 | end 18 | 19 | system("pandoc", 20 | "README.md", 21 | "--smart", 22 | "--template", 23 | "doc/_templates/page.template", 24 | "-s", 25 | "-o", 26 | "doc/index.html") 27 | 28 | system("pandoc", 29 | "CONTRIBUTING.md", 30 | "--smart", 31 | "--template", 32 | "doc/_templates/page.template", 33 | "-s", 34 | "-o", 35 | "doc/contributing.html") -------------------------------------------------------------------------------- /doc/CNAME: -------------------------------------------------------------------------------- 1 | laze.rs 2 | -------------------------------------------------------------------------------- /doc/_templates/page.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Laze RS 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | $body$ 16 | 17 | top 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /doc/css/hack.css: -------------------------------------------------------------------------------- 1 | html{font-size:12px}*{box-sizing:border-box;text-rendering:geometricPrecision}body{font-size:1rem;line-height:1.5rem;margin:0;font-family:Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif}fieldset{border:none;padding:0;margin:0}pre{padding:2rem;margin:1.75rem 0;background-color:#fff;border:1px solid #ccc;overflow:auto}code[class*=language-],pre[class*=language-],pre code{font-weight:100;text-shadow:none;margin:1.75rem 0}a{cursor:pointer;color:#ff2e88;text-decoration:none;border-bottom:1px solid #ff2e88}a:hover{background-color:#ff2e88;color:#fff}body::-webkit-scrollbar{background-color:transparent;width:12px}body::-webkit-scrollbar-thumb{border-radius:0;background-color:#ccc}body::-webkit-scrollbar-thumb:hover{background-color:#b1b1b1}body.dark::-webkit-scrollbar-thumb{border-radius:0;background-color:rgba(95,95,95,.78)}body.dark::-webkit-scrollbar-thumb:hover{background-color:#525252}.grid{display:-ms-flexbox;display:flex}.grid.\-top{-ms-flex-align:start;-ms-grid-row-align:flex-start;align-items:flex-start}.grid.\-middle{-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}.grid.\-bottom{-ms-flex-align:end;-ms-grid-row-align:flex-end;align-items:flex-end}.grid.\-stretch{-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch}.grid.\-baseline{-ms-flex-align:baseline;-ms-grid-row-align:baseline;align-items:baseline}.grid.\-left{-ms-flex-pack:start;justify-content:flex-start}.grid.\-center{-ms-flex-pack:center;justify-content:center}.grid.\-right{-ms-flex-pack:end;justify-content:flex-end}.grid.\-between{-ms-flex-pack:justify;justify-content:space-between}.grid.\-around{-ms-flex-pack:distribute;justify-content:space-around}.cell{-ms-flex:1;flex:1;box-sizing:border-box}.cell.\-1of12,.cell.\-2of12{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%}.cell.\-3of12{-ms-flex:0 0 25%;flex:0 0 25%}.cell.\-4of12{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%}.cell.\-5of12{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%}.cell.\-6of12{-ms-flex:0 0 50%;flex:0 0 50%}.cell.\-7of12{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%}.cell.\-8of12{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%}.cell.\-9of12{-ms-flex:0 0 75%;flex:0 0 75%}.cell.\-10of12{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%}.cell.\-11of12{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%}@media (max-width:768px){.grid{-ms-flex-direction:column;flex-direction:column}.cell{-ms-flex:0 0 auto;flex:0 0 auto}}.hack{word-wrap:break-word}.hack,.hack blockquote,.hack code,.hack em,.hack h1,.hack h2,.hack h3,.hack h4,.hack h5,.hack h6,.hack strong{font-size:1rem;line-height:20px;font-style:normal;font-family:Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif}.hack blockquote,.hack code,.hack footer,.hack h1,.hack h2,.hack h3,.hack h4,.hack h5,.hack h6,.hack header,.hack li,.hack ol,.hack p,.hack section,.hack ul{float:none;margin:0;padding:0}.hack blockquote,.hack h1,.hack ol,.hack p,.hack ul{margin-top:20px;margin-bottom:20px}.hack h1{position:relative;display:inline-block;display:table-cell;padding:20px 0 40px;margin:0;overflow:hidden}.hack h1:after{content:"====================================================================================================";position:absolute;bottom:10px;left:0}.hack h1+*{margin-top:0}.hack h2,.hack h3,.hack h4,.hack h5,.hack h6{position:relative;margin-bottom:1.75rem}.hack h1,.hack h2{line-height:30px}.hack h2:before,.hack h3:before,.hack h4:before,.hack h5:before,.hack h6:before{content:"## ";display:inline}.hack h3:before{content:"### "}.hack h4:before{content:"#### "}.hack h5:before{content:"##### "}.hack h6:before{content:"###### "}.hack li{position:relative;display:block;padding-left:20px}.hack li:after{position:absolute;top:0;left:0}.hack ul>li:after{content:"-"}.hack ol{counter-reset:a}.hack ol>li:after{content:counter(a) ".";counter-increment:a}.hack blockquote{position:relative;padding-left:17px;padding-left:2ch;overflow:hidden}.hack blockquote:after{content:">\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>";white-space:pre;position:absolute;top:0;left:0;line-height:20px}.hack em:after,.hack em:before{content:"*";display:inline}.hack pre code:after,.hack pre code:before{content:''}.hack code{font-weight:700}.hack code:after,.hack code:before{content:"`";display:inline}.hack hr{position:relative;height:20px;font-size:0;line-height:0;overflow:hidden;border:0;margin-bottom:20px}.hack hr:after{content:"----------------------------------------------------------------------------------------------------";position:absolute;top:0;left:0;line-height:20px;width:100%;word-wrap:break-word}@-moz-document url-prefix(){.hack h1{display:block}}.hack-ones ol>li:after{content:"1."}p{margin:0 0 1.75rem}.container{max-width:70rem}.container,.container-fluid{margin:0 auto;padding:0 1rem}.inner{padding:1rem}.inner2x{padding:2rem}.pull-left{float:left}.pull-right{float:right}.progress-bar{height:8px;opacity:.8;background-color:#ccc;margin-top:12px}.progress-bar.progress-bar-show-percent{margin-top:38px}.progress-bar-filled{background-color:gray;height:100%;transition:width .3s ease;position:relative;width:0}.progress-bar-filled:before{content:'';border:6px solid transparent;border-top-color:gray;position:absolute;top:-12px;right:-6px}.progress-bar-filled:after{color:gray;content:attr(data-filled);display:block;font-size:12px;position:absolute;border:6px solid transparent;top:-38px;right:0;-ms-transform:translateX(50%);transform:translateX(50%)}table{width:100%;border-collapse:collapse;margin:1.75rem 0;color:#778087}table td,table th{vertical-align:top;border:1px solid #ccc;line-height:15px;padding:10px}table thead th{font-size:10px}table tbody td:first-child{font-weight:700;color:#333}.form{width:30rem}.form-group{margin-bottom:1.75rem;overflow:auto}.form-group label{border-bottom:2px solid #ccc;color:#333;width:10rem;display:inline-block;height:38px;line-height:38px;padding:0;float:left;position:relative}.form-group.form-success label{color:#4caf50!important;border-color:#4caf50!important}.form-group.form-warning label{color:#ff9800!important;border-color:#ff9800!important}.form-group.form-error label{color:#f44336!important;border-color:#f44336!important}.form-control{outline:none;border:none;border-bottom:2px solid #ccc;padding:.5rem 0;width:20rem;height:38px;background-color:transparent}.form-control:focus{border-color:#555}.form-group.form-textarea label:after{position:absolute;content:'';width:2px;background-color:#fff;right:-2px;top:0;bottom:0}textarea.form-control{height:auto;resize:none;padding:1rem 0;border-bottom:2px solid #ccc;border-left:2px solid #ccc;padding:.5rem}select.form-control{border-radius:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none}.help-block{color:#999;margin-top:.5rem}.form-actions{margin-bottom:1.75rem}.btn{cursor:pointer;outline:none;padding:.65rem 2rem;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative;z-index:1}.btn:active{box-shadow:inset 0 1px 3px rgba(0,0,0,.12)}.btn.btn-ghost{border-color:#757575;color:#757575;background-color:transparent}.btn.btn-ghost:focus,.btn.btn-ghost:hover{border-color:#424242;color:#424242;z-index:2}.btn.btn-ghost:hover{background-color:transparent}.btn-block{width:100%;display:block}.btn-default{color:#fff;background-color:#e0e0e0;border:1px solid #e0e0e0;color:#333}.btn-default:focus:not(.btn-ghost),.btn-default:hover{background-color:#dcdcdc;border-color:#dcdcdc}.btn-success{color:#fff;background-color:#4caf50;border:1px solid #4caf50}.btn-success:focus:not(.btn-ghost),.btn-success:hover{background-color:#43a047;border-color:#43a047}.btn-success.btn-ghost{border-color:#4caf50;color:#4caf50}.btn-success.btn-ghost:focus,.btn-success.btn-ghost:hover{border-color:#388e3c;color:#388e3c;z-index:2}.btn-error{color:#fff;background-color:#f44336;border:1px solid #f44336}.btn-error:focus:not(.btn-ghost),.btn-error:hover{background-color:#e53935;border-color:#e53935}.btn-error.btn-ghost{border-color:#f44336;color:#f44336}.btn-error.btn-ghost:focus,.btn-error.btn-ghost:hover{border-color:#d32f2f;color:#d32f2f;z-index:2}.btn-warning{color:#fff;background-color:#ff9800;border:1px solid #ff9800}.btn-warning:focus:not(.btn-ghost),.btn-warning:hover{background-color:#fb8c00;border-color:#fb8c00}.btn-warning.btn-ghost{border-color:#ff9800;color:#ff9800}.btn-warning.btn-ghost:focus,.btn-warning.btn-ghost:hover{border-color:#f57c00;color:#f57c00;z-index:2}.btn-info{color:#fff;background-color:#00bcd4;border:1px solid #00bcd4}.btn-info:focus:not(.btn-ghost),.btn-info:hover{background-color:#00acc1;border-color:#00acc1}.btn-info.btn-ghost{border-color:#00bcd4;color:#00bcd4}.btn-info.btn-ghost:focus,.btn-info.btn-ghost:hover{border-color:#0097a7;color:#0097a7;z-index:2}.btn-primary{color:#fff;background-color:#2196f3;border:1px solid #2196f3}.btn-primary:focus:not(.btn-ghost),.btn-primary:hover{background-color:#1e88e5;border-color:#1e88e5}.btn-primary.btn-ghost{border-color:#2196f3;color:#2196f3}.btn-primary.btn-ghost:focus,.btn-primary.btn-ghost:hover{border-color:#1976d2;color:#1976d2;z-index:2}.btn-group{overflow:auto}.btn-group .btn{float:left}.btn-group .btn-ghost:not(:first-child){margin-left:-1px}.card{border:1px solid #ccc}.card .card-header{color:#333;text-align:center;background-color:#ddd;padding:.5rem 0}.alert{color:#ccc;padding:1rem;border:1px solid #ccc;margin-bottom:1.75rem}.alert-success{color:#4caf50;border-color:#4caf50}.alert-error{color:#f44336;border-color:#f44336}.alert-info{color:#00bcd4;border-color:#00bcd4}.alert-warning{color:#ff9800;border-color:#ff9800}.media:not(:last-child){margin-bottom:1.25rem}.media-left{padding-right:1rem}.media-left,.media-right{display:table-cell;vertical-align:top}.media-right{padding-left:1rem}.media-body{display:table-cell;vertical-align:top}.media-heading{font-size:1.16667rem;font-weight:700}.media-content{margin-top:.3rem}.avatarholder,.placeholder{background-color:#f0f0f0;text-align:center;color:#b9b9b9;font-size:1rem;border:1px solid #f0f0f0}.avatarholder{width:48px;height:48px;line-height:46px;font-size:2rem;background-size:cover;background-position:50%;background-repeat:no-repeat}.avatarholder.rounded{border-radius:33px}.loading{height:20px;width:20px;animation:a .6s infinite linear;border:2px solid #e91e63;border-right-color:transparent;border-radius:50%}.btn .loading{display:inline-block;float:left;margin-right:.5rem;width:14px;height:14px}@keyframes a{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.menu{width:100%}.menu .menu-item{display:block;color:#616161;border-color:#616161}.menu .menu-item.active,.menu .menu-item:hover{color:#000;border-color:#000;background-color:transparent}@media screen and (max-width:768px){.form-group label{display:block;border-bottom:none;width:100%}.form-group.form-textarea label:after{display:none}.form-control{width:100%}textarea.form-control{border-left:none;padding:.5rem 0}pre::-webkit-scrollbar{height:3px}}@media screen and (max-width:480px){.form{width:100%}}@media screen and (min-width:768px){pre::-webkit-scrollbar{background-color:transparent;height:8px}pre::-webkit-scrollbar-thumb{border-radius:0;background-color:#ccc}pre::-webkit-scrollbar-thumb:hover{background-color:#b1b1b1}} -------------------------------------------------------------------------------- /doc/css/highlight.css: -------------------------------------------------------------------------------- 1 | code > span.kw { color: #268BD2; font-weight: bold; } 2 | code > span.dt { color: #268BD2; } 3 | code > span.dv { color: #D33682; } 4 | code > span.bn { color: #D33682; } 5 | code > span.fl { color: #D33682; } 6 | code > span.ch { color: #4070a0; } 7 | code > span.st { color: #2AA198; } 8 | code > span.co { color: #93A1A1; font-style: italic; } 9 | code > span.ot { color: #A57800; } 10 | code > span.al { color: #CB4B16; font-weight: bold; } 11 | code > span.fu { color: #268BD2; } 12 | code > span.er { color: #D30102; font-weight: bold; } 13 | -------------------------------------------------------------------------------- /doc/css/site.css: -------------------------------------------------------------------------------- 1 | .main { 2 | padding: 20px 10px; 3 | } 4 | 5 | .hack h1 { 6 | padding-top: 0; 7 | } 8 | 9 | .example { 10 | margin-bottom: 20px; 11 | } 12 | 13 | .example .btn { 14 | margin-bottom: 10px; 15 | } 16 | 17 | footer.footer { 18 | border-top: 1px solid #ccc; 19 | margin-top: 5rem; 20 | padding: 3rem 0; 21 | } 22 | 23 | .grid-example { 24 | padding: 0 2px; 25 | background-color: #ccc; 26 | margin-bottom: 1rem; 27 | } 28 | 29 | .grid-example .cell { 30 | text-align: center; 31 | border: 1px solid #ccc; 32 | padding: 4px 2px; 33 | color: #999; 34 | } 35 | 36 | .grid-example .cell .content { 37 | background-color: #ddd; 38 | } 39 | 40 | :not(pre) > code[class*="language-"], pre[class*="language-"] { 41 | background-color: transparent; 42 | font-weight: normal; 43 | } 44 | 45 | .hack, .hack blockquote, .hack em, .hack h1, .hack h2, .hack h3, .hack h4, .hack h5, .hack h6, .hack strong { 46 | font-family: system, -apple-system, BlinkMacSystemFont, 47 | "Helvetica Neue", "Lucida Grande"; 48 | font-size: 1.4em; 49 | line-height: 1.5em; 50 | } -------------------------------------------------------------------------------- /doc/prism.js: -------------------------------------------------------------------------------- 1 | 2 | /* ********************************************** 3 | Begin prism-core.js 4 | ********************************************** */ 5 | 6 | var _self = (typeof window !== 'undefined') 7 | ? window // if in browser 8 | : ( 9 | (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) 10 | ? self // if in worker 11 | : {} // if in node js 12 | ); 13 | 14 | /** 15 | * Prism: Lightweight, robust, elegant syntax highlighting 16 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 17 | * @author Lea Verou http://lea.verou.me 18 | */ 19 | 20 | var Prism = (function(){ 21 | 22 | // Private helper vars 23 | var lang = /\blang(?:uage)?-(\w+)\b/i; 24 | var uniqueId = 0; 25 | 26 | var _ = _self.Prism = { 27 | util: { 28 | encode: function (tokens) { 29 | if (tokens instanceof Token) { 30 | return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias); 31 | } else if (_.util.type(tokens) === 'Array') { 32 | return tokens.map(_.util.encode); 33 | } else { 34 | return tokens.replace(/&/g, '&').replace(/ text.length) { 302 | // Something went terribly wrong, ABORT, ABORT! 303 | break tokenloop; 304 | } 305 | 306 | if (str instanceof Token) { 307 | continue; 308 | } 309 | 310 | pattern.lastIndex = 0; 311 | 312 | var match = pattern.exec(str), 313 | delNum = 1; 314 | 315 | // Greedy patterns can override/remove up to two previously matched tokens 316 | if (!match && greedy && i != strarr.length - 1) { 317 | pattern.lastIndex = pos; 318 | match = pattern.exec(text); 319 | if (!match) { 320 | break; 321 | } 322 | 323 | var from = match.index + (lookbehind ? match[1].length : 0), 324 | to = match.index + match[0].length, 325 | k = i, 326 | p = pos; 327 | 328 | for (var len = strarr.length; k < len && p < to; ++k) { 329 | p += strarr[k].length; 330 | // Move the index i to the element in strarr that is closest to from 331 | if (from >= p) { 332 | ++i; 333 | pos = p; 334 | } 335 | } 336 | 337 | /* 338 | * If strarr[i] is a Token, then the match starts inside another Token, which is invalid 339 | * If strarr[k - 1] is greedy we are in conflict with another greedy pattern 340 | */ 341 | if (strarr[i] instanceof Token || strarr[k - 1].greedy) { 342 | continue; 343 | } 344 | 345 | // Number of tokens to delete and replace with the new match 346 | delNum = k - i; 347 | str = text.slice(pos, p); 348 | match.index -= pos; 349 | } 350 | 351 | if (!match) { 352 | continue; 353 | } 354 | 355 | if(lookbehind) { 356 | lookbehindLength = match[1].length; 357 | } 358 | 359 | var from = match.index + lookbehindLength, 360 | match = match[0].slice(lookbehindLength), 361 | to = from + match.length, 362 | before = str.slice(0, from), 363 | after = str.slice(to); 364 | 365 | var args = [i, delNum]; 366 | 367 | if (before) { 368 | args.push(before); 369 | } 370 | 371 | var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias, match, greedy); 372 | 373 | args.push(wrapped); 374 | 375 | if (after) { 376 | args.push(after); 377 | } 378 | 379 | Array.prototype.splice.apply(strarr, args); 380 | } 381 | } 382 | } 383 | 384 | return strarr; 385 | }, 386 | 387 | hooks: { 388 | all: {}, 389 | 390 | add: function (name, callback) { 391 | var hooks = _.hooks.all; 392 | 393 | hooks[name] = hooks[name] || []; 394 | 395 | hooks[name].push(callback); 396 | }, 397 | 398 | run: function (name, env) { 399 | var callbacks = _.hooks.all[name]; 400 | 401 | if (!callbacks || !callbacks.length) { 402 | return; 403 | } 404 | 405 | for (var i=0, callback; callback = callbacks[i++];) { 406 | callback(env); 407 | } 408 | } 409 | } 410 | }; 411 | 412 | var Token = _.Token = function(type, content, alias, matchedStr, greedy) { 413 | this.type = type; 414 | this.content = content; 415 | this.alias = alias; 416 | // Copy of the full string this token was created from 417 | this.length = (matchedStr || "").length|0; 418 | this.greedy = !!greedy; 419 | }; 420 | 421 | Token.stringify = function(o, language, parent) { 422 | if (typeof o == 'string') { 423 | return o; 424 | } 425 | 426 | if (_.util.type(o) === 'Array') { 427 | return o.map(function(element) { 428 | return Token.stringify(element, language, o); 429 | }).join(''); 430 | } 431 | 432 | var env = { 433 | type: o.type, 434 | content: Token.stringify(o.content, language, parent), 435 | tag: 'span', 436 | classes: ['token', o.type], 437 | attributes: {}, 438 | language: language, 439 | parent: parent 440 | }; 441 | 442 | if (env.type == 'comment') { 443 | env.attributes['spellcheck'] = 'true'; 444 | } 445 | 446 | if (o.alias) { 447 | var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias]; 448 | Array.prototype.push.apply(env.classes, aliases); 449 | } 450 | 451 | _.hooks.run('wrap', env); 452 | 453 | var attributes = ''; 454 | 455 | for (var name in env.attributes) { 456 | attributes += (attributes ? ' ' : '') + name + '="' + (env.attributes[name] || '') + '"'; 457 | } 458 | 459 | return '<' + env.tag + ' class="' + env.classes.join(' ') + '"' + (attributes ? ' ' + attributes : '') + '>' + env.content + ''; 460 | 461 | }; 462 | 463 | if (!_self.document) { 464 | if (!_self.addEventListener) { 465 | // in Node.js 466 | return _self.Prism; 467 | } 468 | // In worker 469 | _self.addEventListener('message', function(evt) { 470 | var message = JSON.parse(evt.data), 471 | lang = message.language, 472 | code = message.code, 473 | immediateClose = message.immediateClose; 474 | 475 | _self.postMessage(_.highlight(code, _.languages[lang], lang)); 476 | if (immediateClose) { 477 | _self.close(); 478 | } 479 | }, false); 480 | 481 | return _self.Prism; 482 | } 483 | 484 | //Get current script and highlight 485 | var script = document.currentScript || [].slice.call(document.getElementsByTagName("script")).pop(); 486 | 487 | if (script) { 488 | _.filename = script.src; 489 | 490 | if (document.addEventListener && !script.hasAttribute('data-manual')) { 491 | if(document.readyState !== "loading") { 492 | if (window.requestAnimationFrame) { 493 | window.requestAnimationFrame(_.highlightAll); 494 | } else { 495 | window.setTimeout(_.highlightAll, 16); 496 | } 497 | } 498 | else { 499 | document.addEventListener('DOMContentLoaded', _.highlightAll); 500 | } 501 | } 502 | } 503 | 504 | return _self.Prism; 505 | 506 | })(); 507 | 508 | if (typeof module !== 'undefined' && module.exports) { 509 | module.exports = Prism; 510 | } 511 | 512 | // hack for components to work correctly in node.js 513 | if (typeof global !== 'undefined') { 514 | global.Prism = Prism; 515 | } 516 | 517 | 518 | /* ********************************************** 519 | Begin prism-markup.js 520 | ********************************************** */ 521 | 522 | Prism.languages.markup = { 523 | 'comment': //, 524 | 'prolog': /<\?[\w\W]+?\?>/, 525 | 'doctype': //i, 526 | 'cdata': //i, 527 | 'tag': { 528 | pattern: /<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i, 529 | inside: { 530 | 'tag': { 531 | pattern: /^<\/?[^\s>\/]+/i, 532 | inside: { 533 | 'punctuation': /^<\/?/, 534 | 'namespace': /^[^\s>\/:]+:/ 535 | } 536 | }, 537 | 'attr-value': { 538 | pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i, 539 | inside: { 540 | 'punctuation': /[=>"']/ 541 | } 542 | }, 543 | 'punctuation': /\/?>/, 544 | 'attr-name': { 545 | pattern: /[^\s>\/]+/, 546 | inside: { 547 | 'namespace': /^[^\s>\/:]+:/ 548 | } 549 | } 550 | 551 | } 552 | }, 553 | 'entity': /&#?[\da-z]{1,8};/i 554 | }; 555 | 556 | // Plugin to make entity title show the real entity, idea by Roman Komarov 557 | Prism.hooks.add('wrap', function(env) { 558 | 559 | if (env.type === 'entity') { 560 | env.attributes['title'] = env.content.replace(/&/, '&'); 561 | } 562 | }); 563 | 564 | Prism.languages.xml = Prism.languages.markup; 565 | Prism.languages.html = Prism.languages.markup; 566 | Prism.languages.mathml = Prism.languages.markup; 567 | Prism.languages.svg = Prism.languages.markup; 568 | 569 | 570 | /* ********************************************** 571 | Begin prism-css.js 572 | ********************************************** */ 573 | 574 | Prism.languages.css = { 575 | 'comment': /\/\*[\w\W]*?\*\//, 576 | 'atrule': { 577 | pattern: /@[\w-]+?.*?(;|(?=\s*\{))/i, 578 | inside: { 579 | 'rule': /@[\w-]+/ 580 | // See rest below 581 | } 582 | }, 583 | 'url': /url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i, 584 | 'selector': /[^\{\}\s][^\{\};]*?(?=\s*\{)/, 585 | 'string': { 586 | pattern: /("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/, 587 | greedy: true 588 | }, 589 | 'property': /(\b|\B)[\w-]+(?=\s*:)/i, 590 | 'important': /\B!important\b/i, 591 | 'function': /[-a-z0-9]+(?=\()/i, 592 | 'punctuation': /[(){};:]/ 593 | }; 594 | 595 | Prism.languages.css['atrule'].inside.rest = Prism.util.clone(Prism.languages.css); 596 | 597 | if (Prism.languages.markup) { 598 | Prism.languages.insertBefore('markup', 'tag', { 599 | 'style': { 600 | pattern: /()[\w\W]*?(?=<\/style>)/i, 601 | lookbehind: true, 602 | inside: Prism.languages.css, 603 | alias: 'language-css' 604 | } 605 | }); 606 | 607 | Prism.languages.insertBefore('inside', 'attr-value', { 608 | 'style-attr': { 609 | pattern: /\s*style=("|').*?\1/i, 610 | inside: { 611 | 'attr-name': { 612 | pattern: /^\s*style/i, 613 | inside: Prism.languages.markup.tag.inside 614 | }, 615 | 'punctuation': /^\s*=\s*['"]|['"]\s*$/, 616 | 'attr-value': { 617 | pattern: /.+/i, 618 | inside: Prism.languages.css 619 | } 620 | }, 621 | alias: 'language-css' 622 | } 623 | }, Prism.languages.markup.tag); 624 | } 625 | 626 | /* ********************************************** 627 | Begin prism-clike.js 628 | ********************************************** */ 629 | 630 | Prism.languages.clike = { 631 | 'comment': [ 632 | { 633 | pattern: /(^|[^\\])\/\*[\w\W]*?\*\//, 634 | lookbehind: true 635 | }, 636 | { 637 | pattern: /(^|[^\\:])\/\/.*/, 638 | lookbehind: true 639 | } 640 | ], 641 | 'string': { 642 | pattern: /(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, 643 | greedy: true 644 | }, 645 | 'class-name': { 646 | pattern: /((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i, 647 | lookbehind: true, 648 | inside: { 649 | punctuation: /(\.|\\)/ 650 | } 651 | }, 652 | 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/, 653 | 'boolean': /\b(true|false)\b/, 654 | 'function': /[a-z0-9_]+(?=\()/i, 655 | 'number': /\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i, 656 | 'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/, 657 | 'punctuation': /[{}[\];(),.:]/ 658 | }; 659 | 660 | 661 | /* ********************************************** 662 | Begin prism-javascript.js 663 | ********************************************** */ 664 | 665 | Prism.languages.javascript = Prism.languages.extend('clike', { 666 | 'keyword': /\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/, 667 | 'number': /\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/, 668 | // Allow for all non-ASCII characters (See http://stackoverflow.com/a/2008444) 669 | 'function': /[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i, 670 | 'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/ 671 | }); 672 | 673 | Prism.languages.insertBefore('javascript', 'keyword', { 674 | 'regex': { 675 | pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/, 676 | lookbehind: true, 677 | greedy: true 678 | } 679 | }); 680 | 681 | Prism.languages.insertBefore('javascript', 'string', { 682 | 'template-string': { 683 | pattern: /`(?:\\\\|\\?[^\\])*?`/, 684 | greedy: true, 685 | inside: { 686 | 'interpolation': { 687 | pattern: /\$\{[^}]+\}/, 688 | inside: { 689 | 'interpolation-punctuation': { 690 | pattern: /^\$\{|\}$/, 691 | alias: 'punctuation' 692 | }, 693 | rest: Prism.languages.javascript 694 | } 695 | }, 696 | 'string': /[\s\S]+/ 697 | } 698 | } 699 | }); 700 | 701 | if (Prism.languages.markup) { 702 | Prism.languages.insertBefore('markup', 'tag', { 703 | 'script': { 704 | pattern: /()[\w\W]*?(?=<\/script>)/i, 705 | lookbehind: true, 706 | inside: Prism.languages.javascript, 707 | alias: 'language-javascript' 708 | } 709 | }); 710 | } 711 | 712 | Prism.languages.js = Prism.languages.javascript; 713 | 714 | /* ********************************************** 715 | Begin prism-file-highlight.js 716 | ********************************************** */ 717 | 718 | (function () { 719 | if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) { 720 | return; 721 | } 722 | 723 | self.Prism.fileHighlight = function() { 724 | 725 | var Extensions = { 726 | 'js': 'javascript', 727 | 'py': 'python', 728 | 'rb': 'ruby', 729 | 'ps1': 'powershell', 730 | 'psm1': 'powershell', 731 | 'sh': 'bash', 732 | 'bat': 'batch', 733 | 'h': 'c', 734 | 'tex': 'latex' 735 | }; 736 | 737 | if(Array.prototype.forEach) { // Check to prevent error in IE8 738 | Array.prototype.slice.call(document.querySelectorAll('pre[data-src]')).forEach(function (pre) { 739 | var src = pre.getAttribute('data-src'); 740 | 741 | var language, parent = pre; 742 | var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i; 743 | while (parent && !lang.test(parent.className)) { 744 | parent = parent.parentNode; 745 | } 746 | 747 | if (parent) { 748 | language = (pre.className.match(lang) || [, ''])[1]; 749 | } 750 | 751 | if (!language) { 752 | var extension = (src.match(/\.(\w+)$/) || [, ''])[1]; 753 | language = Extensions[extension] || extension; 754 | } 755 | 756 | var code = document.createElement('code'); 757 | code.className = 'language-' + language; 758 | 759 | pre.textContent = ''; 760 | 761 | code.textContent = 'Loading…'; 762 | 763 | pre.appendChild(code); 764 | 765 | var xhr = new XMLHttpRequest(); 766 | 767 | xhr.open('GET', src, true); 768 | 769 | xhr.onreadystatechange = function () { 770 | if (xhr.readyState == 4) { 771 | 772 | if (xhr.status < 400 && xhr.responseText) { 773 | code.textContent = xhr.responseText; 774 | 775 | Prism.highlightElement(code); 776 | } 777 | else if (xhr.status >= 400) { 778 | code.textContent = '✖ Error ' + xhr.status + ' while fetching file: ' + xhr.statusText; 779 | } 780 | else { 781 | code.textContent = '✖ Error: File does not exist or is empty'; 782 | } 783 | } 784 | }; 785 | 786 | xhr.send(null); 787 | }); 788 | } 789 | 790 | }; 791 | 792 | document.addEventListener('DOMContentLoaded', self.Prism.fileHighlight); 793 | 794 | })(); 795 | -------------------------------------------------------------------------------- /lazers-changes-stream/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /lazers-changes-stream/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lazers-changes-stream" 3 | version = "0.1.0" 4 | authors = ["Florian Gilcher "] 5 | 6 | build = "tango-build.rs" 7 | 8 | [dependencies] 9 | serde = "0.9.0" 10 | serde_json = "0.9.0" 11 | serde_derive = "0.9.0" 12 | 13 | [dev-dependencies] 14 | hyper = "0.9.0" 15 | 16 | [build-dependencies] 17 | tango = "0.5.0" 18 | 19 | [lib] 20 | name = "lazers_changes_stream" 21 | path = "src/lib.rs" 22 | -------------------------------------------------------------------------------- /lazers-changes-stream/examples/simple_parsing.rs: -------------------------------------------------------------------------------- 1 | extern crate hyper; 2 | extern crate serde; 3 | extern crate serde_json as json; 4 | extern crate lazers_changes_stream; 5 | 6 | use lazers_changes_stream::changes_stream::ChangesStream; 7 | 8 | use hyper::Client; 9 | use hyper::header::Connection; 10 | 11 | fn main() { 12 | // Create a client. 13 | let client = Client::new(); 14 | 15 | // Creating an outgoing request. 16 | let res = client.get("http://localhost:5984/test/_changes?feed=continuous&include_docs=true") 17 | // set a header 18 | .header(Connection::close()) 19 | // let 'er go! 20 | .send().unwrap(); 21 | 22 | let stream: ChangesStream<_,json::Value> = ChangesStream::new(res); 23 | 24 | for change in stream.changes() { 25 | println!("{:?}", change); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/changes_stream.md: -------------------------------------------------------------------------------- 1 | # ChangesStream structure 2 | 3 | This library only handles the streaming changes stream, as specified in 4 | [TODO:link](). 5 | 6 | There are two kinds of forms this steam can take: The full stream and just 7 | changes. This module implements both of them. 8 | 9 | It works by consuming any input stream that implements `Read`, so it can be 10 | used on top of HTTP requests or just File input. 11 | 12 | ## Imports 13 | 14 | The module abstracts of raw streams that implement `Read`, but works 15 | linewise. 16 | This set of traits is needed to make that convenient. 17 | 18 | ```rust 19 | use std::io::{BufRead, BufReader, Read, Lines}; 20 | ``` 21 | 22 | The module abstracts over types as parsing information. They are not stored 23 | as 24 | data itself, so we need PhantomData fields here. 25 | 26 | ```rust 27 | use std::marker::PhantomData; 28 | ``` 29 | 30 | We decide to expect all types to be deserializable through Serde. We're not 31 | fully sure if simpler json libs could be allowed for the payload data. 32 | 33 | ```rust 34 | use serde::de::Deserialize; 35 | ``` 36 | 37 | We use both out own `Change` and `ChangesLines` types. `Change` is any 38 | document 39 | change, `ChangesLines` holds _all_ lines of the changes stream. 40 | 41 | ```rust 42 | use types::change::Change; 43 | use types::changes_lines::ChangesLines; 44 | ``` 45 | 46 | ## Definitions 47 | 48 | ### `ChangesStream` 49 | 50 | Provides reading of the CouchDB wire protocol from any stream that 51 | implements 52 | `Read`. 53 | 54 | It is generic over the kinds of documents included in the changes stream, as 55 | long as they implement "Deserialize". 56 | 57 | ```rust 58 | /// A handle on a changes stream. Provides reading of events from a source of 59 | /// type and holds type information about the documents expected. 60 | pub struct ChangesStream { 61 | source: Lines>, 62 | documents: PhantomData, 63 | } 64 | ``` 65 | ### `Full` 66 | 67 | The `Full` interface gives raw access to all events happening in the changes 68 | stream. This includes `LastSeq` documents, that are intended for internal 69 | tracking. 70 | 71 | ```rust 72 | /// Wrapper for a ChangesStream with full access. 73 | pub struct Full { 74 | stream: ChangesStream, 75 | } 76 | ``` 77 | 78 | ### `Changes` 79 | 80 | `Changes` only includes actual document changes and no protocol information. 81 | Most notably, it filters out `LastSeq` messages. 82 | 83 | ```rust 84 | /// Wrapper for a ChangesStream only returning `Change` documents. 85 | pub struct Changes { 86 | stream: Full, 87 | } 88 | ``` 89 | 90 | The implementation of the `ChangesStream` is intended as a proxy only, it is 91 | constructed and then the user selects if the full stream or only changes are 92 | wanted. Folding `Full` and `ChangesStream` into one was considered, but not 93 | used as this provides a symmetric interface, even though `Changes` 94 | internally 95 | relies on full. 96 | 97 | ```rust 98 | impl ChangesStream { 99 | /// Construct a new changes stream out of every `read` source. 100 | /// `Documents` needs to be any deserializable type. 101 | pub fn new(source: Source) -> ChangesStream { 102 | ChangesStream { 103 | source: BufReader::new(source).lines(), 104 | documents: PhantomData, 105 | } 106 | } 107 | 108 | /// Get an iterator to iterate over the full changes stream, including 109 | /// control events. 110 | pub fn full(self) -> Full { 111 | Full { stream: self } 112 | } 113 | 114 | /// Get an iterator to just read the changes out of the stream, without 115 | /// control events. 116 | pub fn changes(self) -> Changes { 117 | Changes { stream: self.full() } 118 | } 119 | } 120 | ``` 121 | 122 | ### Iterator implementations 123 | 124 | The iterator implementations are rather straight forward, with `Full` 125 | delegating 126 | to `ChangesLines` for parsing and unwrapping its results. 127 | 128 | Note that this implementation silently eats errors (including connection 129 | errors!) currently. 130 | 131 | ```rust 132 | impl Iterator for Full { 133 | type Item = ChangesLines; 134 | 135 | #[inline] 136 | fn next(&mut self) -> Option> { 137 | if let Some(elem) = self.stream.source.next() { 138 | elem.ok() 139 | .iter() 140 | .filter_map(|line| ChangesLines::parse(line).ok()) 141 | .nth(0) 142 | } else { 143 | None 144 | } 145 | 146 | } 147 | } 148 | 149 | impl Iterator for Changes { 150 | type Item = Change; 151 | 152 | #[inline] 153 | fn next(&mut self) -> Option> { 154 | if let Some(next) = self.stream.next() { 155 | next.to_change() 156 | } else { 157 | None 158 | } 159 | } 160 | } 161 | ``` 162 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/lib.md: -------------------------------------------------------------------------------- 1 | # lazers-changes-stream 2 | 3 | lazers-changes-stream is an implementation of the streaming couchdb changes 4 | protocol. It is standalone, and independent of the stream kind. This means 5 | lazers-changes-stream doesn't require HTTP interaction. 6 | lazers-changes-stream 7 | works with both in `include_docs=true` mode and without. 8 | 9 | lazers-changes-steam uses serde for serialization and deserialization of 10 | data. 11 | It is generic over the output data, so it deserializes into any 12 | user-requested 13 | data format, including simple JSON. 14 | 15 | ## Dependencies 16 | 17 | `serde` and `serde_json` are used for deserialisation. `serde_derive` is used for easy derivation of deserialisable types. 18 | 19 | ```rust 20 | extern crate serde; 21 | extern crate serde_json; 22 | #[macro_use] extern crate serde_derive; 23 | ``` 24 | 25 | ## Exposed modules 26 | 27 | ### `types` 28 | 29 | This module defines all types used for reading the CouchDB changes protocol. 30 | Examples for this are the Change type that wraps a changed document and or 31 | the 32 | `LastSeq` type, which describes the last sequence number read. 33 | 34 | ```rust 35 | pub mod types; 36 | ``` 37 | 38 | ### `changes_stream` 39 | 40 | The general interface into the library, most notably the ChangesStream 41 | buffer 42 | implementation. 43 | 44 | ```rust 45 | pub mod changes_stream; 46 | ``` 47 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/types/change.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use serde::de::Deserialize; 3 | use super::revision::Revision; 4 | 5 | #[derive(Debug,Deserialize)] 6 | pub struct Change { 7 | pub seq: i64, 8 | id: String, 9 | changes: Vec, 10 | doc: Option, 11 | deleted: bool, 12 | } 13 | ``` 14 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/types/changes_lines.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use serde_json as json; 3 | use serde::de::Deserialize; 4 | use std::convert::AsRef; 5 | use super::change::Change; 6 | use super::last_seq::LastSeq; 7 | 8 | #[derive(Debug)] 9 | pub enum ChangesLines { 10 | Change(Change), 11 | LastSeq(LastSeq), 12 | } 13 | 14 | impl ChangesLines { 15 | pub fn parse<'a, Line: AsRef>(line: Line) -> Result, json::error::Error> { 16 | json::from_str::>(line.as_ref()) 17 | .map(|c| ChangesLines::Change(c)) 18 | .or_else(|e| { 19 | json::from_str::(line.as_ref()) 20 | .map(|seq| ChangesLines::LastSeq(seq)) 21 | .or(Err(e)) 22 | }) 23 | } 24 | 25 | pub fn change(&self) -> bool { 26 | match *self { 27 | ChangesLines::Change(_) => true, 28 | _ => false, 29 | } 30 | } 31 | 32 | pub fn to_change(self) -> Option> { 33 | match self { 34 | ChangesLines::Change(c) => Some(c), 35 | _ => None, 36 | } 37 | } 38 | 39 | pub fn to_last_seq(self) -> Option { 40 | match self { 41 | ChangesLines::LastSeq(l) => Some(l), 42 | _ => None, 43 | } 44 | } 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/types/document.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use serde::de::Deserialize; 3 | 4 | pub trait Document: Deserialize {} 5 | ``` 6 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/types/last_seq.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | #[derive(Debug, Deserialize)] 3 | pub struct LastSeq { 4 | last_seq: i64, 5 | } 6 | 7 | #[cfg(test)] 8 | mod tests { 9 | use serde_json as json; 10 | use super::LastSeq; 11 | 12 | #[test] 13 | fn parses_last_seq_line() { 14 | json::from_str::("{\"last_seq\":3}").unwrap(); 15 | } 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/types/mod.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | pub mod revision; 3 | pub mod change; 4 | pub mod document; 5 | pub mod last_seq; 6 | pub mod changes_lines; 7 | ``` 8 | -------------------------------------------------------------------------------- /lazers-changes-stream/src/types/revision.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | #[derive(Debug, Deserialize)] 3 | pub struct Revision { 4 | rev: String, 5 | } 6 | ``` 7 | -------------------------------------------------------------------------------- /lazers-changes-stream/tango-build.rs: -------------------------------------------------------------------------------- 1 | extern crate tango; 2 | 3 | fn main() { tango::process_root().unwrap() } 4 | -------------------------------------------------------------------------------- /lazers-hyper-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lazers-hyper-client" 3 | version = "0.1.0" 4 | authors = ["Florian Gilcher "] 5 | 6 | build = "tango-build.rs" 7 | 8 | [dependencies] 9 | backtrace = "0.3.0" 10 | hyper = "0.10.0" 11 | url = "1.4.0" 12 | serde = "0.9.0" 13 | serde_json = "0.9.0" 14 | serde_derive = "0.9.0" 15 | mime = "0.2.1" 16 | futures = "0.1.10" 17 | error-chain = "0.8.1" 18 | 19 | [dependencies.lazers-traits] 20 | path = "../lazers-traits" 21 | 22 | [build-dependencies] 23 | tango = "0.5.0" 24 | 25 | [lib] 26 | name = "lazers_hyper_client" 27 | path = "src/lib.rs" 28 | -------------------------------------------------------------------------------- /lazers-hyper-client/src/lib.md: -------------------------------------------------------------------------------- 1 | # lazers-hyper-client 2 | 3 | A CouchDB client implemented using hyper. 4 | 5 | This is currently a draft implementation that suffers from a few problems, 6 | mainly that generating the errors hooking into lazers-traits a bit noisy. 7 | 8 | This crate itself holds no logic outside of HTTP handling, the description 9 | of 10 | all workflows is in lazers-traits. 11 | 12 | 13 | ```rust 14 | extern crate hyper; 15 | extern crate url; 16 | extern crate lazers_traits; 17 | extern crate serde; 18 | extern crate serde_json; 19 | #[macro_use] extern crate serde_derive; 20 | extern crate mime; 21 | extern crate backtrace; 22 | extern crate futures; 23 | #[macro_use] extern crate error_chain; 24 | 25 | mod types; 26 | use types::document_created::DocumentCreated; 27 | use types::error; 28 | 29 | use lazers_traits::prelude::*; 30 | 31 | use serde_json::de::from_reader; 32 | use serde_json::ser::to_string; 33 | 34 | use hyper::header::ETag; 35 | use hyper::header::ContentType; 36 | 37 | use hyper::client::IntoUrl; 38 | 39 | use hyper::status::StatusCode; 40 | 41 | use url::{Url, ParseError}; 42 | 43 | use futures::BoxFuture; 44 | use futures::Future; 45 | use futures::future::result; 46 | use futures::future::err; 47 | use futures::future::ok; 48 | 49 | pub struct HyperClient { 50 | inner: hyper::client::Client, 51 | base_url: Url, 52 | } 53 | 54 | impl HyperClient { 55 | pub fn new(url: T) -> std::result::Result { 56 | Ok(HyperClient { 57 | inner: hyper::client::Client::new(), 58 | base_url: try!(url.into_url()), 59 | }) 60 | } 61 | } 62 | 63 | impl Default for HyperClient { 64 | fn default() -> HyperClient { 65 | HyperClient { 66 | inner: hyper::client::Client::new(), 67 | base_url: Url::parse("http://localhost:5984").expect("this is a valid URL"), 68 | } 69 | } 70 | } 71 | 72 | #[derive(Clone)] 73 | pub struct RemoteDatabaseCreator { 74 | name: DatabaseName, 75 | base_url: Url, 76 | } 77 | 78 | #[derive(Clone)] 79 | pub struct RemoteDatabase { 80 | name: DatabaseName, 81 | base_url: Url, 82 | } 83 | 84 | impl DatabaseCreator for RemoteDatabaseCreator { 85 | type D = RemoteDatabase; 86 | 87 | fn create(self) -> BoxFuture { 88 | let mut url = self.base_url.clone(); 89 | url.set_path(self.name.as_ref()); 90 | let client = hyper::client::Client::new(); 91 | let res = client.put(url) 92 | .send(); 93 | 94 | let res2 = res.chain_err(|| self.name.clone()); 95 | 96 | match res2 { 97 | Ok(_) => { 98 | ok(RemoteDatabase { 99 | name: self.name, 100 | base_url: self.base_url, 101 | }).boxed() 102 | } 103 | Err(e) => err(e).boxed(), 104 | } 105 | } 106 | } 107 | 108 | impl Database for RemoteDatabase { 109 | type Creator = RemoteDatabaseCreator; 110 | 111 | fn info(&self) -> BoxFuture { 112 | let mut url = self.base_url.clone(); 113 | url.set_path(self.name.as_ref()); 114 | let client = hyper::client::Client::new(); 115 | let res = client.get(url) 116 | .send(); 117 | 118 | let res2 = res.chain_err(|| self.name.clone()); 119 | 120 | match res2 { 121 | Ok(r) => { 122 | match r.status { 123 | StatusCode::Ok => { 124 | let info: types::database_info::CouchDBInfo = from_reader(r).unwrap(); 125 | let db_info = DatabaseInfo::new( 126 | info.instance_start_time, 127 | UpdateSeq::Numeric(info.update_seq) 128 | ); 129 | 130 | ok(db_info).boxed() 131 | } 132 | StatusCode::NotFound => { 133 | err(Error::from(format!("Database vanished: {}", self.name))).boxed() 134 | } 135 | _ => { 136 | err(Error::from(format!("Unexpected status: {}", r.status))).boxed() 137 | } 138 | } 139 | }, 140 | Err(e) => { 141 | err(Error::from(format!("Unexpected HTTP error"))).boxed() 142 | } 143 | } 144 | } 145 | 146 | fn destroy(self) -> BoxFuture { 147 | let mut url = self.base_url.clone(); 148 | url.set_path(self.name.as_ref()); 149 | let client = hyper::client::Client::new(); 150 | let res = client.delete(url) 151 | .send(); 152 | 153 | let res2 = res.chain_err(|| self.name.clone()); 154 | 155 | match res2 { 156 | Ok(_) => { 157 | ok(RemoteDatabaseCreator { 158 | name: self.name, 159 | base_url: self.base_url, 160 | }).boxed() 161 | } 162 | Err(e) => err(e).boxed(), 163 | } 164 | } 165 | 166 | fn doc(&self, 167 | key: K) 168 | -> BoxFuture, Error> { 169 | let mut url = self.base_url.clone(); 170 | url.set_path(format!("{}/{}", self.name, key.id()).as_ref()); 171 | let client = hyper::client::Client::new(); 172 | let res = client.get(url) 173 | .send(); 174 | 175 | match res { 176 | Ok(r) => { 177 | match r.status { 178 | StatusCode::Ok => { 179 | let rev = r.headers.get::().unwrap().clone(); 180 | let key_with_rev = ::from_id_and_rev(key.id().to_owned(), 181 | Some(rev.tag().to_owned())); 182 | let doc = from_reader(r).unwrap(); 183 | ok(DatabaseEntry::present(key_with_rev, doc, self.clone())).boxed() 184 | } 185 | StatusCode::NotFound => ok(DatabaseEntry::absent(key, self.clone())).boxed(), 186 | _ => { 187 | err(Error::from(format!("Unexpected status: {}", r.status))).boxed() 188 | } 189 | } 190 | } 191 | Err(e) => { 192 | err(Error::from(format!("Unexpected HTTP error"))).boxed() 193 | } 194 | } 195 | } 196 | 197 | // this should probably be &doc, as Doc won't be changed, but might 198 | // get a new key 199 | fn insert(&self, key: K, doc: D) -> BoxFuture<(K, D), Error> { 200 | println!("{:?}", key); 201 | let mut url = self.base_url.clone(); 202 | url.set_path(format!("{}/{}", self.name, key.id()).as_ref()); 203 | 204 | if let Some(rev) = key.rev() { 205 | url.query_pairs_mut().append_pair("rev", rev); 206 | } 207 | 208 | let client = hyper::client::Client::new(); 209 | let body = match to_string(&doc) { 210 | Ok(s) => s, 211 | Err(e) => { 212 | return err(Error::from(format!("Unexpected HTTP error"))).boxed() 213 | } 214 | }; 215 | 216 | let mime: mime::Mime = "application/json".parse().unwrap(); 217 | let res = client.put(url) 218 | .header(ContentType(mime)) 219 | .body(&body) 220 | .send(); 221 | 222 | let client_result = match res { 223 | Ok(r) => { 224 | match r.status { 225 | StatusCode::Created => { 226 | let response_data: DocumentCreated = from_reader(r).unwrap(); 227 | 228 | let k = K::from_id_and_rev(response_data.id, Some(response_data.rev)); 229 | 230 | Ok((k, doc)) 231 | } 232 | StatusCode::Conflict => { 233 | let response_data: error::Error = from_reader(r).unwrap(); 234 | match response_data { 235 | error::Error::Conflict(reason) => { 236 | Err(Error::from(format!("Document update conflict: {}", reason))) 237 | } 238 | error::Error::BadRequest(reason) => { 239 | Err(Error::from(format!("Bad request: {}", reason))) 240 | } 241 | } 242 | } 243 | _ => { 244 | Err(Error::from(format!("Unexpected status: {}", r.status))) 245 | } 246 | } 247 | } 248 | Err(e) => { 249 | Err(Error::from(format!("Unexpected HTTP error"))) 250 | } 251 | }; 252 | result(client_result).boxed() 253 | } 254 | 255 | fn delete(&self, key: K) -> BoxFuture<(), Error> { 256 | let mut url = self.base_url.clone(); 257 | url.set_path(format!("{}/{}", self.name, key.id()).as_ref()); 258 | url.query_pairs_mut().append_pair("rev", key.rev().unwrap()); 259 | let client = hyper::client::Client::new(); 260 | let res = client.delete(url) 261 | .send(); 262 | 263 | let client_result = match res { 264 | Ok(r) => { 265 | match r.status { 266 | StatusCode::Ok => Ok(()), 267 | _ => { 268 | Err(Error::from(format!("Unexpected status: {}", r.status))) 269 | } 270 | } 271 | } 272 | Err(e) => { 273 | Err(Error::from(format!("Unexpected HTTP error"))) 274 | } 275 | }; 276 | result(client_result).boxed() 277 | } 278 | } 279 | 280 | impl Client for HyperClient { 281 | type Database = RemoteDatabase; 282 | 283 | fn id(&self) -> String { 284 | self.base_url.to_string() 285 | } 286 | 287 | fn find_database(&self, 288 | name: DatabaseName) 289 | -> BoxFuture, Error> { 290 | let mut url = self.base_url.clone(); 291 | url.set_path(name.as_ref()); 292 | let res = self.inner 293 | .head(url) 294 | .send(); 295 | 296 | match res { 297 | Ok(r) => { 298 | match r.status { 299 | StatusCode::Ok => { 300 | ok(DatabaseState::Existing(RemoteDatabase { 301 | name: name, 302 | base_url: self.base_url.clone(), 303 | })).boxed() 304 | } 305 | StatusCode::NotFound => { 306 | ok(DatabaseState::Absent(RemoteDatabaseCreator { 307 | name: name, 308 | base_url: self.base_url.clone(), 309 | })).boxed() 310 | } 311 | _ => { 312 | err(Error::from(format!("Unexpected status: {}", r.status))).boxed() 313 | } 314 | } 315 | } 316 | Err(e) => { 317 | err(Error::from(format!("Unexpected HTTP error"))).boxed() 318 | } 319 | } 320 | } 321 | } 322 | ``` 323 | -------------------------------------------------------------------------------- /lazers-hyper-client/src/types/database_info.md: -------------------------------------------------------------------------------- 1 | # The CouchDB database document 2 | 3 | ``` 4 | {"db_name":"test","doc_count":0,"doc_del_count":0,"update_seq":0,"purge_seq":0,"compact_running":false,"disk_size":79,"data_size":0,"instance_start_time":"1475608668265086","disk_format_version":6,"committed_update_seq":0} 5 | ``` 6 | 7 | ```rust 8 | #[derive(Debug, Deserialize)] 9 | pub struct CouchDBInfo { 10 | pub db_name: String, 11 | pub doc_count: u64, 12 | pub doc_del_count: u64, 13 | pub update_seq: u64, 14 | pub purge_seq: u64, 15 | pub compact_running: bool, 16 | pub disk_size: u64, 17 | pub data_size: u64, 18 | pub instance_start_time: String, 19 | pub disk_format_version: u64, 20 | pub committed_update_seq: u64 21 | } 22 | 23 | enum CouchDBInfoField { 24 | DbName, 25 | DocCount, 26 | DocDelCount, 27 | UpdateSeq, 28 | PurgeSeq, 29 | CompactRunning, 30 | DiskSize, 31 | DataSize, 32 | InstanceStartTime, 33 | DiskFormatVersion, 34 | CommittedUpdateSeq, 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use serde_json as json; 40 | use super::CouchDBInfo; 41 | 42 | #[test] 43 | fn parses_couchdb_info() { 44 | json::from_str::("{\"db_name\":\"test\",\"doc_count\":0,\"doc_del_count\":0,\"update_seq\":0,\"purge_seq\":0,\"compact_running\":false,\"disk_size\":79,\"data_size\":0,\"instance_start_time\":\"1475608668265086\",\"disk_format_version\":6,\"committed_update_seq\":0}") 45 | .unwrap(); 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /lazers-hyper-client/src/types/document_created.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | #[derive(Debug, Deserialize)] 3 | pub struct DocumentCreated { 4 | pub ok: bool, 5 | pub id: String, 6 | pub rev: String, 7 | } 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use serde_json as json; 12 | use super::DocumentCreated; 13 | 14 | #[test] 15 | fn parses_document_created() { 16 | json::from_str::("{\"ok\": true, \"id\": \"213123\", \"rev\": \ 17 | \"1-cd90201763f897aa0178b7ff05eb80cb\"}") 18 | .unwrap(); 19 | } 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /lazers-hyper-client/src/types/error.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use serde; 3 | use std::fmt; 4 | 5 | #[derive(Debug, Deserialize)] 6 | #[serde(tag = "error", content = "reason")] 7 | pub enum Error { 8 | #[serde(rename = "conflict")] 9 | Conflict(String), 10 | #[serde(rename = "bad_request")] 11 | BadRequest(String), 12 | } 13 | 14 | #[cfg(test)] 15 | mod tests { 16 | use serde_json as json; 17 | use super::Error; 18 | 19 | #[test] 20 | fn parses_error() { 21 | json::from_str::("{\"error\":\"conflict\",\"reason\":\"Document update \ 22 | conflict.\"}") 23 | .unwrap(); 24 | json::from_str::("{\"error\":\"bad_request\",\"reason\":\"Referer header \ 25 | required.\"}") 26 | .unwrap(); 27 | 28 | } 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /lazers-hyper-client/src/types/mod.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | pub mod document_created; 3 | pub mod database_info; 4 | pub mod error; 5 | ``` 6 | -------------------------------------------------------------------------------- /lazers-hyper-client/tango-build.rs: -------------------------------------------------------------------------------- 1 | extern crate tango; 2 | 3 | fn main() { tango::process_root().unwrap() } 4 | -------------------------------------------------------------------------------- /lazers-hyper-client/tests/tests.rs: -------------------------------------------------------------------------------- 1 | extern crate lazers_hyper_client; 2 | extern crate lazers_traits; 3 | extern crate serde_json; 4 | extern crate futures; 5 | 6 | use futures::Future; 7 | use futures::done; 8 | 9 | use lazers_hyper_client::*; 10 | use lazers_traits::prelude::*; 11 | 12 | #[test] 13 | fn test_database_lookup() { 14 | let client = HyperClient::default(); 15 | let res = client.find_database("absent".to_string()).wait(); 16 | assert!(res.is_ok()); 17 | } 18 | 19 | #[test] 20 | fn test_database_absent() { 21 | let client = HyperClient::default(); 22 | let res = client.find_database("absent".to_string()).wait(); 23 | assert!(res.is_ok()); 24 | assert!(res.unwrap().absent()) 25 | } 26 | 27 | #[test] 28 | fn test_database_create() { 29 | let client = HyperClient::default(); 30 | let res = client.find_database("to_be_created".to_string()) 31 | .or_create().wait(); 32 | assert!(res.is_ok()); 33 | assert!(res.unwrap().existing()) 34 | } 35 | 36 | #[test] 37 | fn test_database_create_and_delete() { 38 | let client = HyperClient::default(); 39 | let res = client.find_database("to_be_deleted".to_string()) 40 | .or_create() 41 | .and_delete().wait(); 42 | assert!(res.is_ok()); 43 | assert!(res.unwrap().absent()) 44 | } 45 | 46 | #[test] 47 | fn test_database_get_document() { 48 | use lazers_traits::SimpleKey; 49 | use serde_json::Value; 50 | 51 | let client = HyperClient::default(); 52 | let res = client.find_database("empty_test_db".to_string()) 53 | .or_create().wait(); 54 | assert!(res.is_ok()); 55 | let db = res.unwrap(); 56 | assert!(db.existing()); 57 | 58 | if let DatabaseState::Existing(db) = db { 59 | let key = SimpleKey::from("test".to_owned()); 60 | let doc_res = db.doc::(key); 61 | assert!(doc_res.wait().is_ok()); 62 | } else { 63 | panic!("database not existing!") 64 | } 65 | } 66 | 67 | #[test] 68 | fn test_database_create_document() { 69 | use lazers_traits::SimpleKey; 70 | use serde_json::Value; 71 | 72 | let client = HyperClient::default(); 73 | let res = client.find_database("empty_test_db".to_string()) 74 | .and_delete() 75 | .or_create().wait(); 76 | assert!(res.is_ok()); 77 | let db = res.unwrap(); 78 | assert!(db.existing()); 79 | 80 | if let DatabaseState::Existing(db) = db { 81 | let key = SimpleKey::from("test-will-be-created".to_owned()); 82 | let s = "{\"x\": 1.0, \"y\": 2.0}"; 83 | let value: Value = serde_json::from_str(s).unwrap(); 84 | let doc_res = db.doc(key).wait(); 85 | assert!(doc_res.is_ok()); 86 | 87 | let del_res = done(doc_res).boxed().delete().wait(); 88 | assert!(del_res.is_ok()); 89 | 90 | let set_res = done(del_res).boxed().set(value).wait(); 91 | 92 | match set_res { 93 | Err(e) => {println!("{}", e); panic!()}, 94 | _ => { } 95 | }; 96 | 97 | assert!(set_res.is_ok()); 98 | 99 | let get_res = done(set_res).boxed().get().wait(); 100 | assert!(get_res.is_ok()); 101 | } else { 102 | panic!("database not existing!") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lazers-liblazers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lazers-liblazers" 3 | version = "0.1.0" 4 | authors = ["Florian Gilcher "] 5 | 6 | build = "build.rs" 7 | 8 | [dependencies] 9 | libc = "0.2.20" 10 | 11 | [dependencies.lazers-traits] 12 | path = "../lazers-traits" 13 | 14 | [dependencies.lazers-hyper-client] 15 | path = "../lazers-hyper-client" 16 | 17 | [build-dependencies] 18 | tango = "0.5.0" 19 | rusty-cheddar = "0.3.0" 20 | 21 | [lib] 22 | crate-type = ["dylib"] 23 | name = "lazers_liblazers" 24 | path = "src/lib.rs" 25 | -------------------------------------------------------------------------------- /lazers-liblazers/build.rs: -------------------------------------------------------------------------------- 1 | extern crate tango; 2 | extern crate cheddar; 3 | 4 | fn main() { 5 | tango::process_root().unwrap(); 6 | 7 | cheddar::Cheddar::new().expect("could not read manifest") 8 | .run_build("include/lazers.h"); 9 | } 10 | -------------------------------------------------------------------------------- /lazers-liblazers/src/lib.md: -------------------------------------------------------------------------------- 1 | # liblazers 2 | 3 | The C interface to laze.rs. 4 | 5 | 6 | ```rust 7 | extern crate lazers_hyper_client; 8 | extern crate lazers_traits; 9 | extern crate libc; 10 | 11 | use std::any::Any; 12 | use std::ffi::CStr; 13 | use lazers_traits::Client; 14 | use lazers_hyper_client::HyperClient; 15 | 16 | #[repr(C)] 17 | pub struct CClient { 18 | inspect: unsafe extern "C" fn(*mut std::os::raw::c_void) -> (), 19 | close: unsafe extern "C" fn(*mut std::os::raw::c_void) -> (), 20 | get: unsafe extern "C" fn(*mut std::os::raw::c_void, *mut std::os::raw::c_char) -> (), 21 | client: *mut std::os::raw::c_void, 22 | } 23 | 24 | pub trait CClientInterface { 25 | fn inspect(&self); 26 | fn get(&self, key: &str); 27 | } 28 | 29 | impl From> for CClient { 30 | fn from(i: Box) -> CClient { 31 | CClient { 32 | inspect: inspect::, 33 | close: close::, 34 | get: get::, 35 | client: Box::into_raw(i) as *mut std::os::raw::c_void, 36 | } 37 | } 38 | } 39 | 40 | impl CClientInterface for HyperClient { 41 | fn inspect(&self) { 42 | println!("hey from hyperclient"); 43 | } 44 | fn get(&self, key: &str) { 45 | println!("hey from get: {}", key); 46 | } 47 | } 48 | 49 | unsafe extern "C" fn inspect(client: *mut std::os::raw::c_void) { 50 | let client: Box = Box::from_raw(client as *mut C); 51 | client.inspect(); 52 | std::mem::forget(client); 53 | } 54 | 55 | unsafe extern "C" fn get(client: *mut std::os::raw::c_void, 56 | raw_key: *mut std::os::raw::c_char) { 57 | let client: Box = Box::from_raw(client as *mut C); 58 | let key = CStr::from_ptr(raw_key).to_str().unwrap(); 59 | client.get(key); 60 | std::mem::forget(client); 61 | } 62 | 63 | unsafe extern "C" fn close(client: *mut std::os::raw::c_void) { 64 | let client = client as *mut CClient; 65 | Box::from_raw((*client).client as *mut C); 66 | } 67 | 68 | #[no_mangle] 69 | pub extern "C" fn lzrs_new_hyper_client() -> *mut CClient { 70 | let client = Box::new(HyperClient::default()); 71 | let client_structure = Box::new(client.into()); 72 | Box::into_raw(client_structure) 73 | } 74 | 75 | 76 | #[test] 77 | fn sizes() { 78 | use std::mem::size_of; 79 | assert_eq!(size_of::>(), 8); 80 | assert_eq!(size_of::>(), 16); 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /lazers-liblazers/tests/test.c: -------------------------------------------------------------------------------- 1 | #include "../include/lazers.h" 2 | 3 | int main (int argc, char const *argv[]) 4 | { 5 | CClient* c = lzrs_new_hyper_client(); 6 | c->inspect(c->client); 7 | c->get(c->client, "foobar"); 8 | c->close(c); 9 | } 10 | -------------------------------------------------------------------------------- /lazers-replicator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lazers-replicator" 3 | version = "0.1.0" 4 | authors = ["Florian Gilcher "] 5 | 6 | build = "tango-build.rs" 7 | 8 | [dependencies] 9 | futures = "0.1.10" 10 | backtrace = "0.3.0" 11 | error-chain = "0.8.1" 12 | rust-crypto = "0.2.36" 13 | derive_builder = "0.3.0" 14 | serde = "0.9.0" 15 | serde_json = "0.9.0" 16 | serde_derive = "0.9.0" 17 | 18 | [dependencies.lazers-changes-stream] 19 | path = "../lazers-changes-stream" 20 | 21 | [dependencies.lazers-traits] 22 | path = "../lazers-traits" 23 | 24 | [dev-dependencies.lazers-hyper-client] 25 | path = "../lazers-hyper-client" 26 | 27 | [build-dependencies] 28 | tango = "0.5.0" 29 | 30 | [lib] 31 | name = "lazers_replicator" 32 | path = "src/lib.rs" 33 | -------------------------------------------------------------------------------- /lazers-replicator/src/documents.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | #[derive(Deserialize, Serialize)] 3 | pub struct HistoryEntry { 4 | doc_write_failures: i64, 5 | docs_read: i64, 6 | docs_written: i64, 7 | end_last_seq: i64, 8 | end_time: String,// should be date 9 | missing_checked: i64, 10 | missing_found: i64, 11 | recorded_seq: i64, 12 | session_id: String, 13 | start_last_seq: i64, 14 | start_time: String,// should be Date 15 | } 16 | 17 | #[derive(Deserialize, Serialize)] 18 | pub struct ReplicationLog { 19 | history: Vec, 20 | replication_id_version: i8, 21 | session_id: String, 22 | source_last_seq: i64, 23 | _id: String, 24 | _rev: String 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use serde_json as json; 30 | use super::HistoryEntry; 31 | use super::ReplicationLog; 32 | 33 | #[test] 34 | fn parses_history_info() { 35 | json::from_str::(r#"{"doc_write_failures": 0, "docs_read": 2, "docs_written": 2, "end_last_seq": 5, "end_time": "Thu, 10 Oct 2013 05:56:38 GMT", "missing_checked": 2, "missing_found": 2, "recorded_seq": 5, "session_id": "d5a34cbbdafa70e0db5cb57d02a6b955", "start_last_seq": 3, "start_time": "Thu, 10 Oct 2013 05:56:38 GMT"}"#) 36 | .unwrap(); 37 | } 38 | 39 | #[test] 40 | fn parses_replication_log() { 41 | json::from_str::(r#"{"_id": "_local/b3e44b920ee2951cb2e123b63044427a", "_rev": "0-8", "history": [{"doc_write_failures": 0, "docs_read": 2, "docs_written": 2, "end_last_seq": 5, "end_time": "Thu, 10 Oct 2013 05:56:38 GMT", "missing_checked": 2, "missing_found": 2, "recorded_seq": 5, "session_id": "d5a34cbbdafa70e0db5cb57d02a6b955", "start_last_seq": 3, "start_time": "Thu, 10 Oct 2013 05:56:38 GMT"}, {"doc_write_failures": 0, "docs_read": 1, "docs_written": 1, "end_last_seq": 3, "end_time": "Thu, 10 Oct 2013 05:56:12 GMT", "missing_checked": 1, "missing_found": 1, "recorded_seq": 3, "session_id": "11a79cdae1719c362e9857cd1ddff09d", "start_last_seq": 2, "start_time": "Thu, 10 Oct 2013 05:56:12 GMT"}, {"doc_write_failures": 0, "docs_read": 2, "docs_written": 2, "end_last_seq": 2, "end_time": "Thu, 10 Oct 2013 05:56:04 GMT", "missing_checked": 2, "missing_found": 2, "recorded_seq": 2, "session_id": "77cdf93cde05f15fcb710f320c37c155", "start_last_seq": 0, "start_time": "Thu, 10 Oct 2013 05:56:04 GMT"}], "replication_id_version": 3, "session_id": "d5a34cbbdafa70e0db5cb57d02a6b955", "source_last_seq": 5}"#) 42 | .unwrap(); 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /lazers-replicator/src/errors.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use lazers_traits; 3 | use lazers_traits::DatabaseName; 4 | 5 | error_chain! { 6 | // The type defined for this error. These are the conventional 7 | // and recommended names, but they can be arbitrarily chosen. 8 | types { 9 | Error, ErrorKind, ChainErr, Result; 10 | } 11 | 12 | // Automatic conversions between this error chain and other 13 | // error chains. In this case, it will e.g. generate an 14 | // `ErrorKind` variant called `Dist` which in turn contains 15 | // the `rustup_dist::ErrorKind`, with conversions from 16 | // `rustup_dist::Error`. 17 | // 18 | // This section can be empty. 19 | links { 20 | Core(lazers_traits::result::Error, lazers_traits::result::ErrorKind); 21 | } 22 | 23 | // Automatic conversions between this error chain and other 24 | // error types not defined by the `error_chain!`. These will be 25 | // boxed as the error cause and wrapped in a new error with, 26 | // in this case, the `ErrorKind::Temp` variant. 27 | // 28 | // This section can be empty. 29 | //foreign_links { 30 | //} 31 | 32 | // Define additional `ErrorKind` variants. The syntax here is 33 | // the same as `quick_error!`, but the `from()` and `cause()` 34 | // syntax is not supported. 35 | errors { 36 | SourceDoesNotExist(name: DatabaseName) { 37 | description("Source database is not available") 38 | display("Source database: '{}'", name) 39 | } 40 | TargetDoesNotExist(name: DatabaseName) { 41 | description("Target database is not available") 42 | display("Target database: '{}'", name) 43 | } 44 | } 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /lazers-replicator/src/find_common_ancestry.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use super::Replicator; 3 | use lazers_traits::prelude::*; 4 | 5 | use futures::Future; 6 | use futures::BoxFuture; 7 | use futures::finished; 8 | use futures::future::err; 9 | 10 | use super::PeerInformationReceived; 11 | use super::ReplicatorState; 12 | 13 | use std::convert::From as TransitionFrom; 14 | use lazers_traits::SimpleKey; 15 | use lazers_traits::DatabaseEntry; 16 | 17 | use super::documents::ReplicationLog; 18 | ``` 19 | 20 | ## Find common Ancestry 21 | 22 | We implement the find common ancestry algorithm as described 23 | [here](http://docs.couchdb.org/en/2.0.0/replication/protocol.html#find-common-ancestry). 24 | 25 | ```rust 26 | pub trait State {} 27 | 28 | pub struct Start; 29 | impl State for Start {} 30 | 31 | pub struct GeneratedReplicationId; 32 | impl State for GeneratedReplicationId {} 33 | 34 | impl TransitionFrom for GeneratedReplicationId { 35 | fn from(_: Start) -> GeneratedReplicationId { 36 | GeneratedReplicationId 37 | } 38 | } 39 | 40 | pub struct GotSourceReplicationLog; 41 | impl State for GotSourceReplicationLog {} 42 | 43 | impl TransitionFrom for GotSourceReplicationLog { 44 | fn from(_: GeneratedReplicationId) -> GotSourceReplicationLog { 45 | GotSourceReplicationLog 46 | } 47 | } 48 | 49 | pub struct GotTargetReplicationLog; 50 | impl State for GotTargetReplicationLog {} 51 | 52 | impl TransitionFrom for GotTargetReplicationLog { 53 | fn from(_: GotSourceReplicationLog) -> GotTargetReplicationLog { 54 | GotTargetReplicationLog 55 | } 56 | } 57 | 58 | pub struct ComparedReplicationLog; 59 | impl State for ComparedReplicationLog {} 60 | 61 | impl TransitionFrom for ComparedReplicationLog { 62 | fn from(_: GotTargetReplicationLog) -> ComparedReplicationLog { 63 | ComparedReplicationLog 64 | } 65 | } 66 | ``` 67 | 68 | We then define a `GetPeersInformation` struct to define the flow used in the first few steps. `GetPeersInformation` wraps the replicator struct for the duration of the process. 69 | 70 | ```rust 71 | pub struct FindCommonAncestry { 72 | pub replicator: Replicator, 73 | pub source_replication_log: Option, 74 | pub target_replication_log: Option, 75 | #[allow(dead_code)] 76 | state: S 77 | } 78 | 79 | impl FindCommonAncestry { 80 | fn transition>(self, state: X) -> FindCommonAncestry { 81 | FindCommonAncestry { replicator: self.replicator, source_replication_log: self.source_replication_log, target_replication_log: self.target_replication_log, state: state } 82 | } 83 | } 84 | 85 | impl FindCommonAncestry { 86 | pub fn new(replicator: Replicator) -> Self { 87 | FindCommonAncestry { replicator: replicator, source_replication_log: None, target_replication_log: None, state: Start } 88 | } 89 | 90 | pub fn generate_replication_id(mut self) -> BoxFuture, Error> { 91 | let replication_id = self.replicator.replication_id("yihaaaaw"); 92 | 93 | finished(self.transition(GeneratedReplicationId)).boxed() 94 | } 95 | 96 | } 97 | 98 | impl FindCommonAncestry { 99 | pub fn get_source_replication_log(mut self) -> BoxFuture, Error> { 100 | let future_entry = { 101 | let db = self.replicator.from_db.as_ref().unwrap(); 102 | let replication_id = self.replicator.replication_id.as_ref().unwrap(); 103 | let key = SimpleKey::from(format!("_local/{}", replication_id)); 104 | db.doc::(key) 105 | }; 106 | 107 | future_entry.and_then(|entry| { 108 | match entry { 109 | DatabaseEntry::Present { doc, .. } => self.source_replication_log = Some(doc), 110 | DatabaseEntry::Absent { .. } => self.source_replication_log = None, 111 | DatabaseEntry::Conflicted { .. } => panic!("Unrecoverable: ReplicationLog conflicted") 112 | } 113 | 114 | finished(self.transition(GotSourceReplicationLog)) 115 | }).boxed() 116 | } 117 | } 118 | ``` 119 | -------------------------------------------------------------------------------- /lazers-replicator/src/get_peers_information.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use super::Replicator; 3 | use lazers_traits::prelude::*; 4 | 5 | use futures::Future; 6 | use futures::BoxFuture; 7 | use futures::finished; 8 | 9 | use super::PeersVerified as ReplicatorPeersVerified; 10 | 11 | use std::convert::From as TransitionFrom; 12 | ``` 13 | 14 | ## Get peers information 15 | 16 | We implement the peer information retrieval as described 17 | [here](http://docs.couchdb.org/en/2.0.0/replication/protocol.html#get-peers-information). 18 | 19 | ```rust 20 | pub trait State {} 21 | 22 | pub struct VerifiedPeers; 23 | impl State for VerifiedPeers {} 24 | 25 | pub struct GotSourceInformation; 26 | impl State for GotSourceInformation {} 27 | 28 | impl TransitionFrom for GotSourceInformation { 29 | fn from(_: VerifiedPeers) -> GotSourceInformation { 30 | GotSourceInformation 31 | } 32 | } 33 | 34 | pub struct GotTargetInformation; 35 | impl State for GotTargetInformation {} 36 | 37 | impl TransitionFrom for GotTargetInformation { 38 | fn from(_: GotSourceInformation) -> GotTargetInformation { 39 | GotTargetInformation 40 | } 41 | } 42 | 43 | pub struct GotPeersInformation; 44 | impl State for GotPeersInformation {} 45 | 46 | impl TransitionFrom for GotPeersInformation { 47 | fn from(_: GotTargetInformation) -> GotPeersInformation { 48 | GotPeersInformation 49 | } 50 | } 51 | ``` 52 | 53 | We then define a `GetPeersInformation` struct to define the flow used in the first few steps. `GetPeersInformation` wraps the replicator struct for the duration of the process. 54 | 55 | ```rust 56 | pub struct GetPeersInformation { 57 | pub replicator: Replicator, 58 | #[allow(dead_code)] 59 | state: S 60 | } 61 | 62 | impl GetPeersInformation { 63 | fn transition>(self, state: X) -> GetPeersInformation { 64 | GetPeersInformation { replicator: self.replicator, state: state } 65 | } 66 | } 67 | 68 | /// TODO: definitely need to change the PeersVerified (Replicator state) <> VerfiedPeers (local state) confusion 69 | impl GetPeersInformation { 70 | pub fn new(replicator: Replicator) -> Self { 71 | GetPeersInformation { replicator: replicator, state: VerifiedPeers } 72 | } 73 | 74 | pub fn get_source_information(mut self) -> BoxFuture, Error> { 75 | let future_db_info = self.replicator.from_db.as_ref().unwrap().info(); 76 | 77 | future_db_info.and_then(|db_info| { 78 | self.replicator.from_db_info = Some(db_info); 79 | finished(self.transition(GotSourceInformation)) 80 | }).boxed() 81 | } 82 | } 83 | 84 | impl GetPeersInformation { 85 | pub fn get_target_information(mut self) -> BoxFuture, Error> { 86 | let future_db_info = self.replicator.to_db.as_ref().unwrap().info(); 87 | 88 | future_db_info.and_then(|db_info| { 89 | self.replicator.to_db_info = Some(db_info); 90 | finished(self.transition(GotTargetInformation)) 91 | }).boxed() 92 | } 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /lazers-replicator/src/lib.md: -------------------------------------------------------------------------------- 1 | # lazers-replicator 2 | 3 | A replicator that takes a lazers DB and syncs couchdb data into it. It is 4 | an implementation of the algorithm described here: 5 | [here](http://docs.couchdb.org/en/1.6.1/replication/protocol. 6 | html#replication-protocol-algorithm). 7 | 8 | ```rust 9 | extern crate lazers_traits; 10 | extern crate futures; 11 | extern crate backtrace; 12 | extern crate crypto; 13 | #[macro_use] 14 | extern crate derive_builder; 15 | 16 | #[macro_use] 17 | extern crate error_chain; 18 | 19 | extern crate serde; 20 | extern crate serde_json; 21 | 22 | #[macro_use] 23 | extern crate serde_derive; 24 | 25 | use lazers_traits::prelude::*; 26 | 27 | use futures::Future; 28 | use futures::BoxFuture; 29 | use futures::finished; 30 | 31 | use std::convert::From as TransitionFrom; 32 | 33 | mod utils; 34 | 35 | pub mod errors; 36 | ``` 37 | 38 | ## Document definitions 39 | 40 | Document structures used throughout the process are wrapped in the [`documents`](/lazers-replicator/src/documents) module. 41 | 42 | ```rust 43 | pub mod documents; 44 | ``` 45 | 46 | ## Replicator 47 | 48 | The standard replicator struct is just a pair of clients to sync from and to, along with the databases to use. 49 | 50 | The two clients don't need to be of the same kind. 51 | 52 | The Replicator itself has a high-level state machine. 53 | 54 | ```rust 55 | pub struct Replicator { 56 | from: From, 57 | to: To, 58 | from_db_name: DatabaseName, 59 | to_db_name: DatabaseName, 60 | from_db: Option, 61 | to_db: Option, 62 | from_db_info: Option, 63 | to_db_info: Option, 64 | replication_id: Option, 65 | #[allow(dead_code)] 66 | state: State 67 | } 68 | 69 | impl Replicator { 70 | pub fn new(from: From, to: To, from_db: DatabaseName, to_db: DatabaseName) -> Replicator { 71 | Replicator { 72 | from: from, 73 | to: to, 74 | from_db_name: from_db, 75 | to_db_name: to_db, 76 | from_db: None, 77 | to_db: None, 78 | from_db_info: None, 79 | to_db_info: None, 80 | replication_id: None, 81 | state: Unconnected 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ### The state machine 88 | 89 | The Replicator state machine encodes all high-level steps descriped in the [CouchDB replication protocol](http://docs.couchdb.org/en/2.0.0/replication/protocol.html). 90 | 91 | ```rust 92 | pub trait ReplicatorState {} 93 | 94 | pub struct Unconnected; 95 | impl ReplicatorState for Unconnected {} 96 | 97 | pub struct PeersVerified; 98 | impl ReplicatorState for PeersVerified {} 99 | 100 | impl TransitionFrom for PeersVerified { 101 | fn from(_: Unconnected) -> PeersVerified { 102 | PeersVerified 103 | } 104 | } 105 | 106 | pub struct PeerInformationReceived; 107 | impl ReplicatorState for PeerInformationReceived {} 108 | 109 | impl TransitionFrom for PeerInformationReceived { 110 | fn from(_: PeersVerified) -> PeerInformationReceived { 111 | PeerInformationReceived 112 | } 113 | } 114 | 115 | impl Replicator { 116 | fn transition>(self, state: X) -> Replicator { 117 | Replicator { state: state, from: self.from, to: self.to, from_db: self.from_db, to_db: self.to_db, from_db_name: self.from_db_name, to_db_name: self.to_db_name, from_db_info: self.from_db_info, to_db_info: self.to_db_info, replication_id: self.replication_id } 118 | } 119 | } 120 | ``` 121 | 122 | ## The replication process 123 | 124 | The replication process is implemented in state machines wrapping the steps outlined in the CouchDB documentation, each implemented in a seperate module: 125 | 126 | [`verify_peers`](/lazers-replicator/src/verify_peers) implements peer verification. 127 | 128 | [`get_peers_information`](/lazers-replicator/src/get_peers_information) implements getting all important info from both peers. 129 | 130 | [`find_common_ancestry`](/lazers-replicator/src/find_common_ancestry) implements resolution of the common ancestry between to databases. 131 | 132 | ```rust 133 | mod verify_peers; 134 | mod get_peers_information; 135 | mod find_common_ancestry; 136 | ``` 137 | 138 | All these steps wrap the replicator type. 139 | 140 | Finally, they are glued to the replicator as its public interface. 141 | 142 | ```rust 143 | impl Replicator { 144 | pub fn verify_peers(self) -> BoxFuture, Error> { 145 | self.setup_peers(false) 146 | } 147 | 148 | pub fn setup_peers(self, create_target: bool) -> BoxFuture, Error> { 149 | let verifier = verify_peers::VerifyPeers::new(self); 150 | verifier.verify_source().and_then(|state| { 151 | state.verify_target() 152 | }).and_then(move |state| { 153 | if create_target { 154 | state.create_if_absent() 155 | } else { 156 | state.fail_if_absent() 157 | } 158 | }).and_then(|verifier| { 159 | finished(verifier.replicator.transition(PeersVerified)) 160 | }).boxed() 161 | } 162 | } 163 | 164 | impl Replicator { 165 | pub fn get_peers_information(self) -> BoxFuture, Error> { 166 | let steps = get_peers_information::GetPeersInformation::new(self); 167 | steps.get_source_information().and_then(|steps| { 168 | steps.get_target_information() 169 | }).and_then(|steps| { 170 | finished(steps.replicator.transition(PeerInformationReceived)) 171 | }).boxed() 172 | } 173 | } 174 | ``` 175 | -------------------------------------------------------------------------------- /lazers-replicator/src/utils.md: -------------------------------------------------------------------------------- 1 | # Replication utilities 2 | 3 | This module contains utilities necessary to implement the CouchDB replication protocol. 4 | 5 | ## CouchDB Replication ID 6 | 7 | The CouchDB replication protocol documents the following as inputs into the replication ID: 8 | 9 | * Persistent Peer UUID value. For CouchDB, the local Server UUID is used 10 | * Source and Target URI and if Source or Target are local or remote Databases 11 | * If Target needed to be created 12 | * If Replication is Continuous 13 | * OAuth headers if any 14 | * Any custom headers 15 | * Filter function code if used 16 | * Changes Feed query parameters, if any 17 | 18 | As laze RS is not necessarily a server, all these parameters need to be passed to the replication protocol. 19 | 20 | ```rust 21 | use super::ReplicatorState; 22 | use super::Replicator; 23 | use lazers_traits::Client; 24 | use crypto::md5::Md5; 25 | use crypto::digest::Digest; 26 | 27 | impl Replicator { 28 | // TODO: this needs to be fixed to encode the server 29 | pub fn replication_id(&self, peer_uuid: &str) -> String { 30 | let from_id = self.from.id(); 31 | let to_id = self.to.id(); 32 | 33 | let mut md5 = Md5::new(); 34 | md5.input_str(&from_id); 35 | md5.input_str(&to_id); 36 | 37 | md5.result_str() 38 | } 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /lazers-replicator/src/verify_peers.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | use super::Replicator; 3 | use lazers_traits::prelude::*; 4 | 5 | use futures::Future; 6 | use futures::BoxFuture; 7 | use futures::failed; 8 | use futures::finished; 9 | 10 | use super::Unconnected as ReplicatorUnconnected; 11 | 12 | use std::convert::From as TransitionFrom; 13 | ``` 14 | 15 | ## Verify peers 16 | 17 | We implement the peer verification as described 18 | [here](http://docs.couchdb.org/en/2.0.0/replication/protocol.html#verify-peers). 19 | 20 | We follow a state-machine like pattern here and name all possible states 21 | first. We label all states by using zero sized structs. They only serve as 22 | information for the type system. 23 | 24 | Connections between states are implemented using the `From` trait, aliased as `TransitionFrom`. 25 | 26 | ```rust 27 | pub trait State {} 28 | 29 | pub struct Unconnected; 30 | impl State for Unconnected {} 31 | 32 | pub struct SourceExisting; 33 | impl State for SourceExisting {} 34 | 35 | impl TransitionFrom for SourceExisting { 36 | fn from(_: Unconnected) -> SourceExisting { 37 | SourceExisting 38 | } 39 | } 40 | 41 | pub struct TargetAbsent; 42 | impl State for TargetAbsent {} 43 | 44 | impl TransitionFrom for TargetAbsent { 45 | fn from(_: SourceExisting) -> TargetAbsent { 46 | TargetAbsent 47 | } 48 | } 49 | 50 | pub struct TargetExisting; 51 | impl State for TargetExisting {} 52 | 53 | impl TransitionFrom for TargetExisting { 54 | fn from(_: SourceExisting) -> TargetExisting { 55 | TargetExisting 56 | } 57 | } 58 | 59 | impl TransitionFrom for TargetExisting { 60 | fn from(_: TargetAbsent) -> TargetExisting { 61 | TargetExisting 62 | } 63 | } 64 | ``` 65 | 66 | We then define a `VerifyPeers` struct to define the flow used in the first 67 | few steps. `VerifyPeers` wraps the replicator struct for the duration of the process. 68 | 69 | ```rust 70 | pub struct VerifyPeers { 71 | pub replicator: Replicator, 72 | #[allow(dead_code)] 73 | state: S 74 | } 75 | 76 | impl VerifyPeers { 77 | fn transition>(self, state: X) -> VerifyPeers { 78 | VerifyPeers { replicator: self.replicator, state: state } 79 | } 80 | } 81 | 82 | impl VerifyPeers { 83 | pub fn new(replicator: Replicator) -> Self { 84 | VerifyPeers { replicator: replicator, state: Unconnected } 85 | } 86 | 87 | pub fn verify_source(mut self) -> BoxFuture, Error> { 88 | let database = self.replicator.from_db_name.clone(); 89 | 90 | let future_db_state = self.replicator.from.find_database(database); 91 | future_db_state.and_then(|db_state| { 92 | match db_state { 93 | DatabaseState::Existing(db) => { 94 | self.replicator.from_db = Some(db); 95 | finished(self.transition(SourceExisting)).boxed() 96 | } 97 | _ => { 98 | failed(Error::from("Source doesn't exist")).boxed() 99 | } 100 | } 101 | }).boxed() 102 | } 103 | } 104 | 105 | impl VerifyPeers { 106 | pub fn verify_target(mut self) -> BoxFuture, Error> { 107 | let database = self.replicator.to_db_name.clone(); 108 | 109 | let future_db_state = self.replicator.to.find_database(database); 110 | future_db_state.and_then(|db_state| { 111 | match db_state { 112 | DatabaseState::Existing(db) => { 113 | self.replicator.to_db = Some(db); 114 | finished(TargetBranch::Existing(self.transition(TargetExisting))).boxed() 115 | } 116 | _ => { 117 | finished(TargetBranch::Absent(self.transition(TargetAbsent))).boxed() 118 | } 119 | } 120 | }).boxed() 121 | } 122 | } 123 | 124 | impl TargetBranch { 125 | pub fn create_if_absent(self) -> BoxFuture, Error> { 126 | match self { 127 | TargetBranch::Existing(s) => finished(s).boxed(), 128 | TargetBranch::Absent(s) => { 129 | s.create_target() 130 | } 131 | } 132 | } 133 | 134 | pub fn fail_if_absent(self) -> BoxFuture, Error> { 135 | match self { 136 | TargetBranch::Existing(s) => finished(s).boxed(), 137 | TargetBranch::Absent(_) => { 138 | failed(Error::from("Target doesn't exist")).boxed() 139 | } 140 | } 141 | } 142 | } 143 | 144 | impl VerifyPeers { 145 | pub fn create_target(mut self) -> BoxFuture, Error> { 146 | let database = self.replicator.to_db_name.clone(); 147 | 148 | let future_db_state = self.replicator.to.find_database(database); 149 | future_db_state.or_create().and_then(|db_state| { 150 | match db_state { 151 | DatabaseState::Existing(db) => { 152 | self.replicator.to_db = Some(db); 153 | finished(self.transition(TargetExisting)).boxed() 154 | } 155 | _ => { 156 | failed(Error::from("Creation of target database failed")).boxed() 157 | } 158 | } 159 | }).boxed() 160 | } 161 | } 162 | 163 | pub enum TargetBranch { 164 | Existing(VerifyPeers), 165 | Absent(VerifyPeers) 166 | } 167 | ``` 168 | -------------------------------------------------------------------------------- /lazers-replicator/tango-build.rs: -------------------------------------------------------------------------------- 1 | extern crate tango; 2 | 3 | fn main() { tango::process_root().unwrap() } 4 | -------------------------------------------------------------------------------- /lazers-replicator/tests/http_test.rs: -------------------------------------------------------------------------------- 1 | extern crate lazers_replicator; 2 | extern crate lazers_hyper_client; 3 | extern crate lazers_traits; 4 | extern crate futures; 5 | 6 | use futures::Future; 7 | 8 | use lazers_traits::prelude::*; 9 | use lazers_hyper_client::*; 10 | use lazers_replicator::*; 11 | 12 | 13 | fn ensure_database_present(database: &str) { 14 | let client = HyperClient::default(); 15 | client.find_database(database.to_string()) 16 | .or_create().wait().unwrap(); 17 | wait(1000) 18 | } 19 | 20 | fn ensure_database_absent(database: &str) { 21 | let client = HyperClient::default(); 22 | client.find_database(database.to_string()) 23 | .and_delete().wait().unwrap(); 24 | } 25 | 26 | fn wait(millis: u64) { 27 | use std::{thread, time}; 28 | 29 | let duration = time::Duration::from_millis(millis); 30 | 31 | thread::sleep(duration); 32 | } 33 | 34 | #[test] 35 | fn test_present_databases() { 36 | let from = HyperClient::default(); 37 | let to = HyperClient::default(); 38 | ensure_database_present("present-source"); 39 | ensure_database_present("present-target"); 40 | 41 | let from_db = "present-source".to_string(); 42 | let to_db = "present-target".to_string(); 43 | 44 | let replicator = Replicator::new(from, to, from_db, to_db); 45 | 46 | let res = replicator.verify_peers().wait(); 47 | 48 | assert!(res.is_ok()) 49 | } 50 | 51 | #[test] 52 | fn test_absent_from_database() { 53 | let from = HyperClient::default(); 54 | let to = HyperClient::default(); 55 | ensure_database_absent("absent-source"); 56 | ensure_database_absent("absent-target"); 57 | 58 | let from_db = "absent-source".to_string(); 59 | let to_db = "absent-target".to_string(); 60 | 61 | let replicator = Replicator::new(from, to, from_db, to_db); 62 | 63 | let res = replicator.verify_peers().wait(); 64 | 65 | assert!(res.is_err()) 66 | } 67 | 68 | #[test] 69 | fn test_absent_target_database_without_create() { 70 | let from = HyperClient::default(); 71 | let to = HyperClient::default(); 72 | ensure_database_present("present-source"); 73 | ensure_database_absent("absent-target"); 74 | 75 | let from_db = "present-source".to_string(); 76 | let to_db = "absent-target".to_string(); 77 | 78 | let replicator = Replicator::new(from, to, from_db, to_db); 79 | 80 | let res = replicator.verify_peers().wait(); 81 | 82 | assert!(res.is_err()) 83 | } 84 | 85 | #[test] 86 | fn test_absent_target_database_with_create() { 87 | let from = HyperClient::default(); 88 | let to = HyperClient::default(); 89 | ensure_database_present("present-source"); 90 | ensure_database_absent("absent-target"); 91 | 92 | let from_db = "present-source".to_string(); 93 | let to_db = "absent-target".to_string(); 94 | 95 | let replicator = Replicator::new(from, to, from_db, to_db); 96 | 97 | let res = replicator.setup_peers(true).wait(); 98 | 99 | assert!(res.is_ok()) 100 | } 101 | 102 | #[test] 103 | fn test_absent_source_database_with_create() { 104 | let from = HyperClient::default(); 105 | let to = HyperClient::default(); 106 | ensure_database_absent("absent-source"); 107 | ensure_database_absent("absent-target"); 108 | 109 | let from_db = "absent-source".to_string(); 110 | let to_db = "absent-target".to_string(); 111 | 112 | let replicator = Replicator::new(from, to, from_db, to_db); 113 | 114 | let res = replicator.setup_peers(true).wait(); 115 | 116 | assert!(res.is_err()) 117 | } -------------------------------------------------------------------------------- /lazers-traits/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lazers-traits" 3 | version = "0.1.0" 4 | authors = ["Florian Gilcher "] 5 | 6 | build = "tango-build.rs" 7 | 8 | [dependencies] 9 | serde = "0.9.0" 10 | error-chain = "0.8.1" 11 | futures = "0.1.10" 12 | 13 | [build-dependencies] 14 | tango = "0.5.0" 15 | 16 | [lib] 17 | name = "lazers_traits" 18 | path = "src/lib.rs" 19 | -------------------------------------------------------------------------------- /lazers-traits/src/decorations.md: -------------------------------------------------------------------------------- 1 | # Result Decorations 2 | 3 | These implementations make it easier to work with the results given by the 4 | traits described by the main module. 5 | 6 | They decorate the respective results with generic operations while 7 | propagating 8 | previously occuring errors. 9 | 10 | The pattern is described in detail 11 | [here](http://yakshav.es/decorating-results). 12 | 13 | ## Imports 14 | 15 | All types to be decorated and types necessary for interaction with them. 16 | 17 | ```rust 18 | use super::DatabaseState; 19 | use super::Database; 20 | use super::DatabaseCreator; 21 | use super::DatabaseEntry; 22 | use super::Document; 23 | use super::Key; 24 | 25 | use result::Error; 26 | 27 | use futures::BoxFuture; 28 | use futures::Future; 29 | use futures::finished; 30 | use futures::done; 31 | ``` 32 | 33 | ### Results of finding a Database 34 | 35 | `FindDatabaseResult` decorates the result returned from finding a database. 36 | The 37 | operations provided are `or_create` and `and_delete`. 38 | 39 | `or_create` creates the database if it was not present, otherwise, it just 40 | returns the already-existing database. If and error occured in a previous 41 | step, 42 | the error is passed through and no attempt to create the database is 43 | undertaken. 44 | 45 | `and_delete` delete the database if it is present, otherwise, it just 46 | returns 47 | the already absent state. If and error occured in a previous step, the 48 | error is 49 | passed through and no attempt to create the database is undertaken. 50 | 51 | 52 | ```rust 53 | pub trait FindDatabaseResult { 54 | type D: Database; 55 | 56 | fn or_create(self) -> Self; 57 | fn and_delete(self) -> Self; 58 | } 59 | 60 | impl FindDatabaseResult for BoxFuture, Error> { 61 | type D = D; 62 | 63 | fn or_create(self) -> Self { 64 | self.and_then({ |state| 65 | match state { 66 | DatabaseState::Existing(_) => finished(state).boxed(), 67 | DatabaseState::Absent(creator) => creator.create().and_then(|d| finished(DatabaseState::Existing(d))).boxed(), 68 | } 69 | }).boxed() 70 | } 71 | 72 | fn and_delete(self) -> Self { 73 | self.and_then({ |state| 74 | match state { 75 | DatabaseState::Absent(c) => finished(DatabaseState::Absent(c)).boxed(), 76 | DatabaseState::Existing(d) => d.destroy().and_then(|c| finished(DatabaseState::Absent(c))).boxed(), 77 | } 78 | }).boxed() 79 | } 80 | } 81 | ``` 82 | 83 | ### Results of retrieving documents 84 | 85 | `DocumentResult` decorates the result returned from retrieving a document 86 | from. 87 | The operations provided are `get`, `set` and `delete`. If the result is 88 | already 89 | describing an error, that error is propagated. 90 | 91 | `get` retrieves the document from the result and passes ownership to the 92 | caller. It consumes the result. Getting an absent document or a collided 93 | document is an error. 94 | 95 | `set` changes the document stored under the given key. It consumes the 96 | result 97 | and returns another one instead, describing the new state of the document or 98 | possibly an error. 99 | 100 | `delete` deletes the document stored under the given key. It consumes the 101 | result and returns another one instead, describing the new state of the 102 | document or possibly an error. 103 | 104 | ```rust 105 | pub trait DocumentResult { 106 | type K: Key; 107 | type D: Document; 108 | 109 | fn get(self) -> BoxFuture; 110 | fn set(self, doc: Self::D) -> Self; 111 | fn delete(self) -> Self; 112 | } 113 | 114 | impl DocumentResult for BoxFuture, Error> { 115 | type K = K; 116 | type D = D; 117 | 118 | fn get(self) -> BoxFuture { 119 | self.and_then(|entry| { 120 | let res = match entry { 121 | DatabaseEntry::Present { doc: d, .. } => Ok(d), 122 | DatabaseEntry::Absent { key, .. } => Err(key.id().to_string().into()), 123 | _ => panic!("conflicts are unimplemented"), 124 | }; 125 | done(res).boxed() 126 | }).boxed() 127 | } 128 | 129 | fn set(self, doc: D) -> Self { 130 | self.and_then(|entry| { 131 | match entry { 132 | DatabaseEntry::Absent { key, database: db, .. } | 133 | DatabaseEntry::Present { key, database: db, .. } => { 134 | db.insert(key, doc).and_then(|(key, doc)| { 135 | let new_entry = DatabaseEntry::Present { 136 | key: key, 137 | doc: doc, 138 | database: db, 139 | }; 140 | finished(new_entry) 141 | }) 142 | } 143 | DatabaseEntry::Conflicted { .. } => panic!("unimplemented"), 144 | } 145 | }).boxed() 146 | } 147 | 148 | fn delete(self) -> Self { 149 | self.and_then(|entry| { 150 | match entry { 151 | DatabaseEntry::Present { key, database: db, .. } => { 152 | // ignoring here is fine, the OK value is () 153 | db.delete(key.clone()).and_then( |_| { 154 | let new_entry = DatabaseEntry::Absent { 155 | key: key, 156 | database: db, 157 | }; 158 | finished(new_entry) 159 | }).boxed() 160 | } 161 | a @ DatabaseEntry::Absent { .. } => finished(a).boxed(), 162 | DatabaseEntry::Conflicted { .. } => panic!("unimplemented"), 163 | } 164 | }).boxed() 165 | } 166 | } 167 | ``` 168 | -------------------------------------------------------------------------------- /lazers-traits/src/lib.md: -------------------------------------------------------------------------------- 1 | # lazers-traits - laze RS interface 2 | 3 | ## General philosophy 4 | 5 | This library models the general interactions with CouchDB-like storages, 6 | be it a CouchDB server itself or a local K/V store with a CouchDB interface. 7 | 8 | ### Techniques 9 | 10 | * All operations return Results. An Error value describes a _failed 11 | interaction_, not a negative query result (such as a database missing). 12 | 13 | * Responses with multiple semantic meanings are mapped to enums. 14 | * The library uses decorations of Result types and these enums for easier 15 | access. 16 | 17 | ## Dependencies 18 | 19 | We use `serde`s definitions for serialisation/deserialisation. 20 | 21 | serde provides many features we want, including the ability to read 22 | documents 23 | in a typesafe manner. 24 | 25 | ```rust 26 | extern crate serde; 27 | ``` 28 | 29 | We use the error chain macro to provide the ability to wrap external errors 30 | in an easy fashion. 31 | 32 | ```rust 33 | #[macro_use] 34 | extern crate error_chain; 35 | ``` 36 | 37 | To express asynchronicity, we use futures-rs. 38 | 39 | ```rust 40 | extern crate futures; 41 | ``` 42 | 43 | ## Exports 44 | 45 | The library exports two modules. 46 | 47 | [`result`](/lazers-traits/src/result) defines our own `Result` type. See the module page for details. 48 | 49 | [`prelude`](/lazers-traits/src/prelude) exports all definitions needed for day-to-day work, this allows 50 | users 51 | to simply `use lazers_traits::prelude::*` instead of loading a huge block of 52 | codes imports themselves. 53 | 54 | [`decorations`](/lazers-traits/src/decorations) collects all convenience decorations of the library on, for 55 | example, `Result` types. 56 | 57 | ```rust 58 | pub mod result; 59 | pub mod prelude; 60 | pub mod decorations; 61 | ``` 62 | 63 | ## Use of externals 64 | 65 | We use a custom error created using error_chain!. 66 | 67 | ```rust 68 | use result::Error; 69 | ``` 70 | 71 | Futures, we use mainly through the BoxFuture interface. This carries with it the information that we expect all futures to be `Send`. 72 | 73 | ```rust 74 | use futures::BoxFuture; 75 | ``` 76 | 77 | We don't implement our own `Deserialize` and `Serialize` traits, but instead 78 | use the ones from serde. 79 | 80 | ```rust 81 | use serde::de::Deserialize; 82 | use serde::ser::Serialize; 83 | ``` 84 | 85 | We have to provide custom `Debug` implementations, so we import the trait. 86 | 87 | ```rust 88 | use std::fmt::Debug; 89 | ``` 90 | 91 | ## Definitions 92 | 93 | ### DatabaseName 94 | 95 | The DatabaseName is anything we can use to name a database. Currently, this 96 | type is just an alias for String. 97 | 98 | ```rust 99 | pub type DatabaseName = String; 100 | ``` 101 | 102 | ### Document 103 | 104 | CouchDB is all about handling documents, which means we have to find a 105 | definition for what constitutes a document. In our case, we decide that 106 | anything that can be serialised and deserialised by serde is a document. 107 | 108 | Also, we provide a blanket implementation that ensures that every type that 109 | is Deserialize and Serialize. 110 | 111 | The Document trait is a marker trait and holds no methods. 112 | 113 | Documents, as a design choice, don't hold information about the database 114 | they were loaded from. 115 | 116 | Finally, all Documents must be `Send`, as the represent plain data. 117 | 118 | ```rust 119 | pub trait Document: Deserialize + Serialize + Send {} 120 | 121 | impl Document for D {} 122 | ``` 123 | 124 | ### Key 125 | 126 | Keys are the main method of addressing Documents in CouchDB. As keys can 127 | take 128 | many forms and are regularly used to encode data, we only express the bare 129 | minimum as a trait. 130 | 131 | Keys also encode the revision of the current document. The revision is 132 | optional, but must be given for documents already in the database. 133 | 134 | Along with the trait definition, we ship the most basic implementation of it 135 | for users to use, a simple struct with a `String` key and an optional `rev` 136 | `String`. 137 | 138 | ```rust 139 | pub trait Key: Eq + Clone + Debug + Send { 140 | fn id(&self) -> &str; 141 | fn rev(&self) -> Option<&str>; 142 | fn from_id_and_rev(id: String, rev: Option) -> Self; 143 | } 144 | 145 | #[derive(Debug,Clone,PartialEq,Eq)] 146 | pub struct SimpleKey { 147 | pub id: String, 148 | pub rev: Option, 149 | } 150 | 151 | impl Key for SimpleKey { 152 | fn id(&self) -> &str { 153 | &self.id 154 | } 155 | 156 | fn rev(&self) -> Option<&str> { 157 | match self.rev { 158 | Some(ref string) => Some(string), 159 | None => None, 160 | } 161 | } 162 | 163 | fn from_id_and_rev(id: String, rev: Option) -> Self { 164 | SimpleKey { id: id, rev: rev } 165 | } 166 | } 167 | 168 | impl From for SimpleKey { 169 | fn from(string: String) -> SimpleKey { 170 | SimpleKey { 171 | id: string, 172 | rev: None, 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | ### The Client Trait 179 | 180 | The client trait is the entry point to all global storage level operations 181 | of 182 | CouchDB. Mostly, this is querying for named databases. 183 | 184 | Other operations are currently not supported. 185 | 186 | All operations return a result. 187 | 188 | ```rust 189 | pub trait Client: Default { 190 | type Database: Database; 191 | 192 | fn find_database(&self, name: DatabaseName) -> BoxFuture::Database as Database>::Creator>, Error>; 193 | fn id(&self) -> String; 194 | } 195 | ``` 196 | 197 | ### The DatabaseState Enum 198 | 199 | Querying for a database by name returns an enum describing two possible 200 | options: 201 | 202 | 1. The database exists. A handle to the database can be retrieved from the 203 | `Existing` variant. 204 | 205 | 2. The database is absent. In this case, the `Absent` variant holds the 206 | handle 207 | to a `DatabaseCreator`. The Creator can then be used to create the database. 208 | 209 | For simple querying, `existing` and `absent` methods are implemented. 210 | 211 | ```rust 212 | pub enum DatabaseState { 213 | Existing(D), 214 | Absent(C), 215 | } 216 | 217 | impl DatabaseState { 218 | pub fn absent(&self) -> bool { 219 | match self { 220 | &DatabaseState::Absent(_) => true, 221 | _ => false, 222 | } 223 | } 224 | 225 | pub fn existing(&self) -> bool { 226 | !self.absent() 227 | } 228 | } 229 | ``` 230 | 231 | ## The DatabaseCreator 232 | 233 | A DatabaseCreator trait describes the creation of a database of a _known_ 234 | name. 235 | 236 | It does not provide a way to create a database by passing a name, as it is 237 | intended for use with the DatabaseState enum only. Implementors should pass 238 | the 239 | name of the database to be created to the underlying structure. 240 | 241 | ```rust 242 | pub trait DatabaseCreator 243 | where Self: Sized + Send 244 | { 245 | type D: Database; 246 | 247 | fn create(self) -> BoxFuture; 248 | } 249 | ``` 250 | 251 | ### The `Database` trait 252 | 253 | The `Database` trait describes one `database` in CouchDB lingo. A database 254 | is a 255 | seperate key-value bucket, holding documents and design documents. 256 | 257 | ### Lifecycle 258 | 259 | A struct implementing the `Database` trait also allows destroying the 260 | database, 261 | which also deletes all documents along with it. 262 | 263 | Destroying the database is a consuming operation, returning a 264 | `DatabaseCreator` 265 | on success, to allow creating it again if wanted. 266 | 267 | ### DatabaseInfo 268 | 269 | DatabaseInfo provides access to several pieces of data a `CouchDB`-like database _must_ implement. Several clients might give richer information 270 | for which they should build additional interfaces. 271 | 272 | ```rust 273 | pub struct DatabaseInfo { 274 | instance_start_time: String, 275 | update_seq: UpdateSeq, 276 | } 277 | 278 | impl DatabaseInfo { 279 | pub fn new(instance_start_time: String, update_seq: UpdateSeq) -> DatabaseInfo { 280 | DatabaseInfo { 281 | instance_start_time: instance_start_time, 282 | update_seq: update_seq, 283 | } 284 | } 285 | 286 | pub fn instance_start_time(&self) -> &str { 287 | self.instance_start_time.as_ref() 288 | } 289 | 290 | pub fn update_seq(&self) -> &UpdateSeq { 291 | &self.update_seq 292 | } 293 | } 294 | ``` 295 | 296 | ### UpdateSeq 297 | 298 | The CouchDB update sequence is [either a number, a string or an array of a number and a string](https://github.com/pouchdb/pouchdb/issues/3220). This complexity should be hidden from users, but put into the respective clients. 299 | 300 | UpdateSeq is used in the replication protocol and checks if two databases have reached the same state. 301 | 302 | ```rust 303 | #[derive(Eq, PartialEq)] 304 | pub enum UpdateSeq { 305 | Numeric(u64), 306 | String(String), 307 | Pair(u64, String) 308 | } 309 | ``` 310 | 311 | ### Database access 312 | 313 | The methods for database access are all generic over the key and the 314 | document 315 | type(s) retrieved. Serialisation and Deserialisation failures are expressed 316 | as 317 | Errors. 318 | 319 | * `info`: retrieve general info about the database. 320 | 321 | * `doc`: returns a handle on a database entry, described in "The 322 | `DatabaseEntry` enum" 323 | 324 | * `insert`: directly inserts a document without previously retrieving 325 | information about it. Occuring conflicts are errors. 326 | 327 | * `delete`: directly deletes a document without previously retrieving 328 | information about it. Occuring conflicts or missing necessary revision 329 | information results in an error. 330 | 331 | ```rust 332 | pub trait Database 333 | where Self: Sized + Send 334 | { 335 | type Creator: DatabaseCreator; 336 | //type DBInfo: DatabaseInfo; 337 | 338 | //fn info(self) -> BoxFuture; 339 | fn destroy(self) -> BoxFuture; 340 | fn info(&self) -> BoxFuture; 341 | fn doc(&self, key: K) -> BoxFuture, Error>; 342 | fn insert(&self, key: K, doc: D) -> BoxFuture<(K, D), Error>; 343 | fn delete(&self, key: K) -> BoxFuture<(), Error>; 344 | } 345 | ``` 346 | 347 | ### The `DatabaseEntry` enum 348 | 349 | The `DatabaseEntry` enum describes the three possible states of an entry, 350 | queried by key, in a CouchDB database: 351 | 352 | * `Present`: There is a document for this key 353 | * `Absent`: There is no document for this key 354 | * `Conflicted` : There are conflicts for this key 355 | 356 | As this information makes no sense without knowing the database the key 357 | belongs 358 | to, all variants of `DatabaseEntry` hold a reference to the `Database` 359 | handle 360 | they result from. 361 | 362 | For all three variants, convenience constructors are provided. 363 | 364 | An entry is considered "existing" if there's either a document for this 365 | key, or 366 | a conflicts. An appropriate query method is provided. 367 | 368 | ```rust 369 | #[derive(Debug)] 370 | pub enum DatabaseEntry { 371 | Present { key: K, doc: D, database: DB }, 372 | Absent { key: K, database: DB }, 373 | Conflicted { 374 | key: K, 375 | documents: Vec, 376 | database: DB, 377 | }, 378 | } 379 | 380 | impl DatabaseEntry { 381 | pub fn present(key: K, doc: D, database: DB) -> DatabaseEntry { 382 | DatabaseEntry::Present { 383 | key: key, 384 | doc: doc, 385 | database: database, 386 | } 387 | } 388 | 389 | pub fn absent(key: K, database: DB) -> DatabaseEntry { 390 | DatabaseEntry::Absent { 391 | key: key, 392 | database: database, 393 | } 394 | } 395 | 396 | pub fn exists(&self) -> bool { 397 | match self { 398 | &DatabaseEntry::Present { .. } | 399 | &DatabaseEntry::Conflicted { .. } => true, 400 | _ => false, 401 | } 402 | } 403 | } 404 | ``` 405 | 406 | ### Decorations 407 | 408 | Standard operations over the described types are implemented as decorations 409 | and 410 | can be found in the `decorations` module. 411 | -------------------------------------------------------------------------------- /lazers-traits/src/prelude.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | pub use super::Client; 3 | pub use super::DatabaseName; 4 | pub use super::Database; 5 | pub use super::DatabaseState; 6 | pub use super::DatabaseCreator; 7 | pub use decorations::FindDatabaseResult; 8 | pub use decorations::DocumentResult; 9 | pub use super::Document; 10 | pub use super::DatabaseEntry; 11 | pub use super::Key; 12 | pub use super::DatabaseInfo; 13 | pub use super::UpdateSeq; 14 | pub use super::result::Result; 15 | pub use super::result::Error; 16 | pub use super::result::ErrorKind; 17 | pub use super::result::ChainErr; 18 | ``` 19 | -------------------------------------------------------------------------------- /lazers-traits/src/result.md: -------------------------------------------------------------------------------- 1 | ```rust 2 | error_chain! { 3 | // The type defined for this error. These are the conventional 4 | // and recommended names, but they can be arbitrarily chosen. 5 | types { 6 | Error, ErrorKind, ChainErr, Result; 7 | } 8 | 9 | // Automatic conversions between this error chain and other 10 | // error chains. In this case, it will e.g. generate an 11 | // `ErrorKind` variant called `Dist` which in turn contains 12 | // the `rustup_dist::ErrorKind`, with conversions from 13 | // `rustup_dist::Error`. 14 | // 15 | // This section can be empty. 16 | links { 17 | } 18 | 19 | // Automatic conversions between this error chain and other 20 | // error types not defined by the `error_chain!`. These will be 21 | // boxed as the error cause and wrapped in a new error with, 22 | // in this case, the `ErrorKind::Temp` variant. 23 | // 24 | // This section can be empty. 25 | foreign_links { 26 | 27 | } 28 | 29 | // Define additional `ErrorKind` variants. The syntax here is 30 | // the same as `quick_error!`, but the `from()` and `cause()` 31 | // syntax is not supported. 32 | errors { 33 | DocumentNotAvailable(key: String) { 34 | description("Document requested was not available") 35 | display("requested key: '{}'", key) 36 | } 37 | DatabaseCreationError(database: String) { 38 | description("Could not create database") 39 | display("Error creating Database: '{}'", database) 40 | } 41 | UpdateConflict(id: String) { 42 | description("Document conflict") 43 | display("Document conflicht: '{}'", id) 44 | } 45 | ClientError(desc: String) { 46 | description("Client error") 47 | display("Client error: '{}'", desc) 48 | } 49 | } 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /lazers-traits/tango-build.rs: -------------------------------------------------------------------------------- 1 | extern crate tango; 2 | 3 | fn main() { tango::process_root().unwrap() } 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | verbose = true 2 | wrap_comments = true 3 | write_mode = "replace" 4 | -------------------------------------------------------------------------------- /update_page.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ruby build.rb 6 | 7 | # Make a new repo for the gh-pages branch 8 | cd doc 9 | 10 | git init 11 | # Add, commit and push files 12 | git add --all . 13 | git commit -m "Built documentation" 14 | git checkout -b gh-pages 15 | git remote add origin git@github.com:skade/lazers.git 16 | git push -qf origin gh-pages 17 | 18 | # Cleanup 19 | rm -rf .git 20 | --------------------------------------------------------------------------------