diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7e794163..52c9d939 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neo4rs" -version = "0.6.0" +version = "0.6.1" authors = ["Neo4j Labs ", "John Pradeep Vincent "] edition = "2021" description = "Neo4j driver in rust" diff --git a/lib/src/connection.rs b/lib/src/connection.rs index dd9cc5b7..a2612ad8 100644 --- a/lib/src/connection.rs +++ b/lib/src/connection.rs @@ -42,8 +42,22 @@ impl Connection { }; match url.scheme() { - "bolt" | "neo4j" | "" => Self::new_unencrypted(stream, user, password).await, - "bolt+s" | "neo4j+s" => Self::new_tls(stream, host, user, password).await, + "bolt" | "" => Self::new_unencrypted(stream, user, password).await, + "bolt+s" => Self::new_tls(stream, host, user, password).await, + "neo4j" => { + log::warn!(concat!( + "This driver does not yet implement client-side routing. ", + "It is possible that operations against a cluster (such as Aura) will fail." + )); + Self::new_unencrypted(stream, user, password).await + } + "neo4j+s" => { + log::warn!(concat!( + "This driver does not yet implement client-side routing. ", + "It is possible that operations against a cluster (such as Aura) will fail." + )); + Self::new_tls(stream, host, user, password).await + } otherwise => Err(Error::UnsupportedScheme(otherwise.to_owned())), } } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5eceffaf..4be8b46e 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -764,11 +764,14 @@ mod txn; mod types; mod version; -pub use crate::config::{Config, ConfigBuilder}; -pub use crate::errors::*; -pub use crate::graph::{query, Graph}; -pub use crate::query::Query; -pub use crate::row::{Node, Path, Point2D, Point3D, Relation, Row, UnboundedRelation}; -pub use crate::stream::RowStream; -pub use crate::txn::Txn; -pub use crate::version::Version; +pub use config::{Config, ConfigBuilder}; +pub use errors::*; +pub use graph::{query, Graph}; +pub use query::Query; +pub use row::{Node, Path, Point2D, Point3D, Relation, Row, UnboundedRelation}; +pub use stream::RowStream; +pub use txn::Txn; +pub use version::Version; + +pub use types::*; +pub use neo4rs_macros::FromBoltType; diff --git a/lib/src/row.rs b/lib/src/row.rs index 1d1c7c9a..1c2ea870 100644 --- a/lib/src/row.rs +++ b/lib/src/row.rs @@ -137,6 +137,13 @@ impl Node { self.inner.labels.iter().map(|l| l.to_string()).collect() } + pub fn props(&self) -> Result + where + T: std::convert::TryFrom, + { + self.inner.properties.clone().try_into() + } + /// Get the attributes of the node pub fn get>(&self, key: &str) -> Option { self.inner.get(key) diff --git a/lib/tests/into_custom_struct.rs b/lib/tests/into_custom_struct.rs new file mode 100644 index 00000000..8698d567 --- /dev/null +++ b/lib/tests/into_custom_struct.rs @@ -0,0 +1,94 @@ +use neo4rs::*; + +mod container; + +#[derive(Debug, FromBoltType, PartialEq)] +struct Post { + title: String, + body: Option, +} + +#[derive(Debug, FromBoltType, PartialEq)] +struct Unit; + +#[tokio::test] +async fn from_row_for_custom_struct() { + let config = ConfigBuilder::default() + .db("neo4j") + .fetch_size(500) + .max_connections(10); + let neo4j = container::Neo4jContainer::from_config(config).await; + let graph = neo4j.graph(); + + let mut result = graph + .execute( + query("RETURN { title: $title, body: $body } as n") + .params([("title", "Hello"), ("body", "World!")]), + ) + .await + .unwrap(); + let row = result.next().await.unwrap().unwrap(); + let value: Post = row.get("n").unwrap(); + assert_eq!( + Post { + title: "Hello".into(), + body: Some("World!".into()), + }, + value + ); + assert!(result.next().await.unwrap().is_none()); + + let mut result = graph + .execute( + query("RETURN { title: $title } as n") + .params([("title", "With empty body")]), + ) + .await + .unwrap(); + let row = result.next().await.unwrap().unwrap(); + let value: Post = row.get("n").unwrap(); + assert_eq!( + Post { + title: "With empty body".into(), + body: None, + }, + value + ); + assert!(result.next().await.unwrap().is_none()); +} + +#[tokio::test] +async fn from_node_props_for_custom_struct() { + let config = ConfigBuilder::default() + .db("neo4j") + .fetch_size(500) + .max_connections(10); + let neo4j = container::Neo4jContainer::from_config(config).await; + let graph = neo4j.graph(); + + let mut result = graph + .execute( + query("CREATE (n:Post { title: $title, body: $body }) RETURN n") + .params([("title", "Hello"), ("body", "World!")]), + ) + .await + .unwrap(); + + while let Ok(Some(row)) = result.next().await { + let node: Node = row.get("n").unwrap(); + let id = node.id(); + let labels = node.labels(); + + let value: Post = node.props().unwrap(); + + assert_eq!( + Post { + title: "Hello".into(), + body: Some("World!".into()), + }, + value + ); + assert_eq!(labels, vec!["Post"]); + assert!(id >= 0); + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index db7b3589..24db1ee4 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -3,6 +3,86 @@ use quote::quote; use syn::parse_macro_input; use syn::{DeriveInput, MetaList}; +fn is_option(ty: &syn::Type) -> bool { + if let syn::Type::Path(syn::TypePath { qself: None, path }) = ty { + let segments_str = &path + .segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::>() + .join(":"); + ["Option", "std:option:Option", "core:option:Option"] + .iter() + .find(|s| segments_str == *s) + .is_some() + } else { + false + } +} + +#[proc_macro_derive(FromBoltType)] +pub fn from_bolt_type(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let struct_name = &ast.ident; + + let fields = if let syn::Data::Struct(structure) = ast.data { + match structure.fields { + syn::Fields::Named(syn::FieldsNamed { named, .. }) => named, + syn::Fields::Unnamed(_) => { + unimplemented!(concat!(stringify!(#name), ": unnamed fields not supported")) + } + syn::Fields::Unit => syn::punctuated::Punctuated::new(), + } + } else { + unimplemented!(concat!(stringify!(#name), ": not a struct")); + }; + + let from_map_fields = fields.iter().map(|f| { + let name = &f.ident; + let ty = &f.ty; + + if is_option(ty) { + quote! { + #name: value.get(stringify!(#name)) + } + } else { + quote! { + #name: value.get(stringify!(#name)).unwrap() + } + } + }); + + let expanded = quote!( + impl From for #struct_name { + fn from(value: BoltMap) -> Self { + Self { + #(#from_map_fields,)* + } + } + } + + impl From for #struct_name { + fn from(value: BoltNode) -> Self { + let value = value.properties; + + value.into() + } + } + + impl From for #struct_name { + fn from(value: BoltType) -> Self { + match value { + BoltType::Map(inner) => inner.into(), + BoltType::Node(inner) => inner.into(), + v => panic!("{}: can not be made from {v:?}", stringify!(#struct_name)) + } + } + } + ); + + expanded.into() +} + #[proc_macro_derive(BoltStruct, attributes(signature))] pub fn derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput);