Skip to content

Instantly share code, notes, and snippets.

@irenelfeng
Last active December 26, 2015 23:04
Show Gist options
  • Save irenelfeng/61b72835af3e3600d60e to your computer and use it in GitHub Desktop.
Save irenelfeng/61b72835af3e3600d60e to your computer and use it in GitHub Desktop.
GirlRising example of inheritance on Scala, creating abstract class Versions.
package com.girlrising.models.resources.resources
import reactivemongo.bson._
import com.github.nscala_time.time.Imports._
case class AssetVersion
(
id: Int,
timeCreated: DateTime,
notes: Option[String] = None,
owner: String,
url: Option[String]
) extends Version(id, timeCreated, notes, owner, url)
object AssetVersion{
implicit object AssetVersionReader extends BSONDocumentReader[AssetVersion]{
def read(doc: BSONDocument) = {
AssetVersion(
doc.getAs[Int]("id").get,
doc.getAs[BSONDateTime]("timeCreated").map(dt => new DateTime(dt.value)).get,
doc.getAs[String]("notes"),
doc.getAs[String]("owner").get,
doc.getAs[String]("url")
)
}
}
implicit object AssetVersionWriter extends BSONDocumentWriter[AssetVersion]{
def write(version: AssetVersion) = {
BSONDocument(
"id" -> version.id,
"timeCreated" -> BSONDateTime(version.timeCreated.getMillis),
"notes" -> version.notes,
"url" -> version.url,
"owner" -> version.owner
)
}
}
}
package com.girlrising
import akka.actor.{ActorSystem, Props}
import akka.io.IO
import spray.can.Http
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import com.typesafe.config.ConfigFactory
import com.girlrising.services.MainServiceActor
import reactivemongo.api._
import reactivemongo.core.nodeset.Authenticate
import reactivemongo.bson._
object Boot extends App {
// load configuration
val conf = ConfigFactory.load()
// we need an ActorSystem to host our application in
implicit val system = ActorSystem("on-spray-can")
// create and start our service actor
val service = system.actorOf(Props[MainServiceActor], "demo-service")
val mongoConfig = conf.getConfig("mongolab")
val driver = new MongoDriver
val servers = List(mongoConfig.getString("url"))
val dbName = mongoConfig.getString("dbName")
val userName = mongoConfig.getString("userName")
val password = mongoConfig.getString("password")
val credentials = List(Authenticate(dbName, userName, password))
val conOpts = MongoConnectionOptions(authMode = ScramSha1Authentication)
val connection = driver.connection(servers, options = conOpts, authentications = credentials)
val db = connection(dbName)
println("Bind")
implicit val timeout = Timeout(5.seconds)
// start a new HTTP server on port 8080 with our service actor as the handler
IO(Http) ? Http.Bind(service, interface = "localhost", port = 8080)
}
package com.girlrising.controllers.database
import scala.concurrent.{ExecutionContext, Future,Await}
import ExecutionContext.Implicits.global
import reactivemongo.api.MongoDriver
import reactivemongo.core.nodeset.Authenticate
import reactivemongo.bson.BSONDocument
import com.girlrising._
import scala.util.{Try, Success, Failure}
import reactivemongo.bson._
class CRUDController[T](collection: String)
(implicit reader: BSONDocument => T,
writer: T => BSONDocument) extends MongoController{
//maybe add ability to increment, fold in pre-db call and post db-call events
def add(value: T) = {
AddToDatabase[T](collection,value)
}
def edit(value: T, id: String) = {
val selector = BSONDocument("_id" -> id)
EditInDatabase[T](collection,selector,value)
}
def remove(id: String) = {
RemoveFromDatabase(collection,BSONDocument("_id" -> id))
}
def all(map: Map[String,String]) = {
AllFromDatabase[T](collection,mapToFilter(map))
}
def one(id: String) = {
val selector = BSONDocument("_id" -> id)
OneFromDatabase[T](collection,selector)
}
def mapToFilter(map:Map[String,String]): BSONDocument = {
val filters = List(map.get("bucket").map(category => BSONDocument("bucket" -> category)),
map.get("search").map(search => BSONDocument(
"$or" -> BSONArray(
BSONDocument(
"name" -> BSONRegex(search, "i")
),
BSONDocument(
"url" -> BSONRegex(search, "i")
),
BSONDocument(
"description" -> BSONRegex(search, "i")
),
BSONDocument(
"owner" -> BSONRegex(search, "i")
)
)
))).flatten
filters.fold(BSONDocument())((acc,filter) => acc ++ filter)
}
}
package com.girlrising.services.generic
import reactivemongo.bson._
import spray.json._
import DefaultJsonProtocol._
import com.girlrising._
import com.girlrising.controllers.database._
import com.girlrising.models._
import spray.routing.HttpService
import com.girlrising.controllers.authentication._
/*
To follow conventions:
GET /value?params
POST /value
GET /value/id
PUT /value/id
DELETE /value/id
*/
//abstract class GenericServiceClass[T](val database:String)(implicit val BSONreader: BSONDocument => T, val BSONwriter: T => BSONDocument, val JSONWriter: JsonFormat[T]) extends GenericService[T]{}
class GenericService[T,U](database: String)(implicit a: akka.actor.ActorRefFactory,
BSONreader: BSONDocument => T,
BSONwriter: T => BSONDocument,
JSONWriter: JsonFormat[T],
idWriter: U => BSONValue,
JSONIdWriter: JsonFormat[U]) extends HttpService{
def actorRefFactory = a
val controller = new CRUDController[T](database)
import scala.concurrent.ExecutionContext.Implicits.global
import spray.httpx.SprayJsonSupport._
import com.girlrising.models.serializers._
val route =
parameterMap {params =>
onSuccess(checkSession(params.getOrElse("email","none"),params.getOrElse("token","none"))){ email =>
path(database){
get{
parameterMap {params =>
complete{
print("getting " + params)
controller.all(params)
}
}
} ~
post{
decompressRequest() {
entity(as[String]) { value => // transfer to newly spawned actor
detach() {
complete{
println("\n" + value.parseJson + "\n")
controller.add(value.parseJson.convertTo[T]).map(_.toJson.asJsObject)
}
}
}
}
} ~
options{
complete{
"option"
}
}
} ~
pathPrefix(database / Segment) { id =>
get{
complete{
println("the ID is "+id)
controller.one(id).map(_.toJson.asJsObject)
}
} ~
options{
complete{
"option"
}
} ~
put{
entity(as[String]) { model =>
complete{
println("the model is "+model)
controller.edit(model.parseJson.convertTo[T],id).map(_.toJson.asJsObject)
}
}
} ~
delete{
complete{
println("remove id"+id)
controller.remove(id).map(future => println(future))
controller.remove(id)
}
}
}
}
}
}
package com.girlrising.services
import akka.actor.Actor
import spray.routing._
import spray.http._
import MediaTypes._
import com.girlrising.services.categories._
import com.girlrising.services.resources._
import spray.routing.authentication._ //basic auth
import scala.concurrent._
import ExecutionContext.Implicits.global //for futures
// we don't implement our route structure directly in the service actor because
// we want to be able to test it independently, without having to spin up an actor
class MainServiceActor extends Actor
with CustomExceptions
with AuthenticationController
with AssetService
with StatService
with LoginService
with AssetCategoryService
with StatCategoryService
{
// the HttpService trait defines only one abstract member, which
// connects the services environment to the enclosing actor or test
def actorRefFactory = context
def categoryRoutes = assetCategoryRoute ~ statCategoryRoute
def resourceRoutes = assetRoute ~ statRoute ~ tallyRoute ~ storyRoute
def controllerRoutes = uploadRoute ~ redirectRoute ~ emailRoute ~ deleteRoute
// this actor only runs our route, but you could add
// other things here, like request stream processing
// or timeout handling
def receive = runRoute(categoryRoutes ~ resourceRoutes ~ controllerRoutes)
}
package com.girlrising.controllers.database
import scala.concurrent.{ExecutionContext, Future,Await}
import ExecutionContext.Implicits.global
import reactivemongo.api.MongoDriver
import reactivemongo.core.nodeset.Authenticate
import reactivemongo.bson.BSONDocument
import reactivemongo.api.commands.LastError
import reactivemongo.api.commands.WriteResult
import reactivemongo.core.errors._
import reactivemongo.api.MongoConnectionOptions
import reactivemongo.api._
import com.girlrising._
import com.girlrising.models._
import scala.concurrent.duration._
import scala.util.{Try, Success, Failure}
import com.girlrising.Boot._
trait MongoController{
// connecting to database moved to Boot.
//get one object from collection
//define enter/exit into collection
def OneFromDatabase[T](collection: String, selector: BSONDocument, orelse: Option[T] = None)(implicit converter: BSONDocument => T): Future[T] = {
println("SELECTOR:"+BSONDocument.pretty(selector)) //debugging
println("Look in collection "+collection+":"+db(collection).find(selector).cursor) //debugging
db(collection).find(selector).cursor.headOption.map(value => value match {
case Some(result) => converter(result)
case None => orelse match {
case Some(other) => other
case None => throw TBDException("No selection")
}
}
)
}
//get list of all objects from collection, if none, just empty list
def AllFromDatabase[T](collection: String, selector: BSONDocument)(implicit converter: BSONDocument => T): Future[List[T]] = {
val list = db(collection).find(selector).cursor[BSONDocument].collect[List]()
println("Got database from" + collection)
list.map(value => value.map(value => converter(value)))
}
def AddToDatabase[T](collection: String, value: T)(implicit converter: T => BSONDocument, convertBack: BSONDocument => T): Future[T] = {
// println("Adding "+ value +" to collection "+collection)
db(collection).insert(converter(value))
.map(_ => value) //onSuccess
.recover {
case e => {
if(e.getMessage.contains("duplicate key error index"))
throw DuplicateKeyException("duplicate!")
else throw TBDException("Unhandled Adding to Database Exception")
}
}
}
def RemoveFromDatabase(collection: String, selector: BSONDocument): Future[String] = {
db(collection).remove(selector).map(
_ => "success"
).recover {
case _ => "Not found"
}
}
def EditInDatabase[T](collection: String, selector: BSONDocument, value: T, BSONmod: Option[BSONDocument]=None)(implicit conversion: T => BSONDocument, convertBack: BSONDocument => T): Future[T] = {
BSONmod match {
case Some(modifier) => db(collection).update(selector,modifier).map(_ => value)
case None => db(collection).update(selector,BSONDocument("$set" -> conversion(value))).flatMap(
_ => OneFromDatabase[T](collection,selector,None)
)
}
}
}
package com.girlrising.models.resources.resources
import reactivemongo.bson._
import com.github.nscala_time.time.Imports._
case class StatVersion
(
id: Int,
timeCreated: DateTime,
notes: Option[String],
owner: String,
url: Option[String],
stat: Double,
validUntil: Option[DateTime]
) extends Version(id, timeCreated, notes, owner, url)
object StatVersion{
implicit object StatVersionReader extends BSONDocumentReader[StatVersion]{
def read(doc: BSONDocument) = {
StatVersion(
doc.getAs[Int]("id").get,
doc.getAs[BSONDateTime]("timeCreated").map(dt => new DateTime(dt.value)).get,
doc.getAs[String]("notes"),
doc.getAs[String]("owner").get,
doc.getAs[String]("url"),
doc.getAs[Double]("stat").get,
doc.getAs[BSONDateTime]("validUntil").map(dt => new DateTime(dt.value))
)
}
}
implicit object StatVersionWriter extends BSONDocumentWriter[StatVersion]{
def write(version: StatVersion) = {
BSONDocument(
"id" -> version.id,
"timeCreated" -> BSONDateTime(version.timeCreated.getMillis),
"notes" -> version.notes,
"owner" -> version.owner,
"url" -> version.url,
"stat" -> version.stat,
"validUntil" -> version.validUntil.map(time => BSONDateTime(time.getMillis))
)
}
}
}
package com.girlrising.models.resources.resources
import reactivemongo.bson.{BSONDocumentWriter, BSONDocumentReader, BSONDocument, BSONDateTime, BSONReader, BSONValue}
import com.github.nscala_time.time.Imports._
abstract class Version
(
id: Int,
timeCreated: DateTime,
// clicks: Int,
notes: Option[String],
owner: String,
url: Option[String]
)
package com.girlrising.models.resources.resources
import reactivemongo.bson._
import com.github.nscala_time.time.Imports._
case class VersionedResource[T <: Version]
(
id: String,
name: String,
description: Option[String] = None,
versions: List[T],
bucket: Option[String] = None,
owner: Option[String] = None,
permissions: Option[List[String]] = None,
currentVersion:Int,
watch: Option[List[String]] = None
)
object VersionedResource{
implicit def ResourceReader[T <: Version](implicit converter: BSONReader[ _ <: BSONValue, T]): BSONDocumentReader[VersionedResource[T]] = new BSONDocumentReader[VersionedResource[T]]
{
def read(doc: BSONDocument): VersionedResource[T] = {
VersionedResource(
doc.getAs[String]("_id").get,
doc.getAs[String]("name").get,
doc.getAs[String]("description"),
doc.getAs[List[T]]("versions").get,
doc.getAs[String]("bucket"),
doc.getAs[String]("owner"),
doc.getAs[List[String]]("permissions"),
doc.getAs[Int]("currentVersion").get,
doc.getAs[List[String]]("watch")
)
}
}
implicit def ResourceWriter[T <: Version](implicit converter: BSONWriter[T, _ <: BSONValue]): BSONDocumentWriter[VersionedResource[T]] = new BSONDocumentWriter[VersionedResource[T]]
{
def write(obj: VersionedResource[T]): BSONDocument = {
BSONDocument(
"_id" -> obj.id,
"name" -> obj.name,
"description" -> obj.description,
"versions" -> obj.versions,
"bucket" -> obj.bucket,
"owner" -> obj.owner,
"permissions" -> obj.permissions,
"currentVersion" -> obj.currentVersion,
"watch" -> obj.watch
)
}
}
}
@irenelfeng
Copy link
Author

For class design look at the files titled "package com.girlrising.models" (4). For database operations on objects of class Version, look at controller (2) and service scala files (2, including Boot.scala).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment