8 releases
0.2.6 | Oct 23, 2021 |
---|---|
0.2.5 | Oct 22, 2021 |
0.1.0 | Oct 12, 2021 |
#2653 in Database interfaces
45KB
860 lines
tiny-firestore-odm
tiny-firestore-odm
is a lightweight Object Document Mapper for Firestore. It's built on top of
firestore-serde
(which does the
document/object translation), and adds a Rust representation of Firestore collections along with
methods to create/modify/delete from them.
The intent is not to provide access to all of Firestore's functionality, but to provide a simplified interface centered around using Firestore as a key/value store for arbitrary collections of (serializable) Rust objects.
See Are We Google Cloud Yet? for a compatible Rust/GCP stack.
Usage
use google_authz::Credentials;
use tiny_firestore_odm::{Collection, Database, NamedDocument};
use serde::{Deserialize, Serialize};
use tokio_stream::StreamExt;
// Define our data model.
// Any Rust type that implements Serialize and Deserialize can be stored in a Collection.
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ActorRole {
actor: String,
role: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Movie {
pub name: String,
pub year: u32,
pub runtime: u32,
pub cast: Vec<ActorRole>,
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// Use `google-authz` for credential discovery.
let creds = Credentials::default().await;
// Firestore databases are namespaced by project ID, so we need that too.
let project_id = std::env::var("GCP_PROJECT_ID").expect("Expected GCP_PROJECT_ID env var.");
// A Database is the main wrapper around a raw FirestoreClient.
// It gives us a way to create Collections.
let database = Database::new(creds.into(), &project_id).await;
// A Collection is a reference to a Firestore collection, combined with a type.
let movies: Collection<Movie> = database.collection("tiny-firestore-odm-example-movies");
// Construct a movie to insert into our collection.
let movie = Movie {
name: "The Big Lebowski".to_string(),
year: 1998,
runtime: 117,
cast: vec![
ActorRole {
actor: "Jeff Bridges".to_string(),
role: "The Dude".to_string(),
},
ActorRole {
actor: "John Goodman".to_string(),
role: "Walter Sobchak".to_string(),
},
ActorRole {
actor: "Julianne Moore".to_string(),
role: "Maude Lebowski".to_string(),
},
]
};
// Save the movie to the collection. When we insert a document with `create`, it is assigned
// a random key which is returned to us if it is created successfully.
let movie_id = movies.create(&movie).await.unwrap();
// We can use the key that was returned to fetch the film.
let movie_copy = movies.get(&movie_id).await.unwrap();
assert_eq!(movie, movie_copy);
// Alternatively, we can supply a string to use as the key, like this:
movies.try_create(&movie, "The Big Lebowski").await.unwrap();
// Then, we can retrieve it with the same string.
let movie_copy2 = movies.get("The Big Lebowski").await.unwrap();
assert_eq!(movie, movie_copy2);
// To clean up, let's loop over documents in the collection and delete them.
let mut result = movies.list();
// List returns a `futures_core::Stream` of `NamedDocument` objects.
while let Some(NamedDocument {name, ..}) = result.next().await {
movies.delete(&name).await.unwrap();
}
}
Document Existence Semantics
Different methods are provided to achieve different semantics around what to do if the document does or doesn't exist, summarized in the table below.
Method | Behavior if object exists | Behavior if object does not exist |
---|---|---|
create |
N/A (picks new key) | Create |
create_with_key |
Error | Create |
try_create |
Do nothing; return Ok(false) |
Create; return Ok(true) |
upsert |
Replace | Create |
update |
Replace | Error |
delete |
Delete | Error |
Limitations
This crate is designed for workflows that treat Firestore as a key/value store, with each collection corresponding to one Rust type (though one Rust type may correspond to multiple Firestore collections).
It currently does not support functionality outside of that, including:
- Querying by anything except key
- Updating only part of a document
- Transactions
- Subscribing to updates
(I haven't ruled out supporting any of those features, but the goal is crate is not to comprehensively support all GCP features, just a small but useful subset.)
Running tests
The unit tests in this crate can be run without any special setup. To do so, run:
cargo test --lib
There are also integration tests that test the functionality of interacting with the outside world. To use these, you must provide Google Cloud credentials. I recommend creating a Google Cloud project specifically for integration tests, since Firestore is namespaced by project and it avoids the integration tests writing to a database used for other things.
Then, set two environment variables:
GOOGLE_APPLICATION_CREDENTIALS
, containing the absolute path of a.json
file on disk which contains a service account credentials file. You can download this file for a service account through the Google Cloud Console.GCP_PROJECT_ID
, containing the name of the project whose Firebase you would like to use. This is usually the same as theproject_id
field of the service account JSON file.
With these set, you can run:
cargo test
to run all unit and integration tests.
Dependencies
~65MB
~1M SLoC