Last active
January 13, 2016 05:42
-
-
Save sullivan-/7db6719fe4a1091f704e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package longevity.integration.quickStart | |
import com.github.nscala_time.time.Imports._ | |
import org.scalatest.OptionValues._ | |
import org.scalatest._ | |
import org.scalatest.concurrent.ScalaFutures | |
import org.scalatest.concurrent.ScaledTimeSpans | |
import org.scalatest.time._ | |
import scala.concurrent.ExecutionContext.Implicits.global | |
import scala.concurrent.Future | |
/** demonstrates how to get started quickly with longevity. please | |
* read the manual when you get the chance :) | |
* | |
* @see http://longevityframework.github.io/longevity/quick-start.html | |
* @see http://longevityframework.github.io/longevity/manual | |
*/ | |
object QuickStartSpec { | |
// set up your library dependencies in sbt: | |
// resolvers += Resolver.sonatypeRepo("releases") | |
// libraryDependencies += "org.longevityframework" %% "longevity" % "0.4.1" | |
// if you are using mongo, get the mongo driver: | |
// libraryDependencies += "org.mongodb" %% "casbah" % "3.0.0" | |
// for cassandra, you need a driver and json4s: | |
// libraryDependencies += "com.datastax.cassandra" % "cassandra-driver-core" % "2.2.0-rc3" | |
// TODO pt #107891556 add json4s lib deps | |
// start building our subdomain: | |
import longevity.subdomain._ | |
// shorthands help you use typed wrapper classes instead of raw values: | |
object shorthands { | |
// define your shorthand classes: | |
case class Email(email: String) | |
case class Markdown(markdown: String) | |
case class Uri(uri: String) | |
// some convenience methods for using shorthands: | |
implicit def toEmail(email: String) = Email(email) | |
implicit def toMarkdown(markdown: String) = Markdown(markdown) | |
implicit def toUri(uri: String) = Uri(uri) | |
// build your shorthand pool: | |
implicit val shorthandPool = ShorthandPool( | |
Shorthand[Email, String], | |
Shorthand[Markdown, String], | |
Shorthand[Uri, String]) | |
} | |
import shorthands._ | |
// now define your three aggregates: user, blog, and blog post: | |
case class User( | |
username: String, | |
fullname: String, | |
email: Email, | |
profile: Option[UserProfile] = None) | |
extends Root | |
object User extends RootType[User] { | |
object props { | |
val username = prop[String]("username") | |
val email = prop[Email]("email") | |
} | |
val usernameKey = key(props.username) | |
val emailKey = key(props.email) | |
} | |
case class UserProfile( | |
tagline: String, | |
imageUri: Uri, | |
description: Markdown) | |
extends Entity | |
object UserProfile extends EntityType[UserProfile] | |
case class Blog( | |
uri: Uri, | |
title: String, | |
description: Markdown, | |
authors: Set[Assoc[User]]) | |
extends Root | |
object Blog extends RootType[Blog] { | |
object props { | |
val uri = prop[Uri]("uri") | |
} | |
val uriKey = key(props.uri) | |
} | |
case class BlogPost( | |
uriPathSuffix: String, | |
title: String, | |
slug: Option[Markdown] = None, | |
content: Markdown, | |
labels: Set[String] = Set(), | |
postDate: DateTime, | |
blog: Assoc[Blog], | |
authors: Set[Assoc[User]]) | |
extends Root | |
object BlogPost extends RootType[BlogPost] { | |
object props { | |
val blog = prop[Assoc[Blog]]("blog") | |
val uriPathSuffix = prop[String]("uriPathSuffix") | |
val postDate = prop[DateTime]("postDate") | |
} | |
val uriKey = key(props.blog, props.uriPathSuffix) | |
} | |
// build the subdomain: | |
val blogCore = Subdomain("blogging", EntityTypePool(User, Blog, BlogPost)) | |
// now build the context: | |
import longevity.context._ | |
val context = LongevityContext(blogCore, Mongo) | |
// create some unpersisted entities: | |
val john = User("smithy", "John Smith", "smithy@john-smith.ninja") | |
val frank = User("franky", "Francis Nickerson", "franky@john-smith.ninja") | |
val jerry = User("jerry", "Jerry Jones", "jerry@john-smith.ninja") | |
val blog = Blog( | |
uri = "http://blog.john-smith.ninja/", | |
title = "The Blogging Ninjas", | |
description = "We try to keep things interesting blogging about ninjas.", | |
authors = Set(Assoc(john), Assoc(frank))) | |
val johnsPost = BlogPost( | |
uriPathSuffix = "johns_first_post", | |
title = "John's first post", | |
content = "_work in progress_", | |
postDate = DateTime.now, | |
blog = Assoc(blog), | |
authors = Set(Assoc(john))) | |
val franksPost = BlogPost( | |
uriPathSuffix = "franks_first_post", | |
title = "Frank's first post", | |
content = "_work in progress_", | |
postDate = DateTime.now, | |
blog = Assoc(blog), | |
authors = Set(Assoc(frank))) | |
} | |
class QuickStartSpec | |
extends FlatSpec | |
with BeforeAndAfterAll | |
with GivenWhenThen | |
with Matchers | |
with ScalaFutures | |
with ScaledTimeSpans { | |
override implicit def patienceConfig = PatienceConfig( | |
timeout = scaled(Span(4000, Millis)), | |
interval = scaled(Span(50, Millis))) | |
import QuickStartSpec._ | |
import longevity.subdomain._ | |
import longevity.persistence._ | |
// get the repo pool: | |
// normally we would use `context.repoPool` here, but since this is actually | |
// a test, we will use the test DB: | |
val repos = context.testRepoPool | |
val userRepo = repos[User] | |
val blogRepo = repos[Blog] | |
val blogPostRepo = repos[BlogPost] | |
"QuickStartSpec" should "exercise basic longevity functionality" in { | |
// persist the aggregates all at once. it doesn't matter what | |
// order you pass them in, this method will assure that associated | |
// aggregates always get persisted first. | |
val createManyResult: Future[Seq[PState[_ <: Root]]] = | |
repos.createMany(john, frank, blog, johnsPost, franksPost) | |
// `futureValue` is a ScalaTest way of saying "wait for the future to | |
// complete and assert success" | |
createManyResult.futureValue | |
// retrieve an entity: | |
// `Repo[User].retrieve` returns a `Future[Option[PState[User]]]`, | |
// aka `FOPState[User]` | |
val retrieveResult: FOPState[User] = | |
userRepo.retrieve(User.usernameKey(john.username)) | |
// unwrap the future and option: | |
// `value` is a ScalaTest way of saying "assert the Option is defined, and | |
// get the contents" | |
val userState: PState[User] = retrieveResult.futureValue.value | |
// unwrap the persistent state: | |
val user: User = userState.get | |
user should equal (john) | |
// modify the user and persist the change: | |
val modified: PState[User] = | |
userState.map { user: User => user.copy(fullname = "John Smith Jr.") } | |
val updateResult: FPState[User] = userRepo.update(modified) | |
val updatedUserState: PState[User] = updateResult.futureValue | |
// create a new blog post: | |
val blogKeyVal: root.KeyVal[Blog] = Blog.uriKey(blog.uri) | |
val blogState: PState[Blog] = | |
blogRepo.retrieve(blogKeyVal).futureValue.value | |
val newPost = BlogPost( | |
uriPathSuffix = "new_post", | |
title = "New post", | |
content = "_work in progress_", | |
postDate = DateTime.now, | |
blog = blogState.assoc, | |
authors = Set(userState.assoc)) | |
val futurePostState: Future[PState[BlogPost]] = | |
blogPostRepo.create(newPost) | |
// clean up the new post: | |
val postState = futurePostState.futureValue | |
blogPostRepo.delete(postState).futureValue | |
// add a new author to a blog: | |
val newUserState = userRepo.create(jerry).futureValue | |
val modifiedBlogState = blogState.map { blog => | |
blog.copy(authors = blog.authors + updatedUserState.assoc) | |
} | |
blogRepo.update(modifiedBlogState) | |
// there are convenience methods in `FPState` and `FOPState` that allow | |
// you to conveniently manipulate the enclosed root. for example, suppose | |
// we have two service methods that work on a user: | |
object userService { | |
def updateUser(user: User): User = user | |
def updateUserReactive(user: User): Future[User] = Future.successful(user) | |
} | |
// we can apply these service methods directly to an `FPState[User]` or an | |
// `FOPState[User]`, like so: | |
val updated: FOPState[User] = | |
userRepo.retrieve( | |
User.usernameKey(john.username) | |
).mapRoot( | |
userService.updateUser _ | |
).flatMapState( | |
userRepo.update(_) | |
) | |
updated.futureValue | |
val updatedReactive: FOPState[User] = | |
userRepo.retrieve( | |
User.usernameKey(john.username) | |
).flatMapRoot( | |
userService.updateUserReactive _ | |
).flatMapState( | |
userRepo.update(_) | |
) | |
updatedReactive.futureValue | |
// equivalent to above, but without mapRoot and flatMapRoot | |
val updatedNoMapRoot: FPState[User] = { | |
userRepo retrieve User.usernameKey(john.username) flatMap { optUserState => | |
val userState = optUserState getOrElse { throw new RuntimeException } | |
val updatedState = userState map userService.updateUser | |
userRepo update updatedState | |
} | |
} | |
def applyEvent(username: String): FPState[User] = { | |
userRepo retrieve User.usernameKey(username) flatMap { optUserState => | |
val userState = optUserState getOrElse { throw new RuntimeException } | |
val updatedState = userState map userService.updateUser | |
userRepo update updatedState | |
} | |
} | |
// use an `Assoc` to retrieve an author from a blog post: | |
val post: BlogPost = blogPostRepo.retrieve( | |
BlogPost.uriKey(blogState.assoc, johnsPost.uriPathSuffix) | |
).futureValue.value.get | |
val authorAssoc: Assoc[User] = post.authors.head | |
val author: FPState[User] = userRepo.retrieveOne(authorAssoc) | |
// find posts for a given blog published in the last week: | |
import BlogPost.queryDsl._ | |
val recentPosts: Future[Seq[PState[BlogPost]]] = blogPostRepo.retrieveByQuery( | |
BlogPost.props.blog eqs blogState.assoc and | |
BlogPost.props.postDate gt DateTime.now - 1.week) | |
recentPosts.futureValue.size should equal (2) | |
// same thing without the DSL: | |
import longevity.subdomain.root.Query | |
val noDsl: Future[Seq[PState[BlogPost]]] = blogPostRepo.retrieveByQuery( | |
Query.and( | |
Query.eqs(BlogPost.props.blog, blogState.assoc), | |
Query.gt(BlogPost.props.postDate, DateTime.now - 1.week))) | |
noDsl.futureValue.size should equal (2) | |
} | |
// clean up the database after the test: | |
override def afterAll = { | |
deleteUser(john) | |
deleteUser(frank) | |
deleteUser(jerry) | |
deletePost(johnsPost, blog) | |
deletePost(franksPost, blog) | |
deleteBlog(blog) | |
} | |
private def deleteUser(user: User): Unit = { | |
val futureDeleted = for { | |
optUserState <- userRepo retrieve User.usernameKey(user.username) | |
deleted <- optUserState map userRepo.delete getOrElse Future.successful(()) | |
} yield deleted | |
futureDeleted.futureValue | |
} | |
private def deletePost(post: BlogPost, blog: Blog): Unit = { | |
val deleted = for { | |
optBlogState <- blogRepo retrieve Blog.uriKey(blog.uri) | |
optKeyVal = optBlogState map { blogState => BlogPost.uriKey(blogState.assoc, post.uriPathSuffix) } | |
optPostState <- optKeyVal map blogPostRepo.retrieve getOrElse Future.successful(None) | |
deleted <- optPostState map blogPostRepo.delete getOrElse Future.successful(()) | |
} yield deleted | |
deleted.futureValue | |
} | |
private def deleteBlog(blog: Blog): Unit = { | |
val deleted = for { | |
optBlogState <- blogRepo retrieve Blog.uriKey(blog.uri) | |
deleted <- optBlogState map blogRepo.delete getOrElse Future.successful(()) | |
} yield deleted | |
deleted.futureValue | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment