├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── RELEASE_PROCESS.md ├── examples ├── client.rs └── custom-header.rs ├── src ├── error.rs ├── lib.rs ├── parser.rs ├── request.rs ├── transport.rs ├── utils.rs └── value.rs └── tests ├── python.rs └── version-numbers.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - staging 8 | - trying 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | RUSTFLAGS: "--deny warnings" 16 | MSRV: 1.54.0 17 | 18 | jobs: 19 | test: 20 | strategy: 21 | matrix: 22 | rust: 23 | - stable 24 | - nightly 25 | os: 26 | - ubuntu-latest 27 | - macOS-latest 28 | - windows-latest 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: ${{ matrix.rust }} 36 | override: true 37 | - name: Build 38 | run: cargo build --all --all-targets 39 | - name: Run tests 40 | run: > 41 | cargo test --all && 42 | cargo test --all --no-default-features && 43 | cargo test --all --no-default-features --features=http && 44 | cargo test --all --no-default-features --features=tls 45 | 46 | msrv: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: ${{ env.MSRV }} 54 | override: true 55 | - name: Build 56 | run: cargo build --verbose 57 | 58 | lint: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | toolchain: stable 66 | override: true 67 | components: rustfmt 68 | - name: Check code formatting 69 | run: cargo fmt -- --check 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | No changes. 6 | 7 | ## 0.15.1 - 2021-11-02 8 | 9 | ### New Features 10 | 11 | - Added `From>` impls for `Value`, yielding either `Nil` or the contained value ([#76]) 12 | 13 | [#76]: https://github.com/jonas-schievink/xml-rpc-rs/pull/76 14 | 15 | ## 0.15.0 - 2021-01-23 16 | 17 | ### Breaking Changes 18 | 19 | * Updated `iso8601` dependency to 0.4.0 20 | * Updated `reqwest` dependency to 0.11.0 21 | 22 | ### Misc 23 | 24 | * Changed request formatting to be more readable ([#68]) 25 | * Updated private dependencies 26 | 27 | [#68]: https://github.com/jonas-schievink/xml-rpc-rs/pull/68 28 | 29 | ## 0.14.0 - 2020-02-06 30 | 31 | ### Breaking Changes 32 | 33 | * Updated `iso8601` dependency to 0.3.0 34 | * Updated `reqwest` dependency to 0.10.1 35 | * Added a new default feature `tls` that can be disabled to turn off [reqwest]'s TLS support. 36 | 37 | [reqwest]: https://github.com/seanmonstar/reqwest 38 | 39 | ## 0.13.1 - 2019-02-20 40 | 41 | ### Misc 42 | 43 | * Update internal dependencies 44 | 45 | ## 0.13.0 - 2018-11-09 46 | 47 | ### Breaking Changes 48 | 49 | * Update reqwest to 0.9 to fix openssl-related build failures 50 | ([#44](https://github.com/jonas-schievink/xml-rpc-rs/pull/44)) 51 | 52 | ## 0.12.0 - 2018-08-24 53 | 54 | ### Breaking Changes 55 | 56 | * Bump the minimum supported Rust version and change the Rust version policy. 57 | 58 | From now on, `xmlrpc` will adopt the same policy as [tokio] (on which we 59 | depend): We will support the current Rust version and the 2 releases prior to 60 | that (which currently means that we support 1.25.0+). 61 | 62 | Bumping the required Rust version is no longer considered a breaking change as 63 | long as the latest 3 versions are still supported. 64 | 65 | [tokio]: https://github.com/tokio-rs/tokio 66 | 67 | ### New Features 68 | 69 | * Add `Request::new_multicall` for easier execution of multiple calls via `system.multicall` 70 | 71 | ### Bugfixes 72 | 73 | * Better handling of `Value::DateTime` 74 | * Print the timezone if the zone offset is non-zero 75 | * Print the fractional part of the time if it's non-zero 76 | * Accept base64 values containing whitespace 77 | 78 | ## 0.11.1 - 2018-05-14 79 | 80 | ### Bugfixes 81 | 82 | * Stop checking `Content-Length` headers to support compressed responses ([#41](https://github.com/jonas-schievink/xml-rpc-rs/pull/41)) 83 | 84 | ## 0.11.0 - 2018-02-25 85 | 86 | ### Breaking Changes 87 | 88 | * `Transport` errors must now be `Send + Sync`; this allows our own `Error` type to be `Send + Sync`, which makes it more useful for downstream crates (see: [API guidelines][c-good-err]) ([#39](https://github.com/jonas-schievink/xml-rpc-rs/pull/39)) 89 | 90 | ## 0.10.0 - 2018-02-21 91 | 92 | ### Breaking Changes 93 | 94 | * Replace ad-hoc API with a `Transport` trait that can be implemented to change the way the request is sent 95 | * Stricter checking of server headers 96 | * Removed the nested `Result` you get when performing a call 97 | * Restructure the `RequestError` type to better hide details the user shouldn't need to see 98 | * Rename `RequestError` to just `Error` to better match what other crates do 99 | * Removed the `RequestResult` type alias in favor of explicitly naming the result type 100 | 101 | ### New Features 102 | 103 | * Make the `reqwest` dependency optional - you can opt out and define your own `Transport` instead 104 | * Add `Request::call_url`, an easy to use helper that calls a `&str` URL without needing to depend on `reqwest` in downstream crates 105 | * Add the `http` module, containing a few helper methods for writing custom reqwest-based `Transport`s 106 | * Derive a few more useful traits ([#34](https://github.com/jonas-schievink/xml-rpc-rs/pull/34)) 107 | * Implement `From` for `Value` ([#33](https://github.com/jonas-schievink/xml-rpc-rs/pull/33)) 108 | * Add methods `Value::get` and `Value::as_*`, implement `std::ops::Index` for `Value` for convenient access to wrapped 109 | data ([#37](https://github.com/jonas-schievink/xml-rpc-rs/pull/37)). 110 | 111 | ## <= 0.9.0 112 | 113 | * The API slowly grew to expose more internals in order to accommodate more use cases 114 | 115 | [c-good-err]: https://rust-lang-nursery.github.io/api-guidelines/interoperability.html#c-good-err 116 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Jonas Schievink "] 3 | description = "An XML-RPC implementation for Rust" 4 | documentation = "https://docs.rs/xmlrpc/" 5 | repository = "https://github.com/jonas-schievink/xml-rpc-rs.git" 6 | keywords = ["xml", "rpc", "remote", "ipc"] 7 | categories = ["network-programming", "encoding"] 8 | readme = "README.md" 9 | license = "CC0-1.0" 10 | name = "xmlrpc" 11 | version = "0.15.1" 12 | 13 | # cargo-release configuration 14 | [package.metadata.release] 15 | tag-message = "{{version}}" 16 | no-dev-version = true 17 | pre-release-commit-message = "Release {{version}}" 18 | 19 | # Change the changelog's `Unreleased` section to refer to this release and prepend new `Unreleased` section 20 | [[package.metadata.release.pre-release-replacements]] 21 | file = "CHANGELOG.md" 22 | search = "## Unreleased" 23 | replace = "## Unreleased\n\nNo changes.\n\n## {{version}} - {{date}}" 24 | 25 | # Bump the version inside the example manifest in `README.md` 26 | [[package.metadata.release.pre-release-replacements]] 27 | file = "README.md" 28 | search = 'xmlrpc = "[a-z0-9\\.-]+"' 29 | replace = 'xmlrpc = "{{version}}"' 30 | 31 | # Bump the version referenced by the `html_root_url` attribute in `lib.rs` 32 | [[package.metadata.release.pre-release-replacements]] 33 | file = "src/lib.rs" 34 | search = "https://docs.rs/xmlrpc/[a-z0-9\\.-]+" 35 | replace = "https://docs.rs/xmlrpc/{{version}}" 36 | 37 | [badges] 38 | travis-ci = { repository = "jonas-schievink/xml-rpc-rs" } 39 | maintenance = { status = "actively-developed" } 40 | 41 | [dependencies] 42 | # public 43 | iso8601 = "0.4.0" 44 | reqwest = { version = "0.11.0", features = [ "blocking" ], default-features = false, optional = true } 45 | # private 46 | mime = { version = "0.3", optional = true } 47 | base64 = "0.13.0" 48 | xml-rs = "0.8.0" 49 | 50 | [dev-dependencies] 51 | version-sync = "0.9" 52 | 53 | [features] 54 | http = ["reqwest", "mime"] 55 | tls = ["reqwest/default-tls"] 56 | default = ["http", "tls"] 57 | 58 | [[example]] 59 | name = "client" 60 | required-features = ["http"] 61 | 62 | [[example]] 63 | name = "custom-header" 64 | required-features = ["http"] 65 | 66 | [[test]] 67 | name = "python" 68 | harness = false 69 | required-features = ["http"] 70 | 71 | [[test]] 72 | name = "version-numbers" 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XML-RPC for Rust 2 | 3 | [![crates.io](https://img.shields.io/crates/v/xmlrpc.svg)](https://crates.io/crates/xmlrpc) 4 | [![docs.rs](https://docs.rs/xmlrpc/badge.svg)](https://docs.rs/xmlrpc/) 5 | ![CI](https://github.com/jonas-schievink/xml-rpc-rs/workflows/CI/badge.svg) 6 | 7 | This crate provides a simple implementation of the [XML-RPC specification](http://xmlrpc.scripting.com/spec.html) in stable Rust using `xml-rs` and `reqwest`. 8 | 9 | Please refer to the [changelog](CHANGELOG.md) to see what changed in the last releases. 10 | 11 | ## Rust support 12 | 13 | This crate uses the same Rust versioning policy as [tokio]: It supports the last 14 | 3 stable Rust releases. Increasing the minimum supported version is not 15 | considered a breaking change as long as the latest 3 versions are still 16 | supported. 17 | 18 | ## Usage 19 | 20 | Start by adding an entry to your `Cargo.toml`: 21 | 22 | ```toml 23 | [dependencies] 24 | xmlrpc = "0.15.1" 25 | ``` 26 | 27 | Then import the crate into your Rust code: 28 | 29 | ```rust 30 | extern crate xmlrpc; 31 | ``` 32 | 33 | See [`examples/client.rs`](examples/client.rs) for a small example which connects to a running Python XML-RPC server and calls a method. A more elaborate example that demonstrates how to implement a custom `Transport` to set a cookie header is provided in [`examples/custom-header.rs`](examples/custom-header.rs). 34 | 35 | [tokio]: https://github.com/tokio-rs/tokio 36 | -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # What to do to publish a new release 2 | 3 | 1. Ensure all notable changes are in the [changelog](CHANGELOG.md) under "Unreleased". 4 | 2. Execute `cargo release ` to bump version(s), tag and publish everything. 5 | External subcommand, must be installed with `cargo install cargo-release`. 6 | 7 | `` can be one of `major|minor|patch`. 8 | 3. Go to GitHub and "add release notes" to the just-pushed tag. Copy them from the changelog. 9 | -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | //! You can use this example by executing `python3 -m xmlrpc.server` and then running 2 | //! `cargo run --example client`. 3 | 4 | extern crate xmlrpc; 5 | 6 | use xmlrpc::{Request, Value}; 7 | 8 | fn main() { 9 | // The Python example server exports Python's `pow` method. Let's call it! 10 | let pow_request = Request::new("pow").arg(2).arg(8); // Compute 2**8 11 | 12 | let request_result = pow_request.call_url("http://127.0.0.1:8000"); 13 | 14 | println!("Result: {:?}", request_result); 15 | 16 | let pow_result = request_result.unwrap(); 17 | assert_eq!(pow_result, Value::Int(2i32.pow(8))); 18 | } 19 | -------------------------------------------------------------------------------- /examples/custom-header.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to transmit a request with a custom HTTP header. 2 | 3 | extern crate reqwest; 4 | extern crate xmlrpc; 5 | 6 | use xmlrpc::http::{build_headers, check_response}; 7 | use xmlrpc::{Request, Transport}; 8 | 9 | use reqwest::blocking::{Client, RequestBuilder, Response}; 10 | use reqwest::header::COOKIE; 11 | 12 | use std::error::Error; 13 | 14 | /// Custom transport that adds a cookie header. 15 | struct MyTransport(RequestBuilder); 16 | 17 | impl Transport for MyTransport { 18 | type Stream = Response; 19 | 20 | fn transmit(self, request: &Request) -> Result> { 21 | let mut body = Vec::new(); 22 | request 23 | .write_as_xml(&mut body) 24 | .expect("could not write request to buffer (this should never happen)"); 25 | 26 | let response = build_headers(self.0, body.len() as u64) 27 | .header(COOKIE, "SESSION=123abc") // Our custom header will be a `Cookie` header 28 | .body(body) 29 | .send()?; 30 | 31 | check_response(&response)?; 32 | 33 | Ok(response) 34 | } 35 | } 36 | 37 | fn main() { 38 | let request = Request::new("pow").arg(2).arg(8); 39 | 40 | let tp = MyTransport(Client::new().post("http://localhost/target")); 41 | let result = request.call(tp); 42 | 43 | println!("Result: {:?}", result); 44 | } 45 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Defines error types used by this library. 2 | 3 | use Value; 4 | 5 | use xml::common::TextPosition; 6 | use xml::reader::Error as XmlError; 7 | 8 | use std::collections::BTreeMap; 9 | use std::fmt::{self, Display, Formatter}; 10 | use std::{error, io}; 11 | 12 | /// Errors that can occur when trying to perform an XML-RPC request. 13 | /// 14 | /// This can be a lower-level error (for example, the HTTP request failed), a problem with the 15 | /// server (maybe it's not implementing XML-RPC correctly), or just a failure to execute the 16 | /// operation. 17 | #[derive(Debug)] 18 | pub struct Error(RequestErrorKind); 19 | 20 | impl Error { 21 | /// If this `Error` was caused by the server responding with a `` response, 22 | /// returns the `Fault` in question. 23 | pub fn fault(&self) -> Option<&Fault> { 24 | match self.0 { 25 | RequestErrorKind::Fault(ref fault) => Some(fault), 26 | _ => None, 27 | } 28 | } 29 | } 30 | 31 | #[doc(hidden)] // hide internal impl 32 | impl From for Error { 33 | fn from(kind: RequestErrorKind) -> Self { 34 | Error(kind) 35 | } 36 | } 37 | 38 | impl Display for Error { 39 | fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result { 40 | self.0.fmt(fmt) 41 | } 42 | } 43 | 44 | impl error::Error for Error { 45 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 46 | self.0.source() 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | pub enum RequestErrorKind { 52 | /// The response could not be parsed. This can happen when the server doesn't correctly 53 | /// implement the XML-RPC spec. 54 | ParseError(ParseError), 55 | 56 | /// A communication error originating from the transport used to perform the request. 57 | TransportError(Box), 58 | 59 | /// The server returned a `` response, indicating that the execution of the call 60 | /// encountered a problem (for example, an invalid (number of) arguments was passed). 61 | Fault(Fault), 62 | } 63 | 64 | impl From for RequestErrorKind { 65 | fn from(e: ParseError) -> Self { 66 | RequestErrorKind::ParseError(e) 67 | } 68 | } 69 | 70 | impl From for RequestErrorKind { 71 | fn from(f: Fault) -> Self { 72 | RequestErrorKind::Fault(f) 73 | } 74 | } 75 | 76 | impl Display for RequestErrorKind { 77 | fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result { 78 | match *self { 79 | RequestErrorKind::ParseError(ref err) => write!(fmt, "parse error: {}", err), 80 | RequestErrorKind::TransportError(ref err) => write!(fmt, "transport error: {}", err), 81 | RequestErrorKind::Fault(ref err) => write!(fmt, "{}", err), 82 | } 83 | } 84 | } 85 | 86 | impl error::Error for RequestErrorKind { 87 | fn cause(&self) -> Option<&dyn error::Error> { 88 | match *self { 89 | RequestErrorKind::ParseError(ref err) => Some(err), 90 | RequestErrorKind::TransportError(ref err) => Some(err.as_ref()), 91 | RequestErrorKind::Fault(ref err) => Some(err), 92 | } 93 | } 94 | } 95 | 96 | /// Describes possible error that can occur when parsing a `Response`. 97 | #[derive(Debug, PartialEq)] 98 | pub enum ParseError { 99 | /// Error while parsing (malformed?) XML. 100 | XmlError(XmlError), 101 | 102 | /// Could not parse the given CDATA as XML-RPC value. 103 | /// 104 | /// For example, `AAA` describes an invalid value. 105 | InvalidValue { 106 | /// The type for which an invalid value was supplied (eg. `int` or `dateTime.iso8601`). 107 | for_type: &'static str, 108 | /// The value we encountered, as a string. 109 | found: String, 110 | /// The position of the invalid value inside the XML document. 111 | position: TextPosition, 112 | }, 113 | 114 | /// Found an unexpected tag, attribute, etc. 115 | UnexpectedXml { 116 | /// A short description of the kind of data that was expected. 117 | expected: String, 118 | found: Option, 119 | /// The position of the unexpected data inside the XML document. 120 | position: TextPosition, 121 | }, 122 | } 123 | 124 | impl From for ParseError { 125 | fn from(e: XmlError) -> Self { 126 | ParseError::XmlError(e) 127 | } 128 | } 129 | 130 | impl From for ParseError { 131 | fn from(e: io::Error) -> Self { 132 | ParseError::XmlError(XmlError::from(e)) 133 | } 134 | } 135 | 136 | impl Display for ParseError { 137 | fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result { 138 | match *self { 139 | ParseError::XmlError(ref err) => write!(fmt, "malformed XML: {}", err), 140 | ParseError::InvalidValue { 141 | for_type, 142 | ref found, 143 | ref position, 144 | } => write!( 145 | fmt, 146 | "invalid value for type '{}' at {}: {}", 147 | for_type, position, found 148 | ), 149 | ParseError::UnexpectedXml { 150 | ref expected, 151 | ref position, 152 | found: None, 153 | } => write!( 154 | fmt, 155 | "unexpected XML at {} (expected {})", 156 | position, expected 157 | ), 158 | ParseError::UnexpectedXml { 159 | ref expected, 160 | ref position, 161 | found: Some(ref found), 162 | } => write!( 163 | fmt, 164 | "unexpected XML at {} (expected {}, found {})", 165 | position, expected, found 166 | ), 167 | } 168 | } 169 | } 170 | 171 | impl error::Error for ParseError {} 172 | 173 | /// A `` response, indicating that a request failed. 174 | /// 175 | /// The XML-RPC specification requires that a `` and `` is returned in the 176 | /// `` case, further describing the error. 177 | #[derive(Debug, PartialEq, Eq)] 178 | pub struct Fault { 179 | /// An application-specific error code. 180 | pub fault_code: i32, 181 | /// Human-readable error description. 182 | pub fault_string: String, 183 | } 184 | 185 | impl Fault { 186 | /// Creates a `Fault` from a `Value`. 187 | /// 188 | /// The `Value` must be a `Value::Struct` with a `faultCode` and `faultString` field (and no 189 | /// other fields). 190 | /// 191 | /// Returns `None` if the value isn't a valid `Fault`. 192 | pub fn from_value(value: &Value) -> Option { 193 | match *value { 194 | Value::Struct(ref map) => { 195 | if map.len() != 2 { 196 | // incorrect field count 197 | return None; 198 | } 199 | 200 | match (map.get("faultCode"), map.get("faultString")) { 201 | (Some(&Value::Int(fault_code)), Some(&Value::String(ref fault_string))) => { 202 | Some(Fault { 203 | fault_code, 204 | fault_string: fault_string.to_string(), 205 | }) 206 | } 207 | _ => None, 208 | } 209 | } 210 | _ => None, 211 | } 212 | } 213 | 214 | /// Turns this `Fault` into an equivalent `Value`. 215 | /// 216 | /// The returned value can be parsed back into a `Fault` using `Fault::from_value` or returned 217 | /// as a `` error response by serializing it into a `` tag. 218 | pub fn to_value(&self) -> Value { 219 | let mut map = BTreeMap::new(); 220 | map.insert("faultCode".to_string(), Value::from(self.fault_code)); 221 | map.insert( 222 | "faultString".to_string(), 223 | Value::from(self.fault_string.as_ref()), 224 | ); 225 | 226 | Value::Struct(map) 227 | } 228 | } 229 | 230 | impl Display for Fault { 231 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 232 | write!(f, "{} ({})", self.fault_string, self.fault_code) 233 | } 234 | } 235 | 236 | impl error::Error for Fault {} 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use super::*; 241 | 242 | use std::error; 243 | 244 | #[test] 245 | fn fault_roundtrip() { 246 | let input = Fault { 247 | fault_code: -123456, 248 | fault_string: "The Bald Lazy House Jumps Over The Hyperactive Kitten".to_string(), 249 | }; 250 | 251 | assert_eq!(Fault::from_value(&input.to_value()), Some(input)); 252 | } 253 | 254 | #[test] 255 | fn error_impls_error() { 256 | fn assert_error() {} 257 | 258 | assert_error::(); 259 | } 260 | 261 | #[test] 262 | fn error_is_send_sync() { 263 | fn assert_send_sync() {} 264 | 265 | assert_send_sync::(); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An XML-RPC implementation in Rust. 2 | //! 3 | //! The `xmlrpc` crate provides a minimal implementation of the [XML-RPC specification]. 4 | //! 5 | //! [XML-RPC specification]: http://xmlrpc.scripting.com/spec.html 6 | 7 | #![doc(html_root_url = "https://docs.rs/xmlrpc/0.15.1")] 8 | #![warn(missing_debug_implementations)] 9 | #![warn(rust_2018_idioms)] 10 | #![warn(missing_docs)] 11 | 12 | extern crate base64; 13 | extern crate iso8601; 14 | extern crate xml; 15 | 16 | mod error; 17 | mod parser; 18 | mod request; 19 | mod transport; 20 | mod utils; 21 | mod value; 22 | 23 | pub use error::{Error, Fault}; 24 | pub use request::Request; 25 | pub use transport::Transport; 26 | pub use value::{Index, Value}; 27 | 28 | #[cfg(feature = "http")] 29 | pub use transport::http; 30 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | //! XML-RPC response parser. 2 | 3 | use error::ParseError; 4 | use {Fault, Value}; 5 | 6 | use base64; 7 | use iso8601::datetime; 8 | use std::collections::BTreeMap; 9 | use std::io::{self, ErrorKind, Read}; 10 | use xml::common::Position; 11 | use xml::reader::{EventReader, XmlEvent}; 12 | use xml::ParserConfig; 13 | 14 | /// A response from the server. 15 | /// 16 | /// XML-RPC specifies that a call should either return a single `Value`, or a ``. 17 | pub type Response = Result; 18 | 19 | type ParseResult = Result; 20 | 21 | pub struct Parser<'a, R: Read> { 22 | reader: EventReader<&'a mut R>, 23 | /// Current "token". The parser makes decisions based on this token, then pulls the next one 24 | /// from `reader`. 25 | cur: XmlEvent, 26 | } 27 | 28 | impl<'a, R: Read> Parser<'a, R> { 29 | pub fn new(reader: &'a mut R) -> ParseResult { 30 | let reader = EventReader::new_with_config( 31 | reader, 32 | ParserConfig { 33 | cdata_to_characters: true, 34 | ..Default::default() 35 | }, 36 | ); 37 | 38 | let mut parser = Parser { 39 | cur: XmlEvent::EndDocument, // dummy value 40 | reader, 41 | }; 42 | parser.next()?; 43 | Ok(parser) 44 | } 45 | 46 | /// Disposes `self.cur` and pulls the next event from the XML parser to replace it. 47 | fn next(&mut self) -> ParseResult<()> { 48 | loop { 49 | let event = self.reader.next()?; 50 | match event { 51 | XmlEvent::StartDocument { .. } 52 | | XmlEvent::Comment(_) 53 | | XmlEvent::Whitespace(_) 54 | | XmlEvent::ProcessingInstruction { .. } => continue, // skip these 55 | XmlEvent::StartElement { 56 | ref attributes, 57 | ref name, 58 | .. 59 | } => { 60 | if name.namespace.is_some() || name.prefix.is_some() { 61 | return self.expected("tag without namespace or prefix"); 62 | } 63 | if !attributes.is_empty() { 64 | return self.expected(format!("tag <{}> without attributes", name)); 65 | } 66 | } 67 | XmlEvent::EndElement { ref name } => { 68 | if name.namespace.is_some() || name.prefix.is_some() { 69 | return self.expected("tag without namespace or prefix"); 70 | } 71 | } 72 | XmlEvent::EndDocument | XmlEvent::CData(_) | XmlEvent::Characters(_) => {} 73 | } 74 | 75 | self.cur = event; 76 | return Ok(()); 77 | } 78 | } 79 | 80 | /// Expects that the current token is an opening tag like `` without attributes (and a 81 | /// local name without namespaces). If not, returns an error. 82 | fn expect_open(&mut self, tag: &str) -> ParseResult<()> { 83 | match self.cur { 84 | XmlEvent::StartElement { ref name, .. } if name.local_name == tag => {} 85 | _ => return self.expected(format!("<{}>", tag)), 86 | } 87 | self.next()?; 88 | Ok(()) 89 | } 90 | 91 | /// Expects that the current token is a closing tag like `` with a local name without 92 | /// namespaces. If not, returns an error. 93 | fn expect_close(&mut self, tag: &str) -> ParseResult<()> { 94 | match self.cur { 95 | XmlEvent::EndElement { ref name } if name.local_name == tag => {} 96 | _ => return self.expected(format!("", tag)), 97 | } 98 | self.next()?; 99 | Ok(()) 100 | } 101 | 102 | /// Expects that the current token is a characters sequence. Parses and returns a value. 103 | fn expect_value( 104 | &mut self, 105 | for_type: &'static str, 106 | parse: impl Fn(&str) -> Result, 107 | ) -> ParseResult { 108 | let value = match self.cur { 109 | XmlEvent::Characters(ref string) => { 110 | parse(string).map_err(|_| self.invalid_value(for_type, string.to_owned()))? 111 | } 112 | _ => return self.expected("characters"), 113 | }; 114 | self.next()?; 115 | Ok(value) 116 | } 117 | 118 | /// Builds and returns an `Err(UnexpectedXml)`. 119 | fn expected(&self, expected: E) -> ParseResult { 120 | let expected = expected.to_string(); 121 | let position = self.reader.position(); 122 | 123 | Err(ParseError::UnexpectedXml { 124 | expected, 125 | position, 126 | found: match self.cur { 127 | XmlEvent::StartElement { ref name, .. } => Some(format!("<{}>", name)), 128 | XmlEvent::EndElement { ref name, .. } => Some(format!("", name)), 129 | XmlEvent::EndDocument => Some("end of data".to_string()), 130 | XmlEvent::Characters(ref data) | XmlEvent::CData(ref data) => { 131 | Some(format!("\"{}\"", data)) 132 | } 133 | _ => None, 134 | }, 135 | }) 136 | } 137 | 138 | fn invalid_value(&self, for_type: &'static str, value: String) -> ParseError { 139 | // FIXME: It might be neat to preserve the original error as the cause 140 | ParseError::InvalidValue { 141 | for_type, 142 | found: value, 143 | position: self.reader.position(), 144 | } 145 | } 146 | 147 | fn parse_response(&mut self) -> ParseResult { 148 | let response: Response; 149 | 150 | // 151 | self.expect_open("methodResponse")?; 152 | 153 | // / 154 | match self.cur.clone() { 155 | XmlEvent::StartElement { ref name, .. } => { 156 | if name.local_name == "fault" { 157 | self.next()?; 158 | let value = self.parse_value()?; 159 | let fault = Fault::from_value(&value) 160 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "malformed "))?; 161 | response = Err(fault); 162 | } else if name.local_name == "params" { 163 | self.next()?; 164 | // 165 | self.expect_open("param")?; 166 | 167 | let value = self.parse_value()?; 168 | response = Ok(value); 169 | 170 | // 171 | self.expect_close("param")?; 172 | } else { 173 | return self.expected(format!(" or , got {}", name)); 174 | } 175 | } 176 | _ => return self.expected(" or "), 177 | } 178 | 179 | Ok(response) 180 | } 181 | 182 | fn parse_value(&mut self) -> ParseResult { 183 | // 184 | self.expect_open("value")?; 185 | 186 | if let Ok(()) = self.expect_close("value") { 187 | // empty value, parse as empty string 188 | return Ok(Value::String(String::new())); 189 | } 190 | 191 | let value = self.parse_value_inner()?; 192 | 193 | // 194 | self.expect_close("value")?; 195 | 196 | Ok(value) 197 | } 198 | 199 | fn parse_value_inner(&mut self) -> ParseResult { 200 | let value = match self.cur.clone() { 201 | // Raw string or specific type tag 202 | XmlEvent::StartElement { ref name, .. } => { 203 | let name = &*name.local_name; 204 | match name { 205 | "struct" => { 206 | self.next()?; 207 | let mut members = BTreeMap::new(); 208 | loop { 209 | if let Ok(_) = self.expect_close("struct") { 210 | break; 211 | } 212 | 213 | self.expect_open("member")?; 214 | // 215 | 216 | // NAME 217 | self.expect_open("name")?; 218 | let name = match self.cur { 219 | XmlEvent::Characters(ref string) => string.clone(), 220 | _ => return self.expected("characters"), 221 | }; 222 | self.next()?; 223 | self.expect_close("name")?; 224 | 225 | // Value 226 | let value = self.parse_value()?; 227 | 228 | // 229 | self.expect_close("member")?; 230 | 231 | members.insert(name, value); 232 | } 233 | 234 | Value::Struct(members) 235 | } 236 | "array" => { 237 | self.next()?; 238 | let mut elements: Vec = Vec::new(); 239 | self.expect_open("data")?; 240 | loop { 241 | if let Ok(_) = self.expect_close("data") { 242 | break; 243 | } 244 | 245 | elements.push(self.parse_value()?); 246 | } 247 | self.expect_close("array")?; 248 | Value::Array(elements) 249 | } 250 | "nil" => { 251 | self.next()?; 252 | self.expect_close("nil")?; 253 | Value::Nil 254 | } 255 | "string" => { 256 | self.next()?; 257 | let string = match self.cur.clone() { 258 | XmlEvent::Characters(string) => { 259 | self.next()?; 260 | self.expect_close("string")?; 261 | string 262 | } 263 | XmlEvent::EndElement { ref name } if name.local_name == "string" => { 264 | self.next()?; 265 | String::new() 266 | } 267 | _ => return self.expected("characters or "), 268 | }; 269 | Value::String(string) 270 | } 271 | "base64" => { 272 | self.next()?; 273 | let data = match self.cur.clone() { 274 | XmlEvent::Characters(ref string) => { 275 | self.next()?; 276 | self.expect_close("base64")?; 277 | 278 | let stripped: Vec<_> = string 279 | .bytes() 280 | .filter(|b| !b" \n\t\r\x0b\x0c".contains(b)) 281 | .collect(); 282 | base64::decode(&stripped) 283 | .map_err(|_| self.invalid_value("base64", string.to_string()))? 284 | } 285 | XmlEvent::EndElement { ref name } if name.local_name == "base64" => { 286 | self.next()?; 287 | Vec::new() 288 | } 289 | _ => return self.expected("characters or "), 290 | }; 291 | Value::Base64(data) 292 | } 293 | "i4" | "int" => { 294 | self.next()?; 295 | let value = self 296 | .expect_value("integer", |data| data.parse::().map(Value::Int))?; 297 | self.expect_close(name)?; 298 | value 299 | } 300 | "i8" => { 301 | self.next()?; 302 | let value = 303 | self.expect_value("i8", |data| data.parse::().map(Value::Int64))?; 304 | self.expect_close(name)?; 305 | value 306 | } 307 | "boolean" => { 308 | self.next()?; 309 | let value = self.expect_value("boolean", |data| match data { 310 | "0" => Ok(Value::Bool(false)), 311 | "1" => Ok(Value::Bool(true)), 312 | _ => Err(()), 313 | })?; 314 | self.expect_close(name)?; 315 | value 316 | } 317 | "double" => { 318 | self.next()?; 319 | let value = self.expect_value("double", |data| { 320 | data.parse::().map(Value::Double) 321 | })?; 322 | self.expect_close(name)?; 323 | value 324 | } 325 | "dateTime.iso8601" => { 326 | self.next()?; 327 | let value = self.expect_value("dateTime.iso8601", |data| { 328 | datetime(data).map(Value::DateTime) 329 | })?; 330 | self.expect_close(name)?; 331 | value 332 | } 333 | _ => return self.expected("valid type tag"), 334 | } 335 | } 336 | XmlEvent::Characters(string) => { 337 | self.next()?; 338 | Value::String(string) 339 | } 340 | _ => return self.expected("type tag or characters"), 341 | }; 342 | 343 | Ok(value) 344 | } 345 | } 346 | 347 | /// Parses a response from an XML reader. 348 | pub fn parse_response(reader: &mut R) -> ParseResult { 349 | Parser::new(reader)?.parse_response() 350 | } 351 | 352 | #[cfg(test)] 353 | mod tests { 354 | use super::*; 355 | 356 | use error::Fault; 357 | use Value; 358 | 359 | use std::fmt::Debug; 360 | use std::iter; 361 | 362 | fn read_response(xml: &str) -> ParseResult { 363 | parse_response(&mut xml.as_bytes()) 364 | } 365 | 366 | fn read_value(xml: &str) -> ParseResult { 367 | Parser::new(&mut xml.as_bytes())?.parse_value() 368 | } 369 | 370 | /// Test helper function that will panic with the `Err` if a `Result` is not an `Ok`. 371 | fn assert_ok(result: Result) { 372 | match result { 373 | Ok(_) => {} 374 | Err(e) => panic!("assert_ok called on Err value: {:?}", e), 375 | } 376 | } 377 | 378 | /// Test helper function that will panic with the `Ok` if a `Result` is not an `Err`. 379 | fn assert_err(result: Result) { 380 | match result { 381 | Ok(t) => panic!("assert_err called on Ok value: {:?}", t), 382 | Err(_) => {} 383 | } 384 | } 385 | 386 | #[test] 387 | fn parses_base64_response() { 388 | assert_ok(read_response( 389 | r##" 390 | 391 | 392 | 393 | 394 | 0J/QvtC10YXQsNC70Lgh 395 | 396 | 397 | 398 | "##, 399 | )); 400 | } 401 | 402 | #[test] 403 | fn parses_response() { 404 | assert_ok(read_response( 405 | r##" 406 | 407 | 408 | 409 | 410 | teststring 411 | 412 | 413 | 414 | "##, 415 | )); 416 | } 417 | 418 | #[test] 419 | fn parses_fault() { 420 | assert_eq!( 421 | read_response( 422 | r##" 423 | 424 | 425 | 426 | 427 | 428 | 429 | faultCode 430 | 4 431 | 432 | 433 | faultString 434 | Too many parameters. 435 | 436 | 437 | 438 | 439 | "## 440 | ), 441 | Ok(Err(Fault { 442 | fault_code: 4, 443 | fault_string: "Too many parameters.".into(), 444 | })) 445 | ); 446 | } 447 | 448 | #[test] 449 | fn rejects_additional_fault_fields() { 450 | // "A struct may not contain members other than those specified." 451 | 452 | assert_err(read_response( 453 | r##" 454 | 455 | 456 | 457 | 458 | 459 | 460 | faultCode 461 | 4 462 | 463 | 464 | faultString 465 | Too many parameters. 466 | 467 | 468 | unnecessaryParameter 469 | Too many parameters. 470 | 471 | 472 | 473 | 474 | "##, 475 | )); 476 | } 477 | 478 | #[test] 479 | fn rejects_invalid_faults() { 480 | // Make sure to reject type errors in s - They're specified to contain specifically 481 | // typed fields. 482 | assert_err(read_response( 483 | r##" 484 | 485 | 486 | 487 | 488 | 489 | 490 | faultCode 491 | I'm not an int! 492 | 493 | 494 | faultString 495 | Too many parameters. 496 | 497 | 498 | 499 | 500 | "##, 501 | )); 502 | 503 | assert_err(read_response( 504 | r##" 505 | 506 | 507 | 508 | 509 | 510 | 511 | faultCode 512 | 4 513 | 514 | 515 | faultString 516 | I'm not a string! 517 | 518 | 519 | 520 | 521 | "##, 522 | )); 523 | } 524 | 525 | #[test] 526 | fn parses_string_value_with_whitespace() { 527 | assert_eq!( 528 | read_value(" I'm a string! "), 529 | Ok(Value::String(" I'm a string! ".into())) 530 | ); 531 | } 532 | 533 | #[test] 534 | fn parses_64bit_int() { 535 | assert_eq!( 536 | read_value("12345"), 537 | Ok(Value::Int64(12345)) 538 | ); 539 | assert_eq!( 540 | read_value("-100100100100"), 541 | Ok(Value::Int64(-100100100100)) 542 | ); 543 | } 544 | 545 | #[test] 546 | fn parses_int_with_plus_sign() { 547 | // "You can include a plus or minus at the beginning of a string of numeric characters." 548 | assert_eq!( 549 | read_value("+1234"), 550 | Ok(Value::Int(1234)) 551 | ); 552 | } 553 | 554 | #[test] 555 | fn parses_date_values() { 556 | assert_ok(read_value( 557 | "2015-02-18T23:16:09Z", 558 | )); 559 | assert_ok(read_value( 560 | "19980717T14:08:55", 561 | )); 562 | assert_err(read_value( 563 | "", 564 | )); 565 | assert_err(read_value( 566 | "ILLEGAL VALUE :(", 567 | )); 568 | } 569 | 570 | #[test] 571 | fn parses_base64() { 572 | assert_eq!( 573 | read_value("0J/QvtC10YXQsNC70Lgh"), 574 | Ok(Value::Base64("Поехали!".bytes().collect())) 575 | ); 576 | 577 | assert_eq!( 578 | read_value(" 0J/Qv tC10YXQ sNC70 Lgh "), 579 | Ok(Value::Base64("Поехали!".bytes().collect())) 580 | ); 581 | 582 | assert_eq!( 583 | read_value("\n0J/QvtC10\nYXQsNC7\n0Lgh\n"), 584 | Ok(Value::Base64("Поехали!".bytes().collect())) 585 | ); 586 | } 587 | 588 | #[test] 589 | fn parses_empty_base64() { 590 | assert_eq!( 591 | read_value(""), 592 | Ok(Value::Base64(Vec::new())) 593 | ); 594 | assert_eq!( 595 | read_value(""), 596 | Ok(Value::Base64(Vec::new())) 597 | ); 598 | } 599 | 600 | #[test] 601 | fn parses_array_values() { 602 | assert_eq!( 603 | read_value( 604 | r#" 605 | 606 | 5 607 | a 608 | "# 609 | ), 610 | Ok(Value::Array(vec![Value::Int(5), Value::String("a".into())])) 611 | ); 612 | } 613 | 614 | #[test] 615 | fn parses_raw_value_as_string() { 616 | assert_eq!( 617 | read_value("\t I'm a string! "), 618 | Ok(Value::String("\t I'm a string! ".into())) 619 | ); 620 | } 621 | 622 | #[test] 623 | fn parses_nil_values() { 624 | assert_eq!(read_value(""), Ok(Value::Nil)); 625 | assert_eq!(read_value(""), Ok(Value::Nil)); 626 | assert_err(read_value("ILLEGAL")); 627 | } 628 | 629 | #[test] 630 | fn unescapes_values() { 631 | assert_eq!( 632 | read_value("abc<abc&abc"), 633 | Ok(Value::String("abc"), 641 | Ok(Value::String(String::new())) 642 | ); 643 | assert_eq!( 644 | read_value(""), 645 | Ok(Value::String(String::new())) 646 | ); 647 | } 648 | 649 | #[test] 650 | fn parses_empty_value_as_string() { 651 | assert_eq!( 652 | read_value(""), 653 | Ok(Value::String(String::new())) 654 | ); 655 | } 656 | 657 | #[test] 658 | fn rejects_attributes() { 659 | assert_err(read_value( 660 | r#"\t I'm a string! "#, 661 | )); 662 | 663 | assert_err(read_response( 664 | r##" 665 | 666 | 667 | 668 | 669 | teststring 670 | 671 | 672 | 673 | "##, 674 | )); 675 | assert_err(read_response( 676 | r##" 677 | 678 | 679 | 680 | 681 | teststring 682 | 683 | 684 | 685 | "##, 686 | )); 687 | assert_err(read_response( 688 | r##" 689 | 690 | 691 | 692 | 693 | teststring 694 | 695 | 696 | 697 | "##, 698 | )); 699 | assert_err(read_response( 700 | r##" 701 | 702 | 703 | 704 | 705 | teststring 706 | 707 | 708 | 709 | "##, 710 | )); 711 | assert_err(read_response( 712 | r##" 713 | 714 | 715 | 716 | 717 | 4 718 | 719 | 720 | 721 | "##, 722 | )); 723 | } 724 | 725 | #[test] 726 | fn error_messages() { 727 | fn errstr(value: &str) -> String { 728 | read_value(value).unwrap_err().to_string() 729 | } 730 | 731 | assert_eq!( 732 | errstr(r#"\t I'm a string! "#), 733 | "unexpected XML at 1:1 (expected tag without attributes, found end of data)" 734 | ); 735 | 736 | assert_eq!( 737 | errstr(r#""#), 738 | "unexpected XML at 1:8 (expected valid type tag, found )" 739 | ); 740 | 741 | assert_eq!( 742 | errstr(r#"bla"#), 743 | "invalid value for type \'integer\' at 1:13: bla" 744 | ); 745 | } 746 | 747 | #[test] 748 | fn parses_empty_value_response() { 749 | assert_ok(read_response( 750 | r##" 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | "##, 760 | )); 761 | } 762 | 763 | #[test] 764 | fn parses_empty_value_in_struct_response() { 765 | assert_ok(read_response( 766 | r##" 767 | 768 | 769 | 770 | 771 | 772 | Test 773 | 774 | 775 | 776 | 777 | 778 | "##, 779 | )); 780 | } 781 | 782 | #[test] 783 | fn duplicate_struct_member() { 784 | // Duplicate struct members are overwritten with the last one 785 | assert_eq!( 786 | read_value( 787 | r#" 788 | 789 | 790 | 791 | A 792 | first 793 | 794 | 795 | A 796 | second 797 | 798 | 799 | 800 | "# 801 | ), 802 | Ok(Value::Struct( 803 | iter::once(("A".into(), "second".into())).collect() 804 | )) 805 | ); 806 | } 807 | } 808 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | extern crate reqwest; 3 | 4 | use error::{Error, RequestErrorKind}; 5 | use parser::parse_response; 6 | use transport::Transport; 7 | use utils::escape_xml; 8 | use Value; 9 | 10 | use std::collections::BTreeMap; 11 | use std::io::{self, Write}; 12 | 13 | /// A request to call a procedure. 14 | #[derive(Clone, Debug)] 15 | pub struct Request<'a> { 16 | name: &'a str, 17 | args: Vec, 18 | } 19 | 20 | impl<'a> Request<'a> { 21 | /// Creates a new request to call a function named `name`. 22 | /// 23 | /// By default, no arguments are passed. Use the `arg` method to append arguments. 24 | pub fn new(name: &'a str) -> Self { 25 | Request { 26 | name, 27 | args: Vec::new(), 28 | } 29 | } 30 | 31 | /// Creates a "multicall" request that will perform multiple requests at once. 32 | /// 33 | /// This requires that the server supports the [`system.multicall`] method. 34 | /// 35 | /// [`system.multicall`]: https://mirrors.talideon.com/articles/multicall.html 36 | #[allow(deprecated)] 37 | pub fn new_multicall<'r, I>(requests: I) -> Self 38 | where 39 | 'a: 'r, 40 | I: IntoIterator>, 41 | { 42 | Request { 43 | name: "system.multicall", 44 | args: vec![Value::Array( 45 | requests 46 | .into_iter() 47 | .map(|req| { 48 | let mut multicall_struct: BTreeMap = BTreeMap::new(); 49 | 50 | multicall_struct.insert("methodName".into(), req.name.into()); 51 | multicall_struct.insert("params".into(), Value::Array(req.args.clone())); 52 | 53 | Value::Struct(multicall_struct) 54 | }) 55 | .collect(), 56 | )], 57 | } 58 | } 59 | 60 | /// Appends an argument to be passed to the current list of arguments. 61 | pub fn arg>(mut self, value: T) -> Self { 62 | self.args.push(value.into()); 63 | self 64 | } 65 | 66 | /// Performs the request using a [`Transport`]. 67 | /// 68 | /// If you want to send the request using an HTTP POST request, you can also use [`call_url`], 69 | /// which creates a suitable [`Transport`] internally. 70 | /// 71 | /// # Errors 72 | /// 73 | /// Any errors that occur while sending the request using the [`Transport`] will be returned to 74 | /// the caller. Additionally, if the response is malformed (invalid XML), or indicates that the 75 | /// method call failed, an error will also be returned. 76 | /// 77 | /// [`call_url`]: #method.call_url 78 | /// [`Transport`]: trait.Transport.html 79 | pub fn call(&self, transport: T) -> Result { 80 | let mut reader = transport 81 | .transmit(self) 82 | .map_err(RequestErrorKind::TransportError)?; 83 | 84 | let response = parse_response(&mut reader).map_err(RequestErrorKind::ParseError)?; 85 | 86 | let value = response.map_err(RequestErrorKind::Fault)?; 87 | Ok(value) 88 | } 89 | 90 | /// Performs the request on a URL. 91 | /// 92 | /// You can pass a `&str` or an already parsed reqwest URL. 93 | /// 94 | /// This is a convenience method that will internally create a new `reqwest::Client` and send an 95 | /// HTTP POST request to the given URL. If you only use this method to perform requests, you 96 | /// don't need to depend on `reqwest` yourself. 97 | /// 98 | /// This method is only available when the `http` feature is enabled (this is the default). 99 | /// 100 | /// # Errors 101 | /// 102 | /// Since this is just a convenience wrapper around [`Request::call`], the same error conditions 103 | /// apply. 104 | /// 105 | /// Any reqwest errors will be propagated to the caller. 106 | /// 107 | /// [`Request::call`]: #method.call 108 | /// [`Transport`]: trait.Transport.html 109 | #[cfg(feature = "http")] 110 | pub fn call_url(&self, url: U) -> Result { 111 | // While we could implement `Transport` for `T: IntoUrl`, such an impl might not be 112 | // completely obvious (as it applies to `&str`), so I've added this method instead. 113 | // Might want to reconsider if someone has an objection. 114 | self.call(reqwest::blocking::Client::new().post(url)) 115 | } 116 | 117 | /// Formats this `Request` as a UTF-8 encoded XML document. 118 | /// 119 | /// # Errors 120 | /// 121 | /// Any errors reported by the writer will be propagated to the caller. If the writer never 122 | /// returns an error, neither will this method. 123 | pub fn write_as_xml(&self, fmt: &mut W) -> io::Result<()> { 124 | writeln!(fmt, r#""#)?; 125 | writeln!(fmt, r#""#)?; 126 | writeln!( 127 | fmt, 128 | r#"{}"#, 129 | escape_xml(&self.name) 130 | )?; 131 | writeln!(fmt, r#""#)?; 132 | for value in &self.args { 133 | writeln!(fmt, r#""#)?; 134 | value.write_as_xml(fmt)?; 135 | writeln!(fmt, r#""#)?; 136 | } 137 | writeln!(fmt, r#""#)?; 138 | write!(fmt, r#""#)?; 139 | Ok(()) 140 | } 141 | 142 | /// Serialize this `Request` into an XML-RPC struct that can be passed to 143 | /// the [`system.multicall`](https://mirrors.talideon.com/articles/multicall.html) 144 | /// XML-RPC method, specifically a struct with two fields: 145 | /// 146 | /// * `methodName`: the request name 147 | /// * `params`: the request arguments 148 | #[deprecated(since = "0.11.2", note = "use `Request::multicall` instead")] 149 | pub fn into_multicall_struct(self) -> Value { 150 | let mut multicall_struct: BTreeMap = BTreeMap::new(); 151 | 152 | multicall_struct.insert("methodName".into(), self.name.into()); 153 | multicall_struct.insert("params".into(), Value::Array(self.args)); 154 | 155 | Value::Struct(multicall_struct) 156 | } 157 | } 158 | 159 | #[cfg(test)] 160 | mod tests { 161 | use super::*; 162 | use std::str; 163 | 164 | #[test] 165 | fn escapes_method_names() { 166 | let mut output: Vec = Vec::new(); 167 | let req = Request::new("x<&x"); 168 | 169 | req.write_as_xml(&mut output).unwrap(); 170 | assert!(str::from_utf8(&output) 171 | .unwrap() 172 | .contains("x<&x")); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/transport.rs: -------------------------------------------------------------------------------- 1 | use Request; 2 | 3 | use std::error::Error; 4 | use std::io::Read; 5 | 6 | /// Request and response transport abstraction. 7 | /// 8 | /// The `Transport` trait provides a way to send a `Request` to a server and to receive the 9 | /// corresponding response. A `Transport` implementor is passed to [`Request::call`] in order to use 10 | /// it to perform that request. 11 | /// 12 | /// The most commonly used transport is simple HTTP: If the `http` feature is enabled (it is by 13 | /// default), the reqwest `RequestBuilder` will implement this trait and send the XML-RPC 14 | /// [`Request`] via HTTP. 15 | /// 16 | /// You can implement this trait for your own types if you want to customize how requests are sent. 17 | /// You can modify HTTP headers or wrap requests in a completely different protocol. 18 | /// 19 | /// [`Request::call`]: struct.Request.html#method.call 20 | /// [`Request`]: struct.Request.html 21 | pub trait Transport { 22 | // FIXME replace with `impl Trait` when stable 23 | /// The response stream returned by `transmit`. 24 | type Stream: Read; 25 | 26 | /// Transmits an XML-RPC request and returns the server's response. 27 | /// 28 | /// The response is returned as a `Self::Stream` - some type implementing the `Read` trait. The 29 | /// library will read all of the data and parse it as a response. It must be UTF-8 encoded XML, 30 | /// otherwise the call will fail. 31 | /// 32 | /// # Errors 33 | /// 34 | /// If a transport error occurs, it should be returned as a boxed error - the library will then 35 | /// return an appropriate [`Error`] to the caller. 36 | /// 37 | /// [`Error`]: struct.Error.html 38 | fn transmit(self, request: &Request<'_>) -> Result>; 39 | } 40 | 41 | // FIXME: Link to `Transport` and `RequestBuilder` using intra-rustdoc links. Relative links break 42 | // everything and abs. links don't work locally. 43 | /// Provides helpers for implementing custom `Transport`s using reqwest. 44 | /// 45 | /// This module will be disabled if the `http` feature is not enabled. 46 | /// 47 | /// The default [`Transport`] implementation for `RequestBuilder` looks roughly like 48 | /// this: 49 | /// 50 | /// ```notrust 51 | /// // serialize request into `body` (a `Vec`) 52 | /// 53 | /// build_headers(builder, body.len()); 54 | /// 55 | /// // send `body` using `builder` and get response 56 | /// 57 | /// check_response(&response)?; 58 | /// ``` 59 | /// 60 | /// From this, you can build your own custom transports. 61 | /// 62 | /// [`Transport`]: ../trait.Transport.html 63 | #[cfg(feature = "http")] 64 | pub mod http { 65 | extern crate mime; 66 | extern crate reqwest; 67 | 68 | use self::mime::Mime; 69 | use self::reqwest::blocking::RequestBuilder; 70 | use self::reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT}; 71 | use {Request, Transport}; 72 | 73 | use std::error::Error; 74 | use std::str::FromStr; 75 | 76 | /// Appends all HTTP headers required by the XML-RPC specification to the `RequestBuilder`. 77 | /// 78 | /// More specifically, the following headers are set: 79 | /// 80 | /// ```notrust 81 | /// User-Agent: Rust xmlrpc 82 | /// Content-Type: text/xml; charset=utf-8 83 | /// Content-Length: $body_len 84 | /// ``` 85 | pub fn build_headers(builder: RequestBuilder, body_len: u64) -> RequestBuilder { 86 | // Set all required request headers 87 | // NB: The `Host` header is also required, but reqwest adds it automatically, since 88 | // HTTP/1.1 requires it. 89 | builder 90 | .header(USER_AGENT, "Rust xmlrpc") 91 | .header(CONTENT_TYPE, "text/xml; charset=utf-8") 92 | .header(CONTENT_LENGTH, body_len) 93 | } 94 | 95 | /// Checks that a reqwest `Response` has a status code indicating success and verifies certain 96 | /// headers. 97 | pub fn check_response( 98 | response: &reqwest::blocking::Response, 99 | ) -> Result<(), Box> { 100 | // This is essentially an open-coded version of `Response::error_for_status` that does not 101 | // consume the response. 102 | if response.status().is_client_error() || response.status().is_server_error() { 103 | return Err(format!("server response indicates error: {}", response.status()).into()); 104 | } 105 | 106 | // Check response headers 107 | // "The Content-Type is text/xml. Content-Length must be present and correct." 108 | if let Some(content) = response 109 | .headers() 110 | .get(CONTENT_TYPE) 111 | .and_then(|value| value.to_str().ok()) 112 | .and_then(|value| Mime::from_str(value).ok()) 113 | { 114 | // (we ignore this if the header is missing completely) 115 | match (content.type_(), content.subtype()) { 116 | (mime::TEXT, mime::XML) => {} 117 | (ty, sub) => { 118 | return Err( 119 | format!("expected Content-Type 'text/xml', got '{}/{}'", ty, sub).into(), 120 | ) 121 | } 122 | } 123 | } 124 | 125 | // We ignore the Content-Length header because it doesn't matter for the parser and reqwest 126 | // will remove it when the response is gzip compressed. 127 | 128 | Ok(()) 129 | } 130 | 131 | /// Use a `RequestBuilder` as the transport. 132 | /// 133 | /// The request will be sent as specified in the XML-RPC specification: A default `User-Agent` 134 | /// will be set, along with the correct `Content-Type` and `Content-Length`. 135 | impl Transport for RequestBuilder { 136 | type Stream = reqwest::blocking::Response; 137 | 138 | fn transmit( 139 | self, 140 | request: &Request<'_>, 141 | ) -> Result> { 142 | // First, build the body XML 143 | let mut body = Vec::new(); 144 | // This unwrap never panics as we are using `Vec` as a `Write` implementor, 145 | // and not doing anything else that could return an `Err` in `write_as_xml()`. 146 | request.write_as_xml(&mut body).unwrap(); 147 | 148 | let response = build_headers(self, body.len() as u64).body(body).send()?; 149 | 150 | check_response(&response)?; 151 | 152 | Ok(response) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use iso8601::{Date, DateTime, Time}; 2 | use xml::escape::escape_str_pcdata; 3 | 4 | use std::borrow::Cow; 5 | use std::fmt::Write; 6 | 7 | /// Escape a string for use as XML characters. 8 | /// 9 | /// The resulting string is *not* suitable for use in XML attributes, but XML-RPC doesn't use those. 10 | pub fn escape_xml(s: &str) -> Cow<'_, str> { 11 | escape_str_pcdata(s) 12 | } 13 | 14 | /// Formats a `DateTime` for use in XML-RPC. 15 | /// 16 | /// Note that XML-RPC is extremely underspecified when it comes to datetime values. Apparently, 17 | /// some clients [don't even support timezone information][wp-bug] (we do). For maximum 18 | /// interoperability, this will omit fractional time and time zone if not specified. 19 | /// 20 | /// [wp-bug]: https://core.trac.wordpress.org/ticket/1633#comment:4 21 | pub fn format_datetime(date_time: &DateTime) -> String { 22 | let Time { 23 | hour, 24 | minute, 25 | second, 26 | millisecond, 27 | tz_offset_hours, 28 | tz_offset_minutes, 29 | } = date_time.time; 30 | 31 | match date_time.date { 32 | Date::YMD { year, month, day } => { 33 | // The base format is based directly on the example in the spec and should always work: 34 | let mut string = format!( 35 | "{:04}{:02}{:02}T{:02}:{:02}:{:02}", 36 | year, month, day, hour, minute, second 37 | ); 38 | // Only append milliseconds when they're >0 39 | if millisecond > 0 { 40 | write!(string, ".{:.3}", millisecond).unwrap(); 41 | } 42 | // Only append time zone info if the offset is specified and not 00:00 43 | if tz_offset_hours != 0 || tz_offset_minutes != 0 { 44 | write!( 45 | string, 46 | "{:+03}:{:02}", 47 | tz_offset_hours, 48 | tz_offset_minutes.abs() 49 | ) 50 | .unwrap(); 51 | } 52 | 53 | string 54 | } 55 | // Other format are just not supported at all: 56 | Date::Week { .. } | Date::Ordinal { .. } => unimplemented!(), 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | use iso8601; 64 | 65 | #[test] 66 | fn formats_datetimes() { 67 | let date_time = iso8601::datetime("2016-05-02T06:01:05-0830").unwrap(); 68 | 69 | let formatted = format_datetime(&date_time); 70 | assert_eq!(formatted, "20160502T06:01:05-08:30"); 71 | assert_eq!(iso8601::datetime(&formatted).unwrap(), date_time); 72 | 73 | // milliseconds / fraction 74 | let date_time = iso8601::datetime("20160502T06:01:05.400").unwrap(); 75 | let formatted = format_datetime(&date_time); 76 | assert_eq!(formatted, "20160502T06:01:05.400"); 77 | assert_eq!(iso8601::datetime(&formatted).unwrap(), date_time); 78 | 79 | // milliseconds / fraction + time zone 80 | let date_time = iso8601::datetime("20160502T06:01:05.400+01:02").unwrap(); 81 | let formatted = format_datetime(&date_time); 82 | assert_eq!(formatted, "20160502T06:01:05.400+01:02"); 83 | assert_eq!(iso8601::datetime(&formatted).unwrap(), date_time); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | //! Contains the different types of values understood by XML-RPC. 2 | 3 | use utils::{escape_xml, format_datetime}; 4 | 5 | use base64::encode; 6 | use iso8601::DateTime; 7 | 8 | use std::collections::BTreeMap; 9 | use std::io::{self, Write}; 10 | 11 | /// The possible XML-RPC values. 12 | /// 13 | /// Nested values can be accessed by using [`get`](#method.get) method and Rust's square-bracket 14 | /// indexing operator. 15 | /// 16 | /// A string index can be used to access a value in a `Struct`, and a `usize` index can be used to 17 | /// access an element of an `Array`. 18 | /// 19 | /// # Examples 20 | /// 21 | /// ``` 22 | /// # use xmlrpc::{Value}; 23 | /// let nothing = Value::Nil; 24 | /// 25 | /// let person = Value::Struct(vec![ 26 | /// ("name".to_string(), Value::from("John Doe")), 27 | /// ("age".to_string(), Value::from(37)), 28 | /// ("children".to_string(), Value::Array(vec![ 29 | /// Value::from("Mark"), 30 | /// Value::from("Jennyfer") 31 | /// ])), 32 | /// ].into_iter().collect()); 33 | /// 34 | /// // get 35 | /// assert_eq!(nothing.get("name"), None); 36 | /// assert_eq!(person.get("name"), Some(&Value::from("John Doe"))); 37 | /// assert_eq!(person.get("SSN"), None); 38 | /// 39 | /// // index 40 | /// assert_eq!(nothing["name"], Value::Nil); 41 | /// assert_eq!(person["name"], Value::from("John Doe")); 42 | /// assert_eq!(person["age"], Value::Int(37)); 43 | /// assert_eq!(person["SSN"], Value::Nil); 44 | /// assert_eq!(person["children"][0], Value::from("Mark")); 45 | /// assert_eq!(person["children"][0]["age"], Value::Nil); 46 | /// assert_eq!(person["children"][2], Value::Nil); 47 | /// 48 | /// // extract values 49 | /// assert_eq!(person["name"].as_str(), Some("John Doe")); 50 | /// assert_eq!(person["age"].as_i32(), Some(37)); 51 | /// assert_eq!(person["age"].as_bool(), None); 52 | /// assert_eq!(person["children"].as_array().unwrap().len(), 2); 53 | /// ``` 54 | #[derive(Clone, Debug, PartialEq)] 55 | pub enum Value { 56 | /// A 32-bit signed integer (`` or ``). 57 | Int(i32), 58 | /// A 64-bit signed integer (``). 59 | /// 60 | /// This is an XMLRPC extension and may not be supported by all clients / servers. 61 | Int64(i64), 62 | /// A boolean value (``, 0 == `false`, 1 == `true`). 63 | Bool(bool), 64 | /// A string (``). 65 | // FIXME zero-copy? `Cow<'static, ..>`? 66 | String(String), 67 | /// A double-precision IEEE 754 floating point number (``). 68 | Double(f64), 69 | /// An ISO 8601 formatted date/time value (``). 70 | /// 71 | /// Note that ISO 8601 is highly ambiguous and allows incomplete date-time specifications. For 72 | /// example, servers will frequently leave out timezone information, in which case the client 73 | /// must *know* which timezone is used by the server. For this reason, the contained `DateTime` 74 | /// struct only contains the raw fields specified by the server, without any real date/time 75 | /// functionality like what's offered by the `chrono` crate. 76 | /// 77 | /// To make matters worse, some clients [don't seem to support][wp-bug] time zone information in 78 | /// datetime values. To ensure compatiblity, the xmlrpc crate will try to format datetime values 79 | /// like the example given in the [specification] if the timezone offset is zero. 80 | /// 81 | /// Recommendation: Avoid `DateTime` if possible. A date and time can be specified more 82 | /// precisely by formatting it using RFC 3339 and putting it in a [`String`]. 83 | /// 84 | /// [wp-bug]: https://core.trac.wordpress.org/ticket/1633#comment:4 85 | /// [specification]: http://xmlrpc.scripting.com/spec.html 86 | /// [`String`]: #variant.String 87 | DateTime(DateTime), 88 | /// Base64-encoded binary data (``). 89 | Base64(Vec), 90 | 91 | /// A mapping of named values (``). 92 | Struct(BTreeMap), 93 | /// A list of arbitrary (heterogeneous) values (``). 94 | Array(Vec), 95 | 96 | /// The empty (Unit) value (``). 97 | /// 98 | /// This is an XMLRPC [extension][ext] and may not be supported by all clients / servers. 99 | /// 100 | /// [ext]: https://web.archive.org/web/20050911054235/http://ontosys.com/xml-rpc/extensions.php 101 | Nil, 102 | } 103 | 104 | impl Value { 105 | /// Formats this `Value` as an XML `` element. 106 | /// 107 | /// # Errors 108 | /// 109 | /// Any error reported by the writer will be propagated to the caller. 110 | pub fn write_as_xml(&self, fmt: &mut W) -> io::Result<()> { 111 | write!(fmt, "")?; 112 | 113 | match *self { 114 | Value::Int(i) => { 115 | write!(fmt, "{}", i)?; 116 | } 117 | Value::Int64(i) => { 118 | write!(fmt, "{}", i)?; 119 | } 120 | Value::Bool(b) => { 121 | write!(fmt, "{}", if b { "1" } else { "0" })?; 122 | } 123 | Value::String(ref s) => { 124 | write!(fmt, "{}", escape_xml(s))?; 125 | } 126 | Value::Double(d) => { 127 | write!(fmt, "{}", d)?; 128 | } 129 | Value::DateTime(date_time) => { 130 | write!( 131 | fmt, 132 | "{}", 133 | format_datetime(&date_time) 134 | )?; 135 | } 136 | Value::Base64(ref data) => { 137 | write!(fmt, "{}", encode(data))?; 138 | } 139 | Value::Struct(ref map) => { 140 | writeln!(fmt, "")?; 141 | for (ref name, ref value) in map { 142 | writeln!(fmt, "")?; 143 | writeln!(fmt, "{}", escape_xml(name))?; 144 | value.write_as_xml(fmt)?; 145 | writeln!(fmt, "")?; 146 | } 147 | write!(fmt, "")?; 148 | } 149 | Value::Array(ref array) => { 150 | write!(fmt, "")?; 151 | writeln!(fmt, "")?; 152 | for value in array { 153 | value.write_as_xml(fmt)?; 154 | } 155 | write!(fmt, "")?; 156 | write!(fmt, "")?; 157 | } 158 | Value::Nil => { 159 | write!(fmt, "")?; 160 | } 161 | } 162 | 163 | writeln!(fmt, "")?; 164 | Ok(()) 165 | } 166 | 167 | /// Returns an inner struct or array value indexed by `index`. 168 | /// 169 | /// Returns `None` if the member doesn't exist or `self` is neither a struct nor an array. 170 | /// 171 | /// You can also use Rust's square-bracket indexing syntax to perform this operation if you want 172 | /// a default value instead of an `Option`. Refer to the top-level [examples](#examples) for 173 | /// details. 174 | pub fn get(&self, index: I) -> Option<&Value> { 175 | index.get(self) 176 | } 177 | 178 | /// If the `Value` is a normal integer (`Value::Int`), returns associated value. Returns `None` 179 | /// otherwise. 180 | /// 181 | /// In particular, `None` is also returned if `self` is a `Value::Int64`. Use [`as_i64`] to 182 | /// handle this case. 183 | /// 184 | /// [`as_i64`]: #method.as_i64 185 | pub fn as_i32(&self) -> Option { 186 | match *self { 187 | Value::Int(i) => Some(i), 188 | _ => None, 189 | } 190 | } 191 | 192 | /// If the `Value` is an integer, returns associated value. Returns `None` otherwise. 193 | /// 194 | /// This works with both `Value::Int` and `Value::Int64`. 195 | pub fn as_i64(&self) -> Option { 196 | match *self { 197 | Value::Int(i) => Some(i64::from(i)), 198 | Value::Int64(i) => Some(i), 199 | _ => None, 200 | } 201 | } 202 | 203 | /// If the `Value` is a boolean, returns associated value. Returns `None` otherwise. 204 | pub fn as_bool(&self) -> Option { 205 | match *self { 206 | Value::Bool(b) => Some(b), 207 | _ => None, 208 | } 209 | } 210 | 211 | /// If the `Value` is a string, returns associated value. Returns `None` otherwise. 212 | pub fn as_str(&self) -> Option<&str> { 213 | match *self { 214 | Value::String(ref s) => Some(s), 215 | _ => None, 216 | } 217 | } 218 | 219 | /// If the `Value` is a floating point number, returns associated value. Returns `None` 220 | /// otherwise. 221 | pub fn as_f64(&self) -> Option { 222 | match *self { 223 | Value::Double(d) => Some(d), 224 | _ => None, 225 | } 226 | } 227 | 228 | /// If the `Value` is a date/time, returns associated value. Returns `None` otherwise. 229 | pub fn as_datetime(&self) -> Option { 230 | match *self { 231 | Value::DateTime(dt) => Some(dt), 232 | _ => None, 233 | } 234 | } 235 | 236 | /// If the `Value` is base64 binary data, returns associated value. Returns `None` otherwise. 237 | pub fn as_bytes(&self) -> Option<&[u8]> { 238 | match *self { 239 | Value::Base64(ref data) => Some(data), 240 | _ => None, 241 | } 242 | } 243 | 244 | /// If the `Value` is a struct, returns associated map. Returns `None` otherwise. 245 | pub fn as_struct(&self) -> Option<&BTreeMap> { 246 | match *self { 247 | Value::Struct(ref map) => Some(map), 248 | _ => None, 249 | } 250 | } 251 | 252 | /// If the `Value` is an array, returns associated slice. Returns `None` otherwise. 253 | pub fn as_array(&self) -> Option<&[Value]> { 254 | match *self { 255 | Value::Array(ref array) => Some(array), 256 | _ => None, 257 | } 258 | } 259 | } 260 | 261 | impl From for Value { 262 | fn from(other: i32) -> Self { 263 | Value::Int(other) 264 | } 265 | } 266 | 267 | impl From for Value { 268 | fn from(other: i64) -> Self { 269 | Value::Int64(other) 270 | } 271 | } 272 | 273 | impl From for Value { 274 | fn from(other: bool) -> Self { 275 | Value::Bool(other) 276 | } 277 | } 278 | 279 | impl From for Value { 280 | fn from(other: String) -> Self { 281 | Value::String(other) 282 | } 283 | } 284 | 285 | impl<'a> From<&'a str> for Value { 286 | fn from(other: &'a str) -> Self { 287 | Value::String(other.to_string()) 288 | } 289 | } 290 | 291 | impl From for Value { 292 | fn from(other: f64) -> Self { 293 | Value::Double(other) 294 | } 295 | } 296 | 297 | impl From for Value { 298 | fn from(other: DateTime) -> Self { 299 | Value::DateTime(other) 300 | } 301 | } 302 | 303 | impl From> for Value { 304 | fn from(other: Vec) -> Self { 305 | Value::Base64(other) 306 | } 307 | } 308 | 309 | impl From> for Value { 310 | fn from(other: Option) -> Self { 311 | match other { 312 | Some(x) => Value::Int64(x), 313 | None => Value::Nil, 314 | } 315 | } 316 | } 317 | 318 | impl From> for Value { 319 | fn from(other: Option) -> Self { 320 | match other { 321 | Some(x) => Value::Int(x), 322 | None => Value::Nil, 323 | } 324 | } 325 | } 326 | 327 | impl From> for Value { 328 | fn from(other: Option) -> Self { 329 | match other { 330 | Some(x) => Value::Bool(x), 331 | None => Value::Nil, 332 | } 333 | } 334 | } 335 | 336 | impl From> for Value { 337 | fn from(other: Option) -> Self { 338 | match other { 339 | Some(x) => Value::String(x), 340 | None => Value::Nil, 341 | } 342 | } 343 | } 344 | 345 | impl<'a> From> for Value { 346 | fn from(other: Option<&'a str>) -> Self { 347 | match other { 348 | Some(x) => Value::String(x.to_string()), 349 | None => Value::Nil, 350 | } 351 | } 352 | } 353 | 354 | impl From> for Value { 355 | fn from(other: Option) -> Self { 356 | match other { 357 | Some(x) => Value::Double(x), 358 | None => Value::Nil, 359 | } 360 | } 361 | } 362 | 363 | impl From> for Value { 364 | fn from(other: Option) -> Self { 365 | match other { 366 | Some(x) => Value::DateTime(x), 367 | None => Value::Nil, 368 | } 369 | } 370 | } 371 | impl From>> for Value { 372 | fn from(other: Option>) -> Self { 373 | match other { 374 | Some(x) => Value::Base64(x), 375 | None => Value::Nil, 376 | } 377 | } 378 | } 379 | mod sealed { 380 | /// A trait that is only nameable (and thus implementable) inside this crate. 381 | pub trait Sealed {} 382 | impl Sealed for str {} 383 | impl Sealed for String {} 384 | impl Sealed for usize {} 385 | impl<'a, I> Sealed for &'a I where I: Sealed + ?Sized {} 386 | } 387 | 388 | /// A type that can be used to index into a [`Value`]. 389 | /// 390 | /// You can use Rust's regular indexing syntax to access components of [`Value`]s. Refer to the 391 | /// examples on [`Value`] for details. 392 | /// 393 | /// This trait can not be implemented by custom types. 394 | /// 395 | /// [`Value`]: enum.Value.html 396 | pub trait Index: sealed::Sealed { 397 | /// Gets an inner value of a given value represented by self. 398 | #[doc(hidden)] 399 | fn get<'v>(&self, value: &'v Value) -> Option<&'v Value>; 400 | } 401 | 402 | impl Index for str { 403 | fn get<'v>(&self, value: &'v Value) -> Option<&'v Value> { 404 | if let Value::Struct(ref map) = *value { 405 | map.get(self) 406 | } else { 407 | None 408 | } 409 | } 410 | } 411 | 412 | impl Index for String { 413 | fn get<'v>(&self, value: &'v Value) -> Option<&'v Value> { 414 | if let Value::Struct(ref map) = *value { 415 | map.get(self) 416 | } else { 417 | None 418 | } 419 | } 420 | } 421 | 422 | impl Index for usize { 423 | fn get<'v>(&self, value: &'v Value) -> Option<&'v Value> { 424 | if let Value::Array(ref array) = *value { 425 | array.get(*self) 426 | } else { 427 | None 428 | } 429 | } 430 | } 431 | 432 | impl<'a, I> Index for &'a I 433 | where 434 | I: Index + ?Sized, 435 | { 436 | fn get<'v>(&self, value: &'v Value) -> Option<&'v Value> { 437 | (*self).get(value) 438 | } 439 | } 440 | 441 | impl ::std::ops::Index for Value 442 | where 443 | I: Index, 444 | { 445 | type Output = Value; 446 | fn index(&self, index: I) -> &Self::Output { 447 | index.get(self).unwrap_or(&Value::Nil) 448 | } 449 | } 450 | 451 | #[cfg(test)] 452 | mod tests { 453 | use super::*; 454 | use std::collections::BTreeMap; 455 | use std::str; 456 | 457 | #[test] 458 | fn escapes_strings() { 459 | let mut output: Vec = Vec::new(); 460 | 461 | Value::from(" string") 462 | .write_as_xml(&mut output) 463 | .unwrap(); 464 | assert_eq!( 465 | str::from_utf8(&output).unwrap(), 466 | "<xml>&nbsp;string\n" 467 | ); 468 | } 469 | 470 | #[test] 471 | fn escapes_struct_member_names() { 472 | let mut output: Vec = Vec::new(); 473 | let mut map: BTreeMap = BTreeMap::new(); 474 | map.insert("x&\n\nx&<x\n1\n\n\n"); 478 | } 479 | 480 | #[test] 481 | fn access_nested_values() { 482 | let mut map: BTreeMap = BTreeMap::new(); 483 | map.insert("name".to_string(), Value::from("John Doe")); 484 | map.insert("age".to_string(), Value::from(37)); 485 | map.insert( 486 | "children".to_string(), 487 | Value::Array(vec![Value::from("Mark"), Value::from("Jennyfer")]), 488 | ); 489 | let value = Value::Struct(map); 490 | 491 | assert_eq!(value.get("name"), Some(&Value::from("John Doe"))); 492 | assert_eq!(value.get("age"), Some(&Value::from(37))); 493 | assert_eq!(value.get("birthdate"), None); 494 | assert_eq!(Value::Nil.get("age"), None); 495 | assert_eq!(value["name"], Value::from("John Doe")); 496 | assert_eq!(value["age"], Value::from(37)); 497 | assert_eq!(value["birthdate"], Value::Nil); 498 | assert_eq!(Value::Nil["age"], Value::Nil); 499 | assert_eq!(value["children"][0], Value::from("Mark")); 500 | assert_eq!(value["children"][1], Value::from("Jennyfer")); 501 | assert_eq!(value["children"][2], Value::Nil); 502 | 503 | assert_eq!(value["age"].as_i32(), Some(37)); 504 | assert_eq!(value["children"][0].as_str(), Some("Mark")); 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /tests/python.rs: -------------------------------------------------------------------------------- 1 | //! Tests communication with a python3 XML-RPC server. 2 | 3 | extern crate xmlrpc; 4 | 5 | use xmlrpc::{Fault, Request, Value}; 6 | 7 | use std::net::TcpStream; 8 | use std::process::{Child, Command}; 9 | use std::thread::sleep; 10 | use std::time::{Duration, Instant}; 11 | 12 | const PORT: u16 = 8000; 13 | const URL: &'static str = "http://127.0.0.1:8000"; 14 | 15 | /// Kills a child process when dropped. 16 | struct Reap(Child); 17 | 18 | impl Drop for Reap { 19 | fn drop(&mut self) { 20 | // an error seems to mean that the process has already died, which we don't expect here 21 | self.0.kill().expect("process already died"); 22 | } 23 | } 24 | 25 | fn setup() -> Result { 26 | let start = Instant::now(); 27 | let mut child = match Command::new("python3") 28 | .arg("-m") 29 | .arg("xmlrpc.server") 30 | .spawn() 31 | { 32 | Ok(child) => child, 33 | Err(e) => { 34 | eprintln!( 35 | "could not start python XML-RPC server, ignoring python test ({})", 36 | e 37 | ); 38 | return Err(()); 39 | } 40 | }; 41 | 42 | // wait until someone listens on the port or the child dies 43 | let mut iteration = 0; 44 | loop { 45 | match child.try_wait().unwrap() { 46 | None => {} // still running 47 | Some(status) => panic!("python process unexpectedly died: {}", status), 48 | } 49 | 50 | // try to connect to the server 51 | match TcpStream::connect(("127.0.0.1", PORT)) { 52 | Ok(_) => { 53 | // server should work now 54 | println!( 55 | "connected to server after {:?} (iteration {})", 56 | Instant::now() - start, 57 | iteration 58 | ); 59 | return Ok(Reap(child)); 60 | } 61 | Err(_) => {} // not yet ready 62 | } 63 | 64 | sleep(Duration::from_millis(50)); 65 | 66 | iteration += 1; 67 | } 68 | } 69 | 70 | fn run_tests() { 71 | let pow = Request::new("pow").arg(2).arg(8).call_url(URL).unwrap(); 72 | assert_eq!(pow.as_i64(), Some(2i64.pow(8))); 73 | 74 | // call with wrong operands should return a fault 75 | let err = Request::new("pow") 76 | .arg(2) 77 | .arg(2) 78 | .arg("BLA") 79 | .call_url(URL) 80 | .unwrap_err(); 81 | err.fault().expect("returned error was not a fault"); 82 | 83 | // perform a multicall 84 | let result = Request::new_multicall(&[ 85 | Request::new("pow").arg(2).arg(4), 86 | Request::new("add").arg(2).arg(4), 87 | Request::new("doesn't exist"), 88 | ]) 89 | .call_url(URL) 90 | .unwrap(); 91 | // `result` now contains an array of results. on success, a 1-element array containing the 92 | // result is placed in the `result` array. on fault, the corresponding fault struct is used. 93 | let results = result.as_array().unwrap(); 94 | assert_eq!(results[0], Value::Array(vec![Value::Int(16)])); 95 | assert_eq!(results[1], Value::Array(vec![Value::Int(6)])); 96 | Fault::from_value(&results[2]).expect("expected fault as third result"); 97 | } 98 | 99 | fn main() { 100 | let mut reaper = match setup() { 101 | Ok(reap) => reap, 102 | Err(()) => return, 103 | }; 104 | 105 | match reaper.0.try_wait().unwrap() { 106 | None => {} // still running 107 | Some(status) => { 108 | panic!("python process unexpectedly exited: {}", status); 109 | } 110 | } 111 | 112 | run_tests(); 113 | 114 | match reaper.0.try_wait().unwrap() { 115 | None => {} // still running 116 | Some(status) => { 117 | panic!("python process unexpectedly exited: {}", status); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/version-numbers.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate version_sync; 3 | 4 | #[test] 5 | fn test_readme_deps() { 6 | assert_markdown_deps_updated!("README.md"); 7 | } 8 | 9 | #[test] 10 | fn test_html_root_url() { 11 | assert_html_root_url_updated!("src/lib.rs"); 12 | } 13 | --------------------------------------------------------------------------------