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.
```rust use googleauthz::Credentials; use tinyfirestoreodm::{Collection, Database, NamedDocument}; use serde::{Deserialize, Serialize}; use tokiostream::StreamExt;
// Define our data model. // Any Rust type that implements Serialize and Deserialize can be stored in a Collection.
struct ActorRole { actor: String, role: String, }
struct Movie {
pub name: String,
pub year: u32,
pub runtime: u32,
pub cast: Vec
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 projectid = std::env::var("GCPPROJECTID").expect("Expected GCPPROJECT_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();
}
} ```
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 |
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.)
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 the project_id
field of the service account
JSON file.
With these set, you can run:
cargo test
to run all unit and integration tests.