bolt_client/
lib.rs

1#![warn(rust_2018_idioms)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4//! This crate contains a runtime-agnostic asynchronous client for graph database servers that
5//! support the [Bolt](https://neo4j.com/docs/bolt/current) protocol.
6//!
7//! The central feature of this library is the [`Client`] struct, which allows sending Bolt
8//! messages to a compatible server. Clients can operate over any type that implements
9//! [`AsyncRead`](futures_util::AsyncRead) and [`AsyncWrite`](futures_util::AsyncWrite).
10//!
11//! If you want to connect to a Bolt-compatible server from your application, you probably want to
12//! use a connection pool - see [bb8-bolt](https://crates.io/crates/bb8-bolt),
13//! [deadpool-bolt](https://crates.io/crates/deadpool-bolt), or
14//! [mobc-bolt](https://crates.io/crates/mobc-bolt).
15//!
16//! If you'd rather manage your own connections, an asynchronous TCP/TLS [`Stream`] wrapper is also
17//! available, if you're using the [tokio](https://tokio.rs/) runtime.
18//!
19//! # Features
20//! - `tokio-stream` - enables the [`Stream`] type
21//!
22//! # Example
23//! The below example demonstrates how to communicate with a Neo4j server using Bolt protocol
24//! version 4.
25//! ```
26//! use std::{collections::HashMap, env};
27//!
28//! use tokio::io::BufStream;
29//! use tokio_util::compat::*;
30//!
31//! use bolt_client::*;
32//! use bolt_proto::{message::*, value::*, version::*, Message, Value};
33//!
34//! #[tokio::main]
35//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
36//!     // Let's say you have a type that implements AsyncRead + AsyncWrite. Here's one
37//!     // provided by the `tokio-stream` feature of this library. In this example, all
38//!     // connection/authentication details are stored in environment variables.
39//!     let stream = Stream::connect(env::var("BOLT_TEST_ADDR")?,
40//!                                  env::var("BOLT_TEST_DOMAIN").ok()).await?;
41//!     // Be sure to buffer your IO :)
42//!     let stream = BufStream::new(stream).compat();
43//!
44//!     // Create a new connection to the server and perform a handshake to establish a
45//!     // protocol version. This example demonstrates usage of the v4.3 or v4.2 protocol.
46//!     let mut result = Client::new(stream, &[V4_3, V4_2, 0, 0]).await;
47//! #   skip_if_handshake_failed!(result, Ok(()));
48//!     let mut client = result.unwrap();
49//!     
50//!     // Send a HELLO message with authentication details to the server to initialize
51//!     // the session.
52//!     let response: Message = client.hello(
53//!         Metadata::from_iter(vec![
54//!             ("user_agent", "my-client-name/1.0"),
55//!             ("scheme", "basic"),
56//!             ("principal", &env::var("BOLT_TEST_USERNAME")?),
57//!             ("credentials", &env::var("BOLT_TEST_PASSWORD")?),
58//!         ])).await?;
59//! #   Success::try_from(response.clone()).unwrap();
60//!     assert!(Success::try_from(response).is_ok());
61//!
62//!     // Submit a query for execution on the server
63//!     let response = client.run("RETURN 1 as num;", None, None).await?;
64//!
65//!     // Successful responses will include a SUCCESS message with related metadata
66//!     // Consuming these messages is optional and will be skipped for the rest of the example
67//! #   Success::try_from(response.clone()).unwrap();
68//!     assert!(Success::try_from(response).is_ok());
69//!
70//!     // Use PULL to retrieve results of the query, organized into RECORD messages
71//!     // We get a (Vec<Record>, Message) returned from a PULL
72//!     let pull_meta = Metadata::from_iter(vec![("n", 1)]);
73//!     let (records, response) = client.pull(Some(pull_meta.clone())).await?;
74//! #   Success::try_from(response).unwrap();
75//!
76//!     assert_eq!(records[0].fields(), &[Value::from(1)]);
77//! #    
78//! #   client.run("MATCH (n) DETACH DELETE n;", None, None).await?;
79//! #   client.pull(Some(pull_meta.clone())).await?;
80//!
81//!     // Submit a more complex query with parameters
82//!     let params = Params::from_iter(vec![("name", "Rust")]);
83//!     client.run(
84//!         "CREATE (:Client)-[:WRITTEN_IN]->(:Language {name: $name});",
85//!         Some(params), None).await?;
86//!     client.pull(Some(pull_meta.clone())).await?;
87//!
88//!     // Grab a node from the database and convert it to a native type
89//!     client.run("MATCH (rust:Language) RETURN rust;", None, None).await?;
90//!     let (records, response) = client.pull(Some(pull_meta.clone())).await?;
91//! #   Success::try_from(response).unwrap();
92//!     let node = Node::try_from(records[0].fields()[0].clone())?;
93//!
94//!     // Access properties from returned values
95//!     assert_eq!(node.labels(), &[String::from("Language")]);
96//!     assert_eq!(node.properties(),
97//!                &HashMap::from_iter(vec![(String::from("name"), Value::from("Rust"))]));
98//!
99//!     // End the connection with the server
100//!     client.goodbye().await?;
101//!
102//!     Ok(())
103//! }
104//! ```
105//!
106//! For version 3 of the protocol, the metadata we pass to [`Client::pull`] is not required, since
107//! all records are consumed.
108//! ```
109//! # use std::collections::HashMap;
110//! # use std::env;
111//! #
112//! # use tokio::io::BufStream;
113//! # use tokio_util::compat::*;
114//! #
115//! # use bolt_client::*;
116//! # use bolt_proto::{message::*, value::*, version::*, Message, Value};
117//! #
118//! # #[tokio::main]
119//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
120//! #     let stream = Stream::connect(env::var("BOLT_TEST_ADDR")?,
121//! #                                  env::var("BOLT_TEST_DOMAIN").ok()).await?;
122//! #     let stream = BufStream::new(stream).compat();
123//! // Now we only want Bolt v3
124//! let mut result = Client::new(stream, &[V3_0, 0, 0, 0]).await;
125//! #     skip_if_handshake_failed!(result, Ok(()));
126//! #     let mut client = result.unwrap();
127//! #
128//! #     let response: Message = client.hello(
129//! #         Metadata::from_iter(vec![
130//! #             ("user_agent", "my-client-name/1.0"),
131//! #             ("scheme", "basic"),
132//! #             ("principal", &env::var("BOLT_TEST_USERNAME")?),
133//! #             ("credentials", &env::var("BOLT_TEST_PASSWORD")?),
134//! #         ])).await?;
135//! #     Success::try_from(response).unwrap();
136//! #
137//! #     let response = client.run("RETURN 1 as num;", None, None).await?;
138//! #     Success::try_from(response).unwrap();
139//!
140//! // Use `None` for the PULL metadata
141//! let (records, response) = client.pull(None).await?;
142//! #     Success::try_from(response).unwrap();
143//! #
144//! #     assert_eq!(records[0].fields(), &[Value::from(1 as i8)]);
145//! #     client.run("MATCH (n {test: 'doctest-v3'}) DETACH DELETE n;", None, None).await?;
146//! #     client.pull(None).await?;
147//! #
148//! #     let params = Params::from_iter(vec![("name", "C")]);
149//! #     client.run(
150//! #         "CREATE (:Seabolt {test: 'doctest-v3'})-[:WRITTEN_IN]->(:C {name: $name, test: 'doctest-v3'});",
151//! #         Some(params), None).await?;
152//! #     client.pull(None).await?;
153//! #
154//! #     client.run("MATCH (c:C {test: 'doctest-v3'}) RETURN c;", None, None).await?;
155//! #     let (records, response) = client.pull(None).await?;
156//! #     Success::try_from(response).unwrap();
157//! #     let node = Node::try_from(records[0].fields()[0].clone())?;
158//! #     assert_eq!(node.labels(), &[String::from("C")]);
159//! #     assert_eq!(node.properties(),
160//! #                &HashMap::from_iter(vec![(String::from("name"), Value::from("C")),
161//! #                                         (String::from("test"), Value::from("doctest-v3"))]));
162//! #     client.goodbye().await?;
163//! #     Ok(())
164//! # }
165//! ```
166//!
167//! For versions 1 and 2 of the protocol, there are a couple more differences:
168//! ```
169//! # use std::collections::HashMap;
170//! # use std::env;
171//! #
172//! # use tokio::io::BufStream;
173//! # use tokio_util::compat::*;
174//! #
175//! # use bolt_client::*;
176//! # use bolt_proto::{message::*, value::*, version::*, Message, Value};
177//! #
178//! # #[tokio::main]
179//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
180//! #     let stream = Stream::connect(env::var("BOLT_TEST_ADDR")?,
181//! #                                  env::var("BOLT_TEST_DOMAIN").ok()).await?;
182//! #     let stream = BufStream::new(stream).compat();
183//! // For the handshake, we can support versions 1 and 2 only, preferring version 2.
184//! let mut result = Client::new(stream, &[V2_0, V1_0, 0, 0]).await;
185//! #     skip_if_handshake_failed!(result, Ok(()));
186//! #     let mut client = result.unwrap();
187//! #
188//! #     let response: Message = client.hello(
189//! #         Metadata::from_iter(vec![
190//! #             ("user_agent", "my-client-name/1.0"),
191//! #             ("scheme", "basic"),
192//! #             ("principal", &env::var("BOLT_TEST_USERNAME")?),
193//! #             ("credentials", &env::var("BOLT_TEST_PASSWORD")?),
194//! #         ])).await?;
195//! #     Success::try_from(response).unwrap();
196//! #
197//! #     let response = client.run("RETURN 1 as num;", None, None).await?;
198//! #     Success::try_from(response).unwrap();
199//!
200//! // No need to pass metadata here either
201//! let (records, response) = client.pull(None).await?;
202//! #     Success::try_from(response).unwrap();
203//! #     assert_eq!(records[0].fields(), &[Value::from(1 as i8)]);
204//! #    
205//! #     client.run("MATCH (n {test: 'doctest-v2-v1'}) DETACH DELETE n;", None, None).await?;
206//! #     client.pull(None).await?;
207//! #    
208//! #     client.run("CREATE (:Client {test: 'doctest-v2-v1'})-[:WRITTEN_IN]->(:Language {name: $name, test: 'doctest-v2-v1'});",
209//! #                Some(Params::from_iter(vec![("name".to_string(), Value::from("Rust"))])), None).await?;
210//! #     client.pull(None).await?;
211//! #     client.run("MATCH (rust:Language {test: 'doctest-v2-v1'}) RETURN rust;", None, None).await?;
212//! #     let (records, response) = client.pull(None).await?;
213//! #     Success::try_from(response).unwrap();
214//! #    
215//! #     let node = Node::try_from(records[0].fields()[0].clone())?;
216//! #     assert_eq!(node.labels(), &["Language".to_string()]);
217//! #     assert_eq!(node.properties(),
218//! #                &HashMap::from_iter(vec![(String::from("name"), Value::from("Rust")),
219//! #                                         (String::from("test"), Value::from("doctest-v2-v1"))]));
220//!
221//! // There is no call to `goodbye`
222//! #     Ok(())
223//! # }
224//! ```
225//! See the documentation of the [`Client`] struct for information on transaction management, error
226//! handling, and more.
227#[doc(inline)]
228pub use self::client::Client;
229
230mod client;
231mod define_value_map;
232pub mod error;
233
234pub use bolt_proto;
235
236#[cfg(feature = "tokio-stream")]
237mod stream;
238
239#[cfg(feature = "tokio-stream")]
240pub use stream::Stream;
241
242// TODO: Convert Client methods to return a builder-type object so we don't need these anymore
243define_value_map!(Metadata);
244define_value_map!(Params);
245define_value_map!(RoutingContext);
246
247#[doc(hidden)]
248#[macro_export]
249macro_rules! skip_if_handshake_failed {
250    ($var:expr) => {
251        if let Err(err) = $var {
252            println!("Skipping test: {}", err);
253            return;
254        }
255    };
256    ($var:expr, $ret:expr) => {
257        if let Err(err) = $var {
258            println!("Skipping test: {}", err);
259            return $ret;
260        }
261    };
262}