├── .cache
├── .classpath
├── .gitignore
├── .project
├── .settings
└── org.scala-ide.sdt.core.prefs
├── LICENSE
├── README.md
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
└── src
├── main
├── resources
│ └── application.conf
└── scala
│ └── com
│ └── example
│ ├── Boot.scala
│ ├── dal
│ ├── CustomerDal.scala
│ └── MongoFactory.scala
│ ├── model
│ └── Customer.scala
│ └── service
│ ├── CustomRejectionHandler.scala
│ ├── CutomerService.scala
│ └── UserAuthentication.scala
└── test
└── scala
└── com
└── example
└── MyServiceSpec.scala
/.cache:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karthik20522/SprayLearning/29351797b6acf909babfe6bd898c46edddf289e9/.cache
--------------------------------------------------------------------------------
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.log
3 |
4 | # sbt specific
5 | dist/*
6 | target/
7 | lib_managed/
8 | src_managed/
9 | project/boot/
10 | project/plugins/project/
11 |
12 | # Scala-IDE specific
13 | .scala_dependencies
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 | default-24da5b
3 |
4 |
5 | org.scala-ide.sdt.core.scalabuilder
6 |
7 |
8 |
9 | org.scala-ide.sdt.core.scalanature
10 | org.eclipse.jdt.core.javanature
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.settings/org.scala-ide.sdt.core.prefs:
--------------------------------------------------------------------------------
1 | #Generated by sbteclipse
2 | #Fri Sep 06 14:32:12 EDT 2013
3 | scala.compiler.additionalParams=-encoding utf8
4 | deprecation=true
5 | unchecked=true
6 | scala.compiler.useProjectSettings=true
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Karthik Srinivasan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Guide to learning Spray.io web framework
2 | =============
3 |
4 | Bunch of tutorials to get started with Spray.io and some common features and implementations used in the development of web service.
5 |
6 | Following are the lessons that currently in the code:
7 |
8 | Lesson 1 - Basic setup - installation, project folder setup
9 |
http://kufli.blogspot.com/2013/08/sprayio-rest-web-api-basic-setup.html
10 |
Lesson 2 - Controllers, actions, multiple controllers
11 |
http://kufli.blogspot.com/2013/08/sprayio-rest-service-controller-actions.html
12 |
Lesson 3 - json response, serialization, deserialization
13 |
http://kufli.blogspot.com/2013/08/sprayio-rest-service-json-serialization.html
14 |
Lesson 4 - mongodb database
15 |
http://kufli.blogspot.com/2013/08/sprayio-rest-service-mongodb-lesson-4.html
16 |
Lesson 5 - authentication, directives
17 |
http://kufli.blogspot.com/2013/08/sprayio-rest-service-authentication.html
18 |
Lesson 6 - versioning API
19 |
http://kufli.blogspot.com/2013/08/sprayio-rest-service-api-versioning.html
20 |
Lesson 7 - Exception, Rejection and Timeout Handling
21 |
http://kufli.blogspot.com/2013/08/sprayio-rest-service-exception.html
22 |
Lesson 8 - http error codes
23 |
http://kufli.blogspot.com/2013/09/sprayio-rest-service-http-error-codes.html
24 |
25 | Pending Lessons:
26 |
27 | Lesson 9 - cookies, cross domain requests, zipping, config file reading, caching
28 | Lesson 10 - swagger UI
29 | Lesson 11 - http dispatch
30 | Lesson 12 - akka
31 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | organization := "com.example"
2 |
3 | version := "0.1"
4 |
5 | scalaVersion := "2.10.2"
6 |
7 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
8 |
9 | resolvers ++= Seq(
10 | "spray repo" at "http://repo.spray.io/"
11 | )
12 |
13 | libraryDependencies ++= Seq(
14 | "io.spray" % "spray-can" % "1.2-M8",
15 | "io.spray" % "spray-routing" % "1.2-M8",
16 | "io.spray" % "spray-testkit" % "1.2-M8" % "test",
17 | "com.typesafe.akka" %% "akka-actor" % "2.2.0-RC1",
18 | "com.typesafe.akka" %% "akka-testkit" % "2.2.0-RC1" % "test",
19 | "org.specs2" %% "specs2" % "1.14" % "test",
20 | "org.json4s" %% "json4s-native" % "3.2.4",
21 | "org.mongodb" %% "casbah" % "2.6.2",
22 | "com.typesafe" %% "scalalogging-slf4j" % "1.0.1",
23 | "org.slf4j" % "slf4j-api" % "1.7.1",
24 | "org.slf4j" % "log4j-over-slf4j" % "1.7.1",
25 | "ch.qos.logback" % "logback-classic" % "1.0.3"
26 | )
27 |
28 | seq(Revolver.settings: _*)
29 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.12.3
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.6.2")
2 |
3 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.0.1")
4 |
5 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
--------------------------------------------------------------------------------
/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | akka {
2 | loglevel = INFO
3 | }
4 |
5 | spray.can.server {
6 | request-timeout = 10s
7 | }
8 |
9 | security{
10 | username = "karthik"
11 | password = "kufli"
12 | }
--------------------------------------------------------------------------------
/src/main/scala/com/example/Boot.scala:
--------------------------------------------------------------------------------
1 | package com.example
2 |
3 | import akka.actor.{ ActorSystem, Props }
4 | import akka.io.IO
5 | import spray.can.Http
6 | import com.example.service.CustomerServiceActor
7 |
8 | object Boot extends App {
9 |
10 | // we need an ActorSystem to host our application in
11 | implicit val system = ActorSystem("on-spray-can")
12 |
13 | // create and start our service actor
14 | val service = system.actorOf(Props[CustomerServiceActor], "customer-service")
15 |
16 | // start a new HTTP server on port 8080 with our service actor as the handler
17 | IO(Http) ! Http.Bind(service, interface = "localhost", port = 8080)
18 | }
--------------------------------------------------------------------------------
/src/main/scala/com/example/dal/CustomerDal.scala:
--------------------------------------------------------------------------------
1 | package com.example.dal
2 | import com.mongodb.casbah.Imports._
3 | import com.example.model.Customer
4 |
5 | class CustomerDal {
6 |
7 | val conn = MongoFactory.getConnection
8 |
9 | def saveCustomer(customer: Customer) = {
10 | val customerObj = buildMongoDbObject(customer)
11 | val result = MongoFactory.getCollection(conn).save(customerObj)
12 | val id = customerObj.getAs[org.bson.types.ObjectId]("_id").get
13 | println(id)
14 | id
15 | }
16 |
17 | def findCustomer(id: String) = {
18 | var q = MongoDBObject("_id" -> new org.bson.types.ObjectId(id))
19 | val collection = MongoFactory.getCollection(conn)
20 | val result = collection findOne q
21 |
22 | val customerResult = result.get
23 |
24 | val customer = Customer(firstName = customerResult.as[String]("firstName"),
25 | lastName = customerResult.as[String]("lastName"),
26 | _id = Some(customerResult.as[org.bson.types.ObjectId]("_id").toString()),
27 | phoneNumber = Some(customerResult.as[String]("phoneNumber")),
28 | address = Some(customerResult.as[String]("address")),
29 | city = Some(customerResult.as[String]("city")),
30 | country = Some(customerResult.as[String]("country")),
31 | zipcode = Some(customerResult.as[String]("zipcode")))
32 |
33 | customer //return the customer object
34 | }
35 |
36 | //Convert our Customer object into a BSON format that MongoDb can store.
37 | private def buildMongoDbObject(customer: Customer): MongoDBObject = {
38 | val builder = MongoDBObject.newBuilder
39 | builder += "firstName" -> customer.firstName
40 | builder += "lastName" -> customer.lastName
41 | builder += "phoneNumber" -> customer.phoneNumber.getOrElse("")
42 | builder += "address" -> customer.address.getOrElse("")
43 | builder += "city" -> customer.city.get
44 | builder += "country" -> customer.country.get
45 | builder += "zipcode" -> customer.zipcode.getOrElse("")
46 | builder.result
47 | }
48 | }
--------------------------------------------------------------------------------
/src/main/scala/com/example/dal/MongoFactory.scala:
--------------------------------------------------------------------------------
1 | package com.example.dal
2 |
3 | import com.mongodb.casbah.MongoCollection
4 | import com.mongodb.casbah.MongoConnection
5 |
6 | object MongoFactory {
7 |
8 | private val SERVER = "localhost"
9 | private val PORT = 27017
10 | private val DATABASE = "customerDb"
11 | private val COLLECTION = "customer"
12 |
13 | def getConnection: MongoConnection = return MongoConnection(SERVER)
14 | def getCollection(conn: MongoConnection): MongoCollection = return conn(DATABASE)(COLLECTION)
15 | def closeConnection(conn: MongoConnection) { conn.close }
16 |
17 | }
--------------------------------------------------------------------------------
/src/main/scala/com/example/model/Customer.scala:
--------------------------------------------------------------------------------
1 | package com.example.model
2 |
3 | case class Customer(firstName: String,
4 | lastName: String,
5 | _id: Option[String] = None,
6 | phoneNumber: Option[String] = None,
7 | address: Option[String] = None,
8 | city: Option[String] = Some("New York"),
9 | country: Option[String] = Some("USA"),
10 | zipcode: Option[String] = None) {
11 | }
--------------------------------------------------------------------------------
/src/main/scala/com/example/service/CustomRejectionHandler.scala:
--------------------------------------------------------------------------------
1 | package com.example.service
2 | import spray.routing._
3 | import spray.http._
4 | import StatusCodes._
5 |
6 | trait CustomRejectionHandler extends HttpService {
7 | implicit val myRejectionHandler = RejectionHandler {
8 | case AuthenticationFailedRejection(credentials) :: _ => complete(Unauthorized, "Credential fail " + credentials)
9 | case _ => complete(BadRequest, "Something went wrong here")
10 | }
11 | }
--------------------------------------------------------------------------------
/src/main/scala/com/example/service/CutomerService.scala:
--------------------------------------------------------------------------------
1 | package com.example.service
2 |
3 | import akka.actor.Actor
4 | import spray.routing._
5 | import spray.http._
6 | import spray.http.MediaTypes._
7 | import spray.routing.Directive.pimpApply
8 | import spray.routing.directives.CompletionMagnet.fromObject
9 | import com.example.model.Customer
10 | import spray.httpx.Json4sSupport
11 | import org.json4s.Formats
12 | import org.json4s.DefaultFormats
13 | import com.example.model.Customer
14 | import org.json4s.JsonAST.JObject
15 | import com.example.dal.CustomerDal
16 | import scala.concurrent.ExecutionContext.Implicits.global
17 | import shapeless._
18 | import spray.routing.directives.BasicDirectives._
19 | import spray.util.LoggingContext
20 | import org.json4s.`package`.MappingException
21 |
22 | case class ReponseError(errorCode: String, errorMessage: String) {}
23 |
24 | // we don't implement our route structure directly in the service actor because
25 | // we want to be able to test it independently, without having to spin up an actor
26 | class CustomerServiceActor extends Actor with CustomerService with AjaxService with CustomRejectionHandler {
27 |
28 | implicit def json4sFormats: Formats = DefaultFormats
29 | // the HttpService trait defines only one abstract member, which
30 | // connects the services environment to the enclosing actor or test
31 | def actorRefFactory = context
32 |
33 | // this actor only runs our route, but you could add
34 | // other things here, like request stream processing
35 | // or timeout handling
36 | def receive = handleTimeouts orElse runRoute(handleRejections(myRejectionHandler)(handleExceptions(myExceptionHandler)(
37 | customerRoutes ~ ajaxRoutes)))
38 |
39 | def handleTimeouts: Receive = {
40 | case Timedout(x: HttpRequest) =>
41 | sender ! HttpResponse(StatusCodes.InternalServerError, "Something is taking way too long.")
42 | }
43 |
44 | implicit def myExceptionHandler(implicit log: LoggingContext) =
45 | ExceptionHandler.apply {
46 | case m: MappingException => {
47 | respondWithMediaType(`application/json`) {
48 | val errorMsg = ReponseError("MalformedBody", m.getMessage)
49 | ctx => ctx.complete(415, errorMsg)
50 | }
51 | }
52 | case e: SomeCustomException => ctx => {
53 | val errorMsg = ReponseError("BadRequest", e.getMessage)
54 | ctx.complete(400, errorMsg)
55 | }
56 | case e: Exception => ctx => {
57 | val errorMsg = ReponseError("InternalServerError", e.getMessage)
58 | ctx.complete(500, errorMsg)
59 | }
60 | }
61 | }
62 |
63 | class SomeCustomException(msg: String) extends RuntimeException(msg)
64 |
65 | //http://kufli.blogspot.com/2013/08/sprayio-rest-service-api-versioning.html
66 | trait VersionDirectives {
67 | def versioning: Directive[String :: HNil] =
68 | extract { ctx =>
69 | val header = ctx.request.headers.find(_.name == "X-API-Version")
70 | header match {
71 | case Some(head) => head.value
72 | case _ => "1" //default to 1
73 | }
74 | }
75 | }
76 |
77 | trait AjaxService extends HttpService {
78 | val ajaxRoutes =
79 | path("search" / Segment) { query =>
80 | get {
81 | complete {
82 | //Free text search implementation
83 | s"success ${query}"
84 | }
85 | }
86 | }
87 | }
88 |
89 | // this trait defines our service behavior independently from the service actor
90 | trait CustomerService extends HttpService with Json4sSupport with UserAuthentication {
91 |
92 | //http://kufli.blogspot.com/2013/08/sprayio-rest-service-api-versioning.html
93 | val Version = PathMatcher("""v([0-9]+)""".r)
94 | .flatMap {
95 | case vString :: HNil => {
96 | try Some(Integer.parseInt(vString) :: HNil)
97 | catch {
98 | case _: NumberFormatException => Some(1 :: HNil) //default to version 1
99 | }
100 | }
101 | }
102 |
103 | val customerRoutes =
104 | path("someException") {
105 | get {
106 | complete {
107 | throw new SomeCustomException("This is a custom Exception")
108 | }
109 | }
110 | } ~
111 | path("addCustomer") {
112 | post {
113 | authenticate(authenticateUser) { user =>
114 | entity(as[JObject]) { customerObj =>
115 | complete {
116 | val customer = customerObj.extract[Customer]
117 | val customerDal = new CustomerDal
118 | val id = customerDal.saveCustomer(customer)
119 | id.toString()
120 | }
121 | }
122 | }
123 | }
124 | } ~
125 | path("getCustomer" / Segment) { customerId =>
126 | get {
127 | authenticate(authenticateUser) { user =>
128 | {
129 | complete {
130 | //get customer from db using customerId as Key
131 | val customerDal = new CustomerDal
132 | val customer = customerDal.findCustomer(customerId)
133 | customer
134 | }
135 | }
136 | }
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/scala/com/example/service/UserAuthentication.scala:
--------------------------------------------------------------------------------
1 | package com.example.service
2 |
3 | import spray.routing.authentication.Authentication
4 | import spray.routing.authentication.ContextAuthenticator
5 | import scala.concurrent.Future
6 | import com.typesafe.config.ConfigFactory
7 | import scala.concurrent.ExecutionContext.Implicits.global
8 | import spray.routing.AuthenticationFailedRejection
9 |
10 | case class User(userName: String, token: String) {}
11 |
12 | trait UserAuthentication {
13 |
14 | val conf = ConfigFactory.load()
15 | lazy val configusername = conf.getString("security.username")
16 | lazy val configpassword = conf.getString("security.password")
17 |
18 | def authenticateUser: ContextAuthenticator[User] = {
19 | ctx =>
20 | {
21 | //get username and password from the url
22 | val usr = ctx.request.uri.query.get("usr").get
23 | val pwd = ctx.request.uri.query.get("pwd").get
24 |
25 | doAuth(usr, pwd)
26 | }
27 | }
28 |
29 | private def doAuth(userName: String, password: String): Future[Authentication[User]] = {
30 | //here you can call database or a web service to authenticate the user
31 | Future {
32 | Either.cond(password == configpassword && userName == configusername,
33 | User(userName = userName, token = java.util.UUID.randomUUID.toString),
34 | AuthenticationFailedRejection("CredentialsRejected"))
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/test/scala/com/example/MyServiceSpec.scala:
--------------------------------------------------------------------------------
1 | /*package com.example
2 |
3 | import org.specs2.mutable.Specification
4 | import spray.testkit.Specs2RouteTest
5 | import spray.http._
6 | import StatusCodes._
7 |
8 | class MyServiceSpec extends Specification with Specs2RouteTest with MyService {
9 | def actorRefFactory = system
10 |
11 | "MyService" should {
12 |
13 | "return a greeting for GET requests to the root path" in {
14 | Get() ~> myRoute ~> check {
15 | entityAs[String] must contain("Say hello")
16 | }
17 | }
18 |
19 | "leave GET requests to other paths unhandled" in {
20 | Get("/kermit") ~> myRoute ~> check {
21 | handled must beFalse
22 | }
23 | }
24 |
25 | "return a MethodNotAllowed error for PUT requests to the root path" in {
26 | Put() ~> sealRoute(myRoute) ~> check {
27 | status === MethodNotAllowed
28 | entityAs[String] === "HTTP method not allowed, supported methods: GET"
29 | }
30 | }
31 | }
32 | }*/
--------------------------------------------------------------------------------