Skip to content

Commit

Permalink
Merge pull request #29 from thedodd/28-find-one-and-do
Browse files Browse the repository at this point in the history
28 implement find_one_and_* methods.
  • Loading branch information
thedodd authored Dec 30, 2018
2 parents 4b70711 + 6327556 commit f6d4707
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 19 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ changelog
=========

## 0.8
The core `wither` crate is 100% backwards compatible with this relase.
The core `wither` crate is 100% backwards compatible with this relase, but the `Model` trait has received a few additional methods. Namely the `find_one_and_(delete|replace|update)` methods. Came across a use case where I needed them and then realized that I never implemented them. Now they are here. Woot woot!

The `wither_derive` crate has received a few backwareds incompatible changes. The motivation behind doing this is detailed in [#21](https://github.com/thedodd/wither/issues/21). The main issue is that we need the derive system to be abstract enough to deal with embedded documents. The backwards incompatible changes are here.
- within `#[model(index())]`, the `index_type` attr has been reduced to simply be `index`. All of the same rules apply as before. This change was made for ergonomic reasons. Less typing. Easier to follow.
Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions wither/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
authors = ["Anthony Dodd <[email protected]>"]
categories = ["database", "data-structures"]
description = "An ODM for MongoDB built upon the mongo rust driver."
documentation = "https://github.com/thedodd/wither"
documentation = "https://docs.rs/wither"
homepage = "https://github.com/thedodd/wither"
keywords = ["mongodb", "odm", "orm"]
license = "Apache-2.0"
name = "wither"
readme = "../README.md"
repository = "https://github.com/thedodd/wither"
version = "0.8.0-beta.0"
version = "0.8.0"

[dependencies]
chrono = "0.4"
Expand All @@ -20,7 +20,7 @@ serde_derive = "1"

[dev-dependencies]
lazy_static = "1"
wither_derive = { version = "0.8.0-beta.0", path = "../wither_derive" }
wither_derive = { version = "0.8.0", path = "../wither_derive" }

[features]
docinclude = [] # Used only for activating `doc(include="...")` on nightly.
Expand Down
39 changes: 39 additions & 0 deletions wither/docs/underlying-driver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
### underlying driver
If at any point in time you need direct access to the [underlying driver](https://docs.rs/mongodb/latest/mongodb/), it is always available. All of the `Model` interface methods take a handle to the database, which is part of the underlying driver. You can then use the [`Model::COLLECTION_NAME`](https://docs.rs/wither/latest/wither/model/trait.Model.html#associatedconstant.COLLECTION_NAME) to ensure you are accessing the correct collection. You can also use the various model convenience methods for serialization, such as the [`Model::instance_from_document`](https://docs.rs/wither/latest/wither/model/trait.Model.html#method.instance_from_document) method.

```rust
# #[macro_use]
# extern crate mongodb;
# extern crate serde;
# #[macro_use(Serialize, Deserialize)]
# extern crate serde_derive;
# extern crate wither;
# #[macro_use(Model)]
# extern crate wither_derive;
#
# use wither::prelude::*;
# use mongodb::{
# Document, ThreadedClient,
# coll::options::IndexModel,
# db::ThreadedDatabase,
# };
#
#[derive(Model, Serialize, Deserialize)]
struct MyModel {
#[serde(rename="_id", skip_serializing_if="Option::is_none")]
pub id: Option<mongodb::oid::ObjectId>,

#[model(index(index="dsc", unique="true"))]
pub email: String,
}

fn main() {
// Your DB handle. This is part of the underlying driver.
let db = mongodb::Client::with_uri("mongodb://localhost:27017/").unwrap().db("mydb");

// Use the driver directly, but rely on your model for getting the collection name.
let coll = db.collection(MyModel::COLLECTION_NAME);

// Now you can use the raw collection interface to your heart's content.
}
```
54 changes: 46 additions & 8 deletions wither/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use mongodb::{
Collection,
options::{
CountOptions,
FindOneAndDeleteOptions,
FindOneAndUpdateOptions,
FindOptions,
IndexModel,
Expand Down Expand Up @@ -58,6 +59,7 @@ pub fn basic_index_options(name: &str, background: bool, unique: Option<bool>, e
#[cfg_attr(feature="docinclude", doc(include="../docs/model-sync.md"))]
#[cfg_attr(feature="docinclude", doc(include="../docs/logging.md"))]
#[cfg_attr(feature="docinclude", doc(include="../docs/manually-implementing-model.md"))]
#[cfg_attr(feature="docinclude", doc(include="../docs/underlying-driver.md"))]
pub trait Model<'a> where Self: Serialize + Deserialize<'a> {

/// The name of the collection where this model's data is stored.
Expand All @@ -69,8 +71,8 @@ pub trait Model<'a> where Self: Serialize + Deserialize<'a> {
/// Set the ID for this model.
fn set_id(&mut self, ObjectId);

///////////////////////////////
// Write Concern Abstraction //
//////////////////////////////////////////////////////////////////////////////////////////////
// Write Concern Abstraction /////////////////////////////////////////////////////////////////

/// The model's write concern for database writes.
///
Expand Down Expand Up @@ -168,8 +170,44 @@ pub trait Model<'a> where Self: Serialize + Deserialize<'a> {
Ok(Some(instance))
}

////////////////////
// Instance Layer //
/// Finds a single document and deletes it, returning the original.
fn find_one_and_delete(db: Database, filter: Document, options: Option<FindOneAndDeleteOptions>) -> Result<Option<Self>> {
db.collection(Self::COLLECTION_NAME).find_one_and_delete(filter, options)
.and_then(|docopt| match docopt {
Some(doc) => match Self::instance_from_document(doc) {
Ok(model) => Ok(Some(model)),
Err(err) => Err(err),
}
None => Ok(None),
})
}

/// Finds a single document and replaces it, returning either the original or replaced document.
fn find_one_and_replace(db: Database, filter: Document, replacement: Document, options: Option<FindOneAndUpdateOptions>) -> Result<Option<Self>> {
db.collection(Self::COLLECTION_NAME).find_one_and_replace(filter, replacement, options)
.and_then(|docopt| match docopt {
Some(doc) => match Self::instance_from_document(doc) {
Ok(model) => Ok(Some(model)),
Err(err) => Err(err),
}
None => Ok(None),
})
}

/// Finds a single document and updates it, returning either the original or updated document.
fn find_one_and_update(db: Database, filter: Document, update: Document, options: Option<FindOneAndUpdateOptions>) -> Result<Option<Self>> {
db.collection(Self::COLLECTION_NAME).find_one_and_update(filter, update, options)
.and_then(|docopt| match docopt {
Some(doc) => match Self::instance_from_document(doc) {
Ok(model) => Ok(Some(model)),
Err(err) => Err(err),
}
None => Ok(None),
})
}

//////////////////////////////////////////////////////////////////////////////////////////////
// Instance Layer ////////////////////////////////////////////////////////////////////////////

/// Delete this model instance by ID.
fn delete(&self, db: Database) -> Result<()> {
Expand Down Expand Up @@ -311,8 +349,8 @@ pub trait Model<'a> where Self: Serialize + Deserialize<'a> {
})
}

/////////////////////////
// Convenience Methods //
//////////////////////////////////////////////////////////////////////////////////////////////
// Convenience Methods ///////////////////////////////////////////////////////////////////////

/// Attempt to serialize the given bson document into an instance of this model.
fn instance_from_document(document: Document) -> Result<Self> {
Expand All @@ -322,8 +360,8 @@ pub trait Model<'a> where Self: Serialize + Deserialize<'a> {
}
}

///////////////////////
// Maintenance Layer //
//////////////////////////////////////////////////////////////////////////////////////////////
// Maintenance Layer /////////////////////////////////////////////////////////////////////////

/// Get the vector of index models for this model.
fn indexes() -> Vec<IndexModel> {
Expand Down
108 changes: 108 additions & 0 deletions wither/tests/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,114 @@ fn model_find_one_should_fetch_the_model_instance_matching_given_filter() {
assert_eq!(&user_from_db.email, &user.email);
}

//////////////////////////////////////////////////////////////////////////////
// Model::find_one_and_delete ////////////////////////////////////////////////

#[test]
fn model_find_one_and_delete_should_delete_the_target_doc() {
let fixture = Fixture::new().with_dropped_database().with_synced_models();
let db = fixture.get_db();
let mut user = User{id: None, email: "[email protected]".to_string()};
let mut user2 = User{id: None, email: "[email protected]".to_string()};

user.save(db.clone(), None).expect("Expected a successful save operation.");
user2.save(db.clone(), None).expect("Expected a successful save operation.");
let output = User::find_one_and_delete(db.clone(), doc!{"email": "[email protected]"}, None)
.expect("Expected a operation.").unwrap();

assert_eq!(&output.email, &user.email);
}

//////////////////////////////////////////////////////////////////////////////
// Model::find_one_and_replace ///////////////////////////////////////////////

#[test]
fn model_find_one_and_replace_should_replace_the_target_doc_and_return_new_doc() {
let fixture = Fixture::new().with_dropped_database().with_synced_models();
let db = fixture.get_db();
let mut user = User{id: None, email: "[email protected]".to_string()};
let mut user2 = User{id: None, email: "[email protected]".to_string()};
let mut opts = FindOneAndUpdateOptions::new();
opts.return_document = Some(ReturnDocument::After);

user.save(db.clone(), None).expect("Expected a successful save operation.");
user2.save(db.clone(), None).expect("Expected a successful save operation.");
let output = User::find_one_and_replace(
db.clone(),
doc!{"email": "[email protected]"},
doc!{"email": "[email protected]"},
Some(opts),
).expect("Expected a operation.").unwrap();

assert_eq!(&output.email, "[email protected]");
}

#[test]
fn model_find_one_and_replace_should_replace_the_target_doc_and_return_old_doc() {
let fixture = Fixture::new().with_dropped_database().with_synced_models();
let db = fixture.get_db();
let mut user = User{id: None, email: "[email protected]".to_string()};
let mut user2 = User{id: None, email: "[email protected]".to_string()};
let mut opts = FindOneAndUpdateOptions::new();
opts.return_document = Some(ReturnDocument::Before);

user.save(db.clone(), None).expect("Expected a successful save operation.");
user2.save(db.clone(), None).expect("Expected a successful save operation.");
let output = User::find_one_and_replace(
db.clone(),
doc!{"email": "[email protected]"},
doc!{"email": "[email protected]"},
Some(opts),
).expect("Expected a operation.").unwrap();

assert_eq!(&output.email, "[email protected]");
}

//////////////////////////////////////////////////////////////////////////////
// Model::find_one_and_update ////////////////////////////////////////////////

#[test]
fn model_find_one_and_update_should_update_target_document_and_return_new() {
let fixture = Fixture::new().with_dropped_database().with_synced_models();
let db = fixture.get_db();
let mut user = User{id: None, email: "[email protected]".to_string()};
let mut user2 = User{id: None, email: "[email protected]".to_string()};
let mut opts = FindOneAndUpdateOptions::new();
opts.return_document = Some(ReturnDocument::After);

user.save(db.clone(), None).expect("Expected a successful save operation.");
user2.save(db.clone(), None).expect("Expected a successful save operation.");
let output = User::find_one_and_update(
db.clone(),
doc!{"email": "[email protected]"},
doc!{"$set": doc!{"email": "[email protected]"}},
Some(opts),
).expect("Expected a operation.").unwrap();

assert_eq!(&output.email, "[email protected]");
}

#[test]
fn model_find_one_and_update_should_update_target_document_and_return_old() {
let fixture = Fixture::new().with_dropped_database().with_synced_models();
let db = fixture.get_db();
let mut user = User{id: None, email: "[email protected]".to_string()};
let mut user2 = User{id: None, email: "[email protected]".to_string()};
let mut opts = FindOneAndUpdateOptions::new();
opts.return_document = Some(ReturnDocument::Before);

user.save(db.clone(), None).expect("Expected a successful save operation.");
user2.save(db.clone(), None).expect("Expected a successful save operation.");
let output = User::find_one_and_update(
db.clone(),
doc!{"email": "[email protected]"},
doc!{"$set": doc!{"email": "[email protected]"}},
Some(opts),
).expect("Expected a operation.").unwrap();

assert_eq!(&output.email, "[email protected]");
}

//////////////////////////////////////////////////////////////////////////////
// Model.update //////////////////////////////////////////////////////////////

Expand Down
4 changes: 2 additions & 2 deletions wither_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
authors = ["Anthony Dodd <[email protected]>"]
categories = ["database", "data-structures"]
description = "An ODM for MongoDB built upon the mongo rust driver."
documentation = "https://github.com/thedodd/wither"
documentation = "https://docs.rs/wither"
homepage = "https://github.com/thedodd/wither"
keywords = ["mongodb", "odm", "orm"]
license = "Apache-2.0"
name = "wither_derive"
readme = "README.md"
repository = "https://github.com/thedodd/wither"
version = "0.8.0-beta.0"
version = "0.8.0"

[lib]
proc-macro = true
Expand Down

0 comments on commit f6d4707

Please sign in to comment.