tiny-firestore-odm 0.2.4

A tiny object-document mapper for Google Firestore, focusing on a key/value object store usage model.
Documentation
# `tiny-firestore-odm`

[![wokflow state](https://github.com/paulgb/tiny-firestore-odm/workflows/Rust/badge.svg)](https://github.com/paulgb/tiny-firestore-odm/actions/workflows/rust.yml)
[![crates.io](https://img.shields.io/crates/v/tiny-firestore-odm.svg)](https://crates.io/crates/tiny-firestore-odm)
[![docs.rs](https://img.shields.io/badge/docs-release-brightgreen)](https://docs.rs/tiny-firestore-odm/)

`tiny-firestore-odm` is a lightweight Object Document Mapper for Firestore. It's built on top of
[`firestore-serde`](https://github.com/paulgb/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?](https://github.com/paulgb/are-we-google-cloud-yet) for a compatible Rust/GCP stack.

## Usage

```rust
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 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.