-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add simple rest service implementation
- Loading branch information
Showing
11 changed files
with
530 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import AssemblyKeys._ | ||
|
||
name := "rest" | ||
|
||
version := "1.0" | ||
|
||
scalaVersion := "2.10.2" | ||
|
||
libraryDependencies ++= Seq( | ||
"io.spray" % "spray-can" % "1.1-M8", | ||
"io.spray" % "spray-http" % "1.1-M8", | ||
"io.spray" % "spray-routing" % "1.1-M8", | ||
"net.liftweb" %% "lift-json" % "2.5.1", | ||
"com.typesafe.slick" %% "slick" % "1.0.1", | ||
"mysql" % "mysql-connector-java" % "5.1.25", | ||
"com.typesafe.akka" %% "akka-actor" % "2.1.4", | ||
"com.typesafe.akka" %% "akka-slf4j" % "2.1.4", | ||
"ch.qos.logback" % "logback-classic" % "1.0.13" | ||
) | ||
|
||
resolvers ++= Seq( | ||
"Spray repository" at "http://repo.spray.io", | ||
"Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" | ||
) | ||
|
||
assemblySettings |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
resolvers ++= Seq( | ||
"Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/", | ||
"Sonatype releases" at "https://oss.sonatype.org/content/repositories/releases/" | ||
) | ||
|
||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.0-SNAPSHOT") | ||
|
||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.0") |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
akka { | ||
loglevel = DEBUG | ||
event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] | ||
} | ||
|
||
service { | ||
host = "localhost" | ||
port = 8080 | ||
} | ||
|
||
db { | ||
host = "localhost" | ||
port = 3306 | ||
name = "rest" | ||
user = "root" | ||
password = null | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<configuration> | ||
|
||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> | ||
<target>System.out</target> | ||
<encoder> | ||
<pattern>%date{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{1} - %msg%n</pattern> | ||
</encoder> | ||
</appender> | ||
|
||
<logger name="akka" level="INFO"/> | ||
<logger name="scala.slick" level="INFO"/> | ||
|
||
<root level="DEBUG"> | ||
<appender-ref ref="CONSOLE"/> | ||
</root> | ||
|
||
</configuration> |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.sysgears.example.boot | ||
|
||
import akka.actor.{Props, ActorSystem} | ||
import akka.io.IO | ||
import com.sysgears.example.config.Configuration | ||
import com.sysgears.example.rest.RestServiceActor | ||
import spray.can.Http | ||
|
||
object Boot extends App with Configuration { | ||
|
||
// create an actor system for application | ||
implicit val system = ActorSystem("rest-service-example") | ||
|
||
// create and start rest service actor | ||
val restService = system.actorOf(Props[RestServiceActor], "rest-endpoint") | ||
|
||
// start HTTP server with rest service actor as a handler | ||
IO(Http) ! Http.Bind(restService, serviceHost, servicePort) | ||
} |
36 changes: 36 additions & 0 deletions
36
src/main/scala/com/sysgears/example/config/Configuration.scala
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package com.sysgears.example.config | ||
|
||
import com.typesafe.config.ConfigFactory | ||
import util.Try | ||
|
||
/** | ||
* Holds service configuration settings. | ||
*/ | ||
trait Configuration { | ||
|
||
/** | ||
* Application config object. | ||
*/ | ||
val config = ConfigFactory.load() | ||
|
||
/** Host name/address to start service on. */ | ||
lazy val serviceHost = Try(config.getString("service.host")).getOrElse("localhost") | ||
|
||
/** Port to start service on. */ | ||
lazy val servicePort = Try(config.getInt("service.port")).getOrElse(8080) | ||
|
||
/** Database host name/address. */ | ||
lazy val dbHost = Try(config.getString("db.host")).getOrElse("localhost") | ||
|
||
/** Database host port number. */ | ||
lazy val dbPort = Try(config.getInt("db.port")).getOrElse(3306) | ||
|
||
/** Service database name. */ | ||
lazy val dbName = Try(config.getString("db.name")).getOrElse("rest") | ||
|
||
/** User name used to access database. */ | ||
lazy val dbUser = Try(config.getString("db.user")).toOption.orNull | ||
|
||
/** Password for specified user and database. */ | ||
lazy val dbPassword = Try(config.getString("db.password")).toOption.orNull | ||
} |
163 changes: 163 additions & 0 deletions
163
src/main/scala/com/sysgears/example/dao/CustomerDAO.scala
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
package com.sysgears.example.dao | ||
|
||
import com.sysgears.example.config.Configuration | ||
import com.sysgears.example.domain._ | ||
import java.sql._ | ||
import scala.Some | ||
import scala.slick.driver.MySQLDriver.simple.Database.threadLocalSession | ||
import scala.slick.driver.MySQLDriver.simple._ | ||
import slick.jdbc.meta.MTable | ||
|
||
/** | ||
* Provides DAL for Customer entities for MySQL database. | ||
*/ | ||
class CustomerDAO extends Configuration { | ||
|
||
// init Database instance | ||
private val db = Database.forURL(url = "jdbc:mysql://%s:%d/%s".format(dbHost, dbPort, dbName), | ||
user = dbUser, password = dbPassword, driver = "com.mysql.jdbc.Driver") | ||
|
||
// create tables if not exist | ||
db.withSession { | ||
if (MTable.getTables("customers").list().isEmpty) { | ||
Customers.ddl.create | ||
} | ||
} | ||
|
||
/** | ||
* Saves customer entity into database. | ||
* | ||
* @param customer customer entity to | ||
* @return saved customer entity | ||
*/ | ||
def create(customer: Customer): Either[Failure, Customer] = { | ||
try { | ||
val id = db.withSession { | ||
Customers returning Customers.id insert customer | ||
} | ||
Right(customer.copy(id = Some(id))) | ||
} catch { | ||
case e: SQLException => | ||
Left(databaseError(e)) | ||
} | ||
} | ||
|
||
/** | ||
* Updates customer entity with specified one. | ||
* | ||
* @param id id of the customer to update. | ||
* @param customer updated customer entity | ||
* @return updated customer entity | ||
*/ | ||
def update(id: Long, customer: Customer): Either[Failure, Customer] = { | ||
try | ||
db.withSession { | ||
Customers.where(_.id === id) update customer.copy(id = Some(id)) match { | ||
case 0 => Left(notFoundError(id)) | ||
case _ => Right(customer.copy(id = Some(id))) | ||
} | ||
} | ||
catch { | ||
case e: SQLException => | ||
Left(databaseError(e)) | ||
} | ||
} | ||
|
||
/** | ||
* Deletes customer from database. | ||
* | ||
* @param id id of the customer to delete | ||
* @return deleted customer entity | ||
*/ | ||
def delete(id: Long): Either[Failure, Customer] = { | ||
try { | ||
db.withTransaction { | ||
val query = Customers.where(_.id === id) | ||
val customers = query.run.asInstanceOf[List[Customer]] | ||
customers.size match { | ||
case 0 => | ||
Left(notFoundError(id)) | ||
case _ => { | ||
query.delete | ||
Right(customers.head) | ||
} | ||
} | ||
} | ||
} catch { | ||
case e: SQLException => | ||
Left(databaseError(e)) | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves specific customer from database. | ||
* | ||
* @param id id of the customer to retrieve | ||
* @return customer entity with specified id | ||
*/ | ||
def get(id: Long): Either[Failure, Customer] = { | ||
try { | ||
db.withSession { | ||
Customers.findById(id).firstOption match { | ||
case Some(customer: Customer) => | ||
Right(customer) | ||
case _ => | ||
Left(notFoundError(id)) | ||
} | ||
} | ||
} catch { | ||
case e: SQLException => | ||
Left(databaseError(e)) | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves list of customers with specified parameters from database. | ||
* | ||
* @param params search parameters | ||
* @return list of customers that match given parameters | ||
*/ | ||
def search(params: CustomerSearchParameters): Either[Failure, List[Customer]] = { | ||
implicit val typeMapper = Customers.dateTypeMapper | ||
|
||
try { | ||
db.withSession { | ||
val query = for { | ||
customer <- Customers if { | ||
Seq( | ||
params.firstName.map(customer.firstName is _), | ||
params.lastName.map(customer.lastName is _), | ||
params.birthday.map(customer.birthday is _) | ||
).flatten match { | ||
case Nil => ConstColumn.TRUE | ||
case seq => seq.reduce(_ && _) | ||
} | ||
} | ||
} yield customer | ||
|
||
Right(query.run.toList) | ||
} | ||
} catch { | ||
case e: SQLException => | ||
Left(databaseError(e)) | ||
} | ||
} | ||
|
||
/** | ||
* Produce database error description. | ||
* | ||
* @param e SQL Exception | ||
* @return database error description | ||
*/ | ||
protected def databaseError(e: SQLException) = | ||
Failure("%d: %s".format(e.getErrorCode, e.getMessage), FailureType.DatabaseFailure) | ||
|
||
/** | ||
* Produce customer not found error description. | ||
* | ||
* @param customerId id of the customer | ||
* @return not found error description | ||
*/ | ||
protected def notFoundError(customerId: Long) = | ||
Failure("Customer with id=%d does not exist".format(customerId), FailureType.NotFound) | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.sysgears.example.domain | ||
|
||
import scala.slick.driver.MySQLDriver.simple._ | ||
|
||
/** | ||
* Customer entity. | ||
* | ||
* @param id unique id | ||
* @param firstName first name | ||
* @param lastName last name | ||
* @param birthday date of birth | ||
*/ | ||
case class Customer(id: Option[Long], firstName: String, lastName: String, birthday: Option[java.util.Date]) | ||
|
||
/** | ||
* Mapped customers table object. | ||
*/ | ||
object Customers extends Table[Customer]("customers") { | ||
|
||
def id = column[Long]("id", O.PrimaryKey, O.AutoInc) | ||
|
||
def firstName = column[String]("first_name") | ||
|
||
def lastName = column[String]("last_name") | ||
|
||
def birthday = column[java.util.Date]("birthday", O.Nullable) | ||
|
||
def * = id.? ~ firstName ~ lastName ~ birthday.? <>(Customer, Customer.unapply _) | ||
|
||
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Date]( | ||
{ | ||
ud => new java.sql.Date(ud.getTime) | ||
}, { | ||
sd => new java.util.Date(sd.getTime) | ||
}) | ||
|
||
val findById = for { | ||
id <- Parameters[Long] | ||
c <- this if c.id is id | ||
} yield c | ||
} |
14 changes: 14 additions & 0 deletions
14
src/main/scala/com/sysgears/example/domain/CustomerSearchParameters.scala
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.sysgears.example.domain | ||
|
||
import java.util.Date | ||
|
||
/** | ||
* Customers search parameters. | ||
* | ||
* @param firstName first name | ||
* @param lastName last name | ||
* @param birthday date of birth | ||
*/ | ||
case class CustomerSearchParameters(firstName: Option[String] = None, | ||
lastName: Option[String] = None, | ||
birthday: Option[Date] = None) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.sysgears.example.domain | ||
|
||
import spray.http.{StatusCodes, StatusCode} | ||
|
||
/** | ||
* Service failure description. | ||
* | ||
* @param message error message | ||
* @param errorType error type | ||
*/ | ||
case class Failure(message: String, errorType: FailureType.Value) { | ||
|
||
/** | ||
* Return corresponding HTTP status code for failure specified type. | ||
* | ||
* @return HTTP status code value | ||
*/ | ||
def getStatusCode: StatusCode = { | ||
FailureType.withName(this.errorType.toString) match { | ||
case FailureType.BadRequest => StatusCodes.BadRequest | ||
case FailureType.NotFound => StatusCodes.NotFound | ||
case FailureType.Duplicate => StatusCodes.Forbidden | ||
case FailureType.DatabaseFailure => StatusCodes.InternalServerError | ||
case _ => StatusCodes.InternalServerError | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Allowed failure types. | ||
*/ | ||
object FailureType extends Enumeration { | ||
type Failure = Value | ||
|
||
val BadRequest = Value("bad_request") | ||
val NotFound = Value("not_found") | ||
val Duplicate = Value("entity_exists") | ||
val DatabaseFailure = Value("database_error") | ||
val InternalError = Value("internal_error") | ||
} | ||
|
Oops, something went wrong.