A Rust Crux Client crate/lib. For now, this crate intends to support 2 ways to interact with Crux:
Docker
with a crux-standalone
version docker-hub.HTTP
using the REST API
.Other solutions may be added after the first release.
Docker only
) uses a few modified endpoints due to its Docker implementation.ATM Crux
(under development) for more complete and interactive example.To add this crate to your project you should add one of the following line to your dependencies
field in Cargo.toml
:
>
[dependencies] transistor = "0.4.3"
All operations with Transistor start in the module client
with Crux::new("localhost", "3000")
. The struct Crux
is responsabile for defining request HeadersMap
and the request URL
. The URL
definition is required and it is done by the static function new
, which receives as argument a host
and a port
and returns a Crux
instance. To change HeadersMap
info so that you can add AUTHORIZATION
you can use the function with_authorization
that receives as argument the authorization token and mutates the Crux
instance.
* HeaderMap
already contains the header Content-Type: application/edn
.
Finally, to create a Crux Client the function <type>_client
should be called, for example docker_client
. This function returns a struct that contains all possible implementarions to query Crux Docker.
```rust
use transistor::client::Crux;
// DockerClient with AUTHORIZATION let authclient = Crux::new("127.0.0.1","3000").withauthorization("my-auth-token").docker_client();
// DockerClient without AUTHORIZATION let client = Crux::new("127.0.0.1","3000").docker_client(); ```
Once you have called docker_client
you will have an instance of the DockerClient
struct which has a bunch of functions to query Crux on Docker:
* state
queries endpoint /
with a GET
. No args. Returns various details about the state of the database.
```rust
let body = client.state().unwrap();
// StateResponse { // indexindexversion: 5, // doclogconsumerstate: None, // txlogconsumerstate: None, // kvkvstore: "crux.kv.rocksdb.RocksKv", // kvestimatenumkeys: 56, // kvsize: 2271042 // } ```
tx_log
requests endpoint /tx-log
via POST
. A Vector of Action
is expected as argument. The "write" endpoint, to post transactions.
```rust
use transistor::docker::{Action};
use transistor::client::Crux;
use transistor::types::{CruxId};let person1 = Person { cruxdb_id: CruxId::new("jorge-3"), .. };
let person2 = Person { cruxdb_id: CruxId::new("manuel-1"), .. };
let action1 = Action::Put(person1.serialize()); let action2 = Action::Put(person2.serialize());
let body = client.tx_log(vec![action1, action2]).unwrap(); // {:crux.tx/tx-id 7, :crux.tx/tx-time #inst \"2020-07-16T21:50:39.309-00:00\"} ```
tx_logs
requests endpoint /tx-log
via GET
. No args. Returns a list of all transactions.
```rust
use transistor::client::Crux;let body = client.tx_logs().unwrap();
// TxLogsResponse { // txevents: [ // TxLogResponse { // txtxid: 0, // txtxtime: "2020-07-09T23:38:06.465-00:00", // txeventtxevents: Some( // [ // [ // ":crux.tx/put", // "a15f8b81a160b4eebe5c84e9e3b65c87b9b2f18e", // "125d29eb3bed1bf51d64194601ad4ff93defe0e2", // ], // ], // ), // }, // TxLogResponse { // txtxid: 1, // txtxtime: "2020-07-09T23:39:33.815-00:00", // txeventtx_events: Some( // [ // [ // ":crux.tx/put", // "a15f8b81a160b4eebe5c84e9e3b65c87b9b2f18e", // "1b42e0d5137e3833423f7bb958622bee29f91eee", // ], // ], // ), // }, // ... // ] // } ```
entity
requests endpoint /entity
via POST
. A serialized CruxId
, serialized Edn::Key
or a String containing a keyword
must be passed as argument. Returns an entity for a given ID and optional valid-time/transaction-time co-ordinates.
```rust
let person = Person {
cruxdb_id: CruxId::new("hello-entity"),
...
};let client = Crux::new("localhost", "3000").docker_client();
let ednbody = client.entity(person.cruxdbid.serialize()).unwrap(); // Map( // Map( // { // ":crux.db/id": Key( // ":hello-entity", // ), // ":first-name": Str( // "Hello", // ), // ":last-name": Str( // "World", // ), // }, // ), // ) ```
entity_tx
requests endpoint /entity-tx
via POST
. A serialized CruxId
, serialized Edn::Key
or a String containing a keyword
must be passed as argument. Returns the transaction that most recently set a key.
```rust
use transistor::docker::{Action};
use transistor::client::Crux;
use transistor::types::{CruxId};let person = Person { cruxdb_id: CruxId::new("hello-entity"), ... };
let client = Crux::new("localhost", "3000").docker_client();
let txbody = client.entitytx(person.cruxdbid.serialize()).unwrap(); // EntityTxResponse { // dbid: "d72ccae848ce3a371bd313865cedc3d20b1478ca", // dbcontenthash: "1828ebf4466f98ea3f5252a58734208cd0414376", // dbvalidtime: "2020-07-20T20:38:27.515-00:00", // txtxid: 31, // tx_tx_time: "2020-07-20T20:38:27.515-00:00", // } ```
document_by_id
requests endpoint /document/{:content-hash}
via GET
. {:content-hash}
can be obtained with function entity_tx
. Returns the document for a given content hash.
```rust
use transistor::docker::{Action};
use transistor::client::Crux;
use transistor::types::{CruxId};let person = Person { cruxdbid: CruxId::new("hello-entity"), firstname: "Hello".tostring(), lastname: "World".to_string() };
let client = Crux::new("localhost", "3000").docker_client();
let document = client.documentbyid(txbody.dbcontenthash).unwrap(); // Person { // cruxdb_id: CruxId( // ":hello-entity", // ), // firstname: "Hello", // last_name: "World", // } ```
documents
requests endpoint /documents
via POST
. The argument of this reuqest is a vector of content-hashes
that converts to an edn set as a body. Returns a map of document ids and respective documents for a given set of content hashes submitted in the request body.
```rust
use transistor::docker::{Action};
use transistor::client::Crux;
use transistor::types::{CruxId};let person1 = Person { cruxdb_id: CruxId::new("hello-entity"), ... };
let person2 = Person { cruxdb_id: CruxId::new("hello-documents"), ... };
let client = Crux::new("localhost", "3000").docker_client();
let contesnthashes = vec![txbody1.dbcontenthash, txbody2.dbcontent_hash];
let documents = client.documents(contesnt_hashes).unwrap();
// {
// "1828ebf4466f98ea3f5252a58734208cd0414376": Map(
// Map(
// {
// ":crux.db/id": Key(
// ":hello-entity",
// ),
// ":first-name": Str(
// "Hello",
// ),
// ":last-name": Str(
// "World",
// ),
// },
// ),
// ),
// "1aeb98a4e11f30827e0304a9c289aad673b6cf57": Map(
// Map(
// {
// ":crux.db/id": Key(
// ":hello-documents",
// ),
// ":first-name": Str(
// "Hello",
// ),
// ":last-name": Str(
// "Documents",
// ),
// },
// ),
// ),
// }
* [`query`](https://docs.rs/transistor/0.4.1/transistor/docker/struct.DockerClient.html#method.query) requests endpoint [`/query`](https://opencrux.com/docs#rest-query) via `POST`. Argument is a `query` of the type `Query`. Retrives a Set containing a vector of the values defined by the function `Query::find`.
Available functions are `find`, `where_clause`, `args`, `order_by`, `limit`, `offset`, examples [`complex_query`](https://github.com/naomijub/transistor/blob/master/examples/complex_query.rs) and [`limit_offset_query`](https://github.com/naomijub/transistor/blob/master/examples/limit_offset_query.rs) have examples on how to use them.
rust
use transistor::client::Crux;
use transistor::types::{query::Query};
let client = Crux::new("localhost", "3000").docker_client();
let queryissql = Query::find(vec!["?p1", "?n"]) .where_clause(vec!["?p1 :name ?n", "?p1 :is-sql true"]) .build(); // Query: // {:query // {:find [?p1 ?n] // :where [[?p1 :name ?n] // [?p1 :is-sql true]]}}
let issql = client.query(queryis_sql.unwrap()).unwrap(); // {[":mysql", "MySQL"], [":postgres", "Postgres"]} BTreeSet ```
Action
is an enum with a set of options to use in association with the function tx_log
:
* PUT
- Write a version of a document
* Delete
- Deletes the specific document at a given valid time
* Evict
- Evicts a document entirely, including all historical versions (receives only the ID to evict)
Query
is a struct responsible for creating the fields and serializing them into the correct query
format. It has a function for each field and a build
function to help check if it is correctyly formatted.
* find
is a static builder function to define the elements inside the :find
clause.
* where_clause
is a builder function that defines the vector os elements inside the :where []
array.
* order_by
is a builder function to define the elements inside the :order-by
clause.
* args
is a builder function to define the elements inside the :args
clause.
* limit
is a builder function to define the elements inside the :limit
clause.
* offset
is a builder function to define the elements inside the :offset
clause.
Errors are defined in the CruxError
enum.
* ParseEdnError
is originated by edn_rs
crate. The provided EDN did not match schema.
* RequestError
is originated by reqwest
crate. Failed to make HTTP request.
* QueryFormatError
is originated when the provided Query struct did not match schema.
* QueryError
is responsible for encapsulation the Stacktrace error from Crux response:
```rust
use transistor::client::Crux;
use transistor::types::{query::Query};
let client = Crux::new("localhost", "3000").dockerclient();
// field n
doesn't exist
let queryerrorresponse = Query::find(vec!["?p1", "?n"])
.whereclause(vec!["?p1 :name ?g", "?p1 :is-sql true"])
.build();
let error = client.query(queryerrorresponse?)?; println!("Stacktrace n{:?}", error);
// Stacktrace // QueryError("{:via // [{:type java.lang.IllegalArgumentException, // :message \"Find refers to unknown variable: n\", // :at [crux.query$q invokeStatic \"query.clj\" 1152]}], // :trace // [[crux.query$q invokeStatic \"query.clj\" 1152] // [crux.query$q invoke \"query.clj\" 1099] // [crux.query$q$fn10850 invoke \"query.clj\" 1107] // [clojure.core$bindingconveyorfn$fn5754 invoke \"core.clj\" 2030] // [clojure.lang.AFn call \"AFn.java\" 18] // [java.util.concurrent.FutureTask run \"FutureTask.java\" 264] // [java.util.concurrent.ThreadPoolExecutor // runWorker // \"ThreadPoolExecutor.java\" // 1128] // [java.util.concurrent.ThreadPoolExecutor$Worker // run // \"ThreadPoolExecutor.java\" // 628] // [java.lang.Thread run \"Thread.java\" 834]], // :cause \"Find refers to unknown variable: n\"} // ") ```
For testing purpose there is a feature
called mock
that enables the docker_mock
function that is a replacement for the docker_client
function. To use it run your commands with the the flag --features "mock"
as in cargo test --test lib --no-fail-fast --features "mock"
. The mocking feature uses the crate mockito = "0.26"
as a Cargo dependency. An example usage with this feature enabled:
```rust use transistor::client::Crux; use transistor::docker::Action; use transistor::ednrs::{serstruct, Serialize}; use transistor::types::{CruxId}; use mockito::mock;
fn mockclient() { let _m = mock("POST", "/tx-log") .withstatus(200) .matchbody("[[:crux.tx/put { :crux.db/id :jorge-3, :first-name \"Michael\", :last-name \"Jorge\", }], [:crux.tx/put { :crux.db/id :manuel-1, :first-name \"Diego\", :last-name \"Manuel\", }]]") .withheader("content-type", "text/plain") .with_body("{:crux.tx/tx-id 8, :crux.tx/tx-time #inst \"2020-07-16T21:53:14.628-00:00\"}") .create();
let person1 = Person {
// ...
};
let person2 = Person {
/// ...
};
let actions = vec![Action::Put(person1.serialize()), Action::Put(person2.serialize())];
let body = Crux::new("localhost", "3000")
.docker_mock()
.tx_log(actions)
.unwrap();
assert_eq!(
format!("{:?}", body),
String::from("TxLogResponse { tx___tx_id: 8, tx___tx_time: \"2020-07-16T21:53:14.628-00:00\", tx__event___tx_events: None }")
);
}
serstruct! { #[derive(Debug, Clone)] #[allow(nonsnakecase)] pub struct Person { cruxdbid: CruxId, // ... } }
```
A strong dependency of this crate is the edn-rs crate, as many of the return types are in the Edn format. The sync http client is reqwest
with blocking
feature enabled.
This project is licensed under LGPP-3.0 (GNU Lesser General Public License v3.0).