SlideShare a Scribd company logo
1 of 128
REST Test
Exploring DSL design in Scala
Who am I?
Iain Hull
Email: iain.hull@workday.com
Twitter: @IainHull
Web: http://IainHull.github.io
Workday
HTTP Clients … What’s the problem?
http://www.hdwpapers.com/pretty_face_of_boxer_dog_wallpaper_hd-wallpapers.html
Simple HTTP Client
case class Request(
method: Method,
url: URI,
headers: Map[String, List[String]],
body: Option[String])
case class Response(
statusCode: Int,
headers: Map[String, List[String]],
body: Option[String])
type HttpClient = Request => Response
Simple HTTP Client
case class Request(
method: Method,
url: URI,
headers: Map[String, List[String]],
body: Option[String])
case class Response(
statusCode: Int,
headers: Map[String, List[String]],
body: Option[String])
type HttpClient = Request => Response
Simple HTTP Client
case class Request(
method: Method,
url: URI,
headers: Map[String, List[String]],
body: Option[String])
case class Response(
statusCode: Int,
headers: Map[String, List[String]],
body: Option[String])
type HttpClient = Request => Response
Simple HTTP Client
case class Request(
method: Method,
url: URI,
headers: Map[String, List[String]],
body: Option[String])
case class Response(
statusCode: Int,
headers: Map[String, List[String]],
body: Option[String])
type HttpClient = Request => Response
Using the Client
val request = Request(
GET,
new URI("http://api.rest.org/person",
Map(), None))
val response = httpClient(request)
assert(response.code1 === Status.OK)
assert(jsonAsList[Person](response.body.get) ===
EmptyList)
Using the Client
val request = Request(
GET,
new URI("http://api.rest.org/person",
Map(), None))
val response = httpClient(request)
assert(response.code === Status.OK)
assert(jsonAsList[Person](response.body.get) ===
EmptyList)
Using the Client
val request = Request(
GET,
new URI("http://api.rest.org/person",
Map(), None))
val response = httpClient(request)
assert(response.code === Status.OK)
assert(jsonAsList[Person](response.body.get) ===
EmptyList)
Using the Client
val request = Request(
GET,
new URI("http://api.rest.org/person",
Map(), None))
val response = httpClient(request)
assert(response.code === Status.OK)
assert(jsonAsList[Person](response.body.get) ===
EmptyList)
Sample API
GET /person List all the persons
POST /person Create a new person
GET /person/{id} Retrieve a person
DELETE /person/{id} Delete a person
Retrieve the list
GET /person Verify list is empty
Create a new person
GET /person Verify list is empty
POST /person Verify it succeeds and returns an id
Retrieve the person
GET /person Verify list is empty
POST /person Verify it succeeds and returns an id
GET /person/{id} Verify details are the same
Retrieve the list
GET /person Verify list is empty
POST /person Verify it succeeds and returns an id
GET /person/{id} Verify details are the same
GET /person Verify list contains the person
Delete the person
GET /person Verify list is empty
POST /person Verify it succeeds and returns an id
GET /person/{id} Verify details are the same
GET /person Verify list contains the person
DELETE /person/{id} Verify the status code
Retrieve the list
GET /person Verify list is empty
POST /person Verify it succeeds and returns an id
GET /person/{id} Verify details are the same
GET /person Verify list contains the person
DELETE /person/{id} Verify the status code
GET /person Verify list is empty
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r1.statusCode === Status.OK)
r1.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
assert(r2.statusCode === Status.Created)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r3.statusCode === Status.OK)
r3.body match {
Some(body) =>
assert(jsonAs[Person](body) === Jason)
None => fail("Expected a body"))
}
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r4.statusCode === Status.OK)
r4.body match {
Some(body) =>
assert(jsonAsList[Person](body) === Seq(Jason))
None => fail("Expected a body"))
}
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r5.statusCode === Status.OK)
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r6.statusCode === Status.OK)
r6.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
Agh!!
I can’t read
this
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r1.statusCode === Status.OK)
r1.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
assert(r2.statusCode === Status.Created)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r3.statusCode === Status.OK)
r3.body match {
Some(body) =>
assert(jsonAs[Person](body) === Jason)
None => fail("Expected a body"))
}
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r4.statusCode === Status.OK)
r4.body match {
Some(body) =>
assert(jsonAsList[Person](body) === Seq(Jason))
None => fail("Expected a body"))
}
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r5.statusCode === Status.OK)
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r6.statusCode === Status.OK)
r6.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
New Code
Modifying
Code
Reading Code
http://www.codinghorror.com/blog/2006/09/when-understanding-means-rewriting.html
http://www.flickr.com/photos/paulwicks/1753350803/sizes/o/
Boilerplate
http://www.flickr.com/photos/paulwicks/1753350803/sizes/o/
Boilerplate
http://www.flickr.com/photos/whoshotya/1014730135/
Domain Specific
Language
http://www.flickr.com/photos/whoshotya/1014730135/
Domain Specific
Language
How can it help?
http://www.flickr.com/photos/whoshotya/1014730135/
Domain Specific
Language
How can it help?
How do I write
one?
http://www.flickr.com/photos/whoshotya/1014730135/
Domain Specific
Language
How can it help?
How do I write
one?
Scala
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
Applying the builder
val personJson = """{ "name": "Jason" }"""
val rb = RequestBuilder()
.withUrl("http://api.rest.org/person/")
val r1 = httpClient(rb.withMethod(GET).toRequest)
val r2 = httpClient(rb.withMethod(POST)
.withBody(personJson).toRequest)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(rb.withMethod(GET).addPath(id)
.toRequest)
val r4 = httpClient(rb.withMethod(GET).toRequest)
val r5 = httpClient(rb.withMethod(DELETE).addPath(id)
.toRequest)
val r6 = httpClient(rb.withMethod(GET).toRequest)
Applying the builder
val personJson = """{ "name": "Jason" }"""
val rb = RequestBuilder()
.withUrl("http://api.rest.org/person/")
val r1 = httpClient(rb.withMethod(GET).toRequest)
val r2 = httpClient(rb.withMethod(POST)
.withBody(personJson).toRequest)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(rb.withMethod(GET).addPath(id)
.toRequest)
val r4 = httpClient(rb.withMethod(GET).toRequest)
val r5 = httpClient(rb.withMethod(DELETE).addPath(id)
.toRequest)
val r6 = httpClient(rb.withMethod(GET).toRequest)
Applying the builder
val personJson = """{ "name": "Jason" }"""
val rb = RequestBuilder()
.withUrl("http://api.rest.org/person/")
val r1 = httpClient(rb.withMethod(GET).toRequest)
val r2 = httpClient(rb.withMethod(POST)
.withBody(personJson).toRequest)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(rb.withMethod(GET).addPath(id)
.toRequest)
val r4 = httpClient(rb.withMethod(GET).toRequest)
val r5 = httpClient(rb.withMethod(DELETE).addPath(id)
.toRequest)
val r6 = httpClient(rb.withMethod(GET).toRequest)
Applying the builder
val personJson = """{ "name": "Jason" }"""
val rb = RequestBuilder()
.withUrl("http://api.rest.org/person/")
val r1 = httpClient(rb.withMethod(GET).toRequest)
val r2 = httpClient(rb.withMethod(POST)
.withBody(personJson).toRequest)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(rb.withMethod(GET).addPath(id)
.toRequest)
val r4 = httpClient(rb.withMethod(GET).toRequest)
val r5 = httpClient(rb.withMethod(DELETE).addPath(id)
.toRequest)
val r6 = httpClient(rb.withMethod(GET).toRequest)
Implicit Conversion
val personJson = """{ "name": "Jason" }"""
val rb = RequestBuilder()
.withUrl("http://api.rest.org/person/")
val r1 = httpClient(rb.withMethod(GET).toRequest)
val r2 = httpClient(rb.withMethod(POST)
.withBody(personJson).toRequest)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(rb.withMethod(GET).addPath(id)
.toRequest)
val r4 = httpClient(rb.withMethod(GET).toRequest)
val r5 = httpClient(rb.withMethod(DELETE).addPath(id)
.toRequest)
val r6 = httpClient(rb.withMethod(GET).toRequest)
implicit def toRequest(b: RequestBuilder): Request =
b.toRequest
Implicit Conversion
val personJson = """{ "name": "Jason" }"""
val rb = RequestBuilder()
.withUrl("http://api.rest.org/person/"
val r1 = httpClient(rb.withMethod(GET))
val r2 = httpClient(rb.withMethod(POST)
.withBody(personJson))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(rb.withMethod(GET).addPath(id))
val r4 = httpClient(rb.withMethod(GET))
val r5 = httpClient(rb.withMethod(DELETE).addPath(id))
val r6 = httpClient(rb.withMethod(GET))
implicit def toRequest(b: RequestBuilder): Request =
b.toRequest
Implicit Conversion
val personJson = """{ "name": "Jason" }"""
val rb = RequestBuilder()
.withUrl("http://api.rest.org/person/")
val r1 = httpClient(rb.withMethod(GET))
val r2 = httpClient(rb.withMethod(POST)
.withBody(personJson))
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(rb.withMethod(GET).addPath(id))
val r4 = httpClient(rb.withMethod(GET))
val r5 = httpClient(rb.withMethod(DELETE).addPath(id))
val r6 = httpClient(rb.withMethod(GET))
Nice, but show
me some DSL
Narrate your code
• Get from url http://api.rest.org/person/
• Post personJson to url http://api.rest.org/person/
• Get from url http://api.rest.org/person/personId
• Delete from url http://api.rest.org/person/personId
Narrate your code
• Get from url http://api.rest.org/person/
• Post personJson to url http://api.rest.org/person/
• Get from url http://api.rest.org/person/personId
• Delete from url http://api.rest.org/person/personId
Bootstapping the DSL
httpClient(RequestBuilder()
.withMethod(GET)
.withUrl("http://api.rest.org/person/"))
Bootstapping the DSL
httpClient(RequestBuilder()
.withMethod(GET)
.withUrl("http://api.rest.org/person/"))
Bootstapping the DSL
httpClient(RequestBuilder()
.withMethod(GET)
.withUrl("http://api.rest.org/person/"))
implicit def methodToRequestBuilder(method: Method):
RequestBuilder = RequestBuilder().withMethod(method)
Bootstapping the DSL
httpClient(GET.withUrl("http://api.rest.org/person/"))
implicit def methodToRequestBuilder(method: Method):
RequestBuilder = RequestBuilder().withMethod(method)
Bootstapping the DSL
httpClient(GET.withUrl("http://api.rest.org/person/"))
implicit def methodToRequestBuilder(method: Method):
RequestBuilder = RequestBuilder().withMethod(method)
methodToRequestBuilder(GET)
.withUrl("http://api.rest.org/person/")
Bootstapping the DSL
httpClient(GET.withUrl("http://api.rest.org/person/"))
implicit def methodToRequestBuilder(method: Method):
RequestBuilder = RequestBuilder().withMethod(method)
Bootstapping the DSL
httpClient(GET withUrl "http://api.rest.org/person/")
Initial DSL
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(
GET withUrl "http://api.rest.org/person/")
val r2 = httpClient(
POST withUrl "http://api.rest.org/person/"
withBody personJson)
val id = r2.headers.get("X-Person-Id").get.head
val r3 = httpClient(
GET withUrl "http://api.rest.org/person/" addPath id)
val r4 = httpClient(
GET withUrl "http://api.rest.org/person/")
val r5 = httpClient(
DELETE withUrl "http://api.rest.org/person/" addPath id)
val r6 = httpClient(
GET withUrl "http://api.rest.org/person/")
Initial DSL
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(
GET withUrl "http://api.rest.org/person/")
val r2 = httpClient(
POST withUrl "http://api.rest.org/person/"
withBody personJson)
val id = r2.headers.get("X-Person-Id").get.head
val r3 = httpClient(
GET withUrl "http://api.rest.org/person/" addPath id)
val r4 = httpClient(
GET withUrl "http://api.rest.org/person/")
val r5 = httpClient(
DELETE withUrl "http://api.rest.org/person/" addPath id)
val r6 = httpClient(
GET withUrl "http://api.rest.org/person/")
Initial DSL
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(GET withUrl "http://api.rest.org/person/")
val r2 = httpClient(POST withUrl "http://api.rest.org/person/"
withBody personJson)
val id = r2.headers.get("X-Person-Id").get.head
val r3 = httpClient(GET withUrl "http://api.rest.org/person/" addPath id)
val r4 = httpClient(GET withUrl "http://api.rest.org/person/")
val r5 = httpClient(DELETE withUrl "http://api.rest.org/person/"
addPath id)
implicit class RichRequestBuilder(builder: RequestBuilder) {
def execute()(implicit httpClient: HttpClient): Response = {
httpClient(builder)
}
}
Initial DSL
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
val r1 = GET withUrl "http://api.rest.org/person/" execute ()
val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson
execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()
val r4 = GET withUrl "http://api.rest.org/person/" execute ()
val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()
val r6 = GET withUrl "http://api.rest.org/person/" execute ()
implicit class RichRequestBuilder(builder: RequestBuilder) {
def execute()(implicit httpClient: HttpClient): Response = {
httpClient(builder)
}
}
Initial DSL
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
val r1 = GET withUrl "http://api.rest.org/person/" execute ()
val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson
execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()
val r4 = GET withUrl "http://api.rest.org/person/" execute ()
val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()
val r6 = GET withUrl "http://api.rest.org/person/" execute ()
implicit class RichRequestBuilder(builder: RequestBuilder) {
def execute()(implicit httpClient: HttpClient): Response = {
httpClient(builder)
}
}
Initial DSL
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
val r1 = GET withUrl "http://api.rest.org/person/" execute ()
val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson
execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()
val r4 = GET withUrl "http://api.rest.org/person/" execute ()
val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()
val r6 = GET withUrl "http://api.rest.org/person/" execute ()
implicit class RichRequestBuilder(builder: RequestBuilder) {
def execute()(implicit httpClient: HttpClient): Response = {
httpClient(builder)
}
}
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }""”
val r1 = GET withUrl "http://api.rest.org/person/" execute ()
val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson
execute ()
val id = r2.headers.get("X-Person-Id”)
val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()
val r4 = GET withUrl "http://api.rest.org/person/" execute ()
val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()
val r6 = GET withUrl "http://api.rest.org/person/" execute ()
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
val r1 = GET withUrl "http://api.rest.org/person/" execute ()
val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson
execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()
val r4 = GET withUrl "http://api.rest.org/person/" execute ()
val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()
val r6 = GET withUrl "http://api.rest.org/person/" execute ()
implicit def methodToRequestBuilder(m: Method): RequestBuilder
= RequestBuilder().withMethod(m)
Common Configuration
implicit val httpClient = ...
implicit val rb = RequestBuilder() withUrl "http://api.rest.org/person/"
val personJson = """{ "name": "Jason" }"""
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
implicit def methodToRequestBuilder(m: Method)
(implicit builder: RequestBuilder): RequestBuilder
= builder.withMethod(m)
Common Configuration
implicit val httpClient = ...
implicit val rb = RequestBuilder() withUrl "http://api.rest.org/person/"
val personJson = """{ "name": "Jason" }"""
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
using(_ withUrl "http://api.rest.org/person") { implicit rb =>
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
}
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
using(_ withUrl "http://api.rest.org/person") { implicit rb =>
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
}
def using(config: RequestBuilder => RequestBuilder)
(process: RequestBuilder => Unit)
(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
using(_ withUrl "http://api.rest.org/person") { implicit rb =>
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
}
def using(config: RequestBuilder => RequestBuilder)
(process: RequestBuilder => Unit)
(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
using(_ withUrl "http://api.rest.org/person") { implicit rb =>
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
}
def using(config: RequestBuilder => RequestBuilder)
(process: RequestBuilder => Unit)
(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
using(_ withUrl "http://api.rest.org/person") { implicit rb =>
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
}
def using(config: RequestBuilder => RequestBuilder)
(process: RequestBuilder => Unit)
(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}
Common Configuration
object RequestBuilder {
implicit val emptyBuilder = RequestBuilder(
None, None, Seq(), Seq(), None)
}
Common Configuration
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
using(_ withUrl "http://api.rest.org/person") { implicit rb =>
val r1 = GET execute ()
val r2 = POST withBody personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET addPath id execute ()
val r4 = GET execute ()
val r5 = DELETE addPath id execute ()
val r6 = GET execute ()
}
A spoonful of sugar
A Spoonful of sugar
implicit class RichRequestBuilder(b: RequestBuilder) {
def url(u: String) = b.withUrl(u)
def body(b: String) = b.withBody(b)
def /(p: Any) = b.addPath(p.toString)
def :?(params: (Symbol, Any)*) =
b.addQuery(params map (
p => (p._1.name, p._2.toString)): _*)
// ...
}
A Spoonful of sugar
implicit class RichRequestBuilder(b: RequestBuilder) {
def url(u: String) = b.withUrl(u)
def body(b: String) = b.withBody(b)
def /(p: Any) = b.addPath(p.toString)
def :?(params: (Symbol, Any)*) =
b.addQuery(params map (
p => (p._1.name, p._2.toString)): _*)
// ...
}
A Spoonful of sugar
implicit class RichRequestBuilder(b: RequestBuilder) {
def url(u: String) = b.withUrl(u)
def body(b: String) = b.withBody(b)
def /(p: Any) = b.addPath(p.toString)
def :?(params: (Symbol, Any)*) =
b.addQuery(params map (
p => (p._1.name, p._2.toString)): _*)
// ...
}
A Spoonful of sugar
implicit class RichRequestBuilder(b: RequestBuilder) {
def url(u: String) = b.withUrl(u)
def body(b: String) = b.withBody(b)
def /(p: Any) = b.addPath(p.toString)
def :?(params: (Symbol, Any)*) =
b.addQuery(params map (
p => (p._1.name, p._2.toString)): _*)
// ...
}
A Spoonful of sugar
implicit val httpClient = ...
val personJson = """{ "name": "Jason" }"""
using(_ url "http://api.rest.org") { implicit rb =>
val r1 = GET / "person" execute ()
val r2 = POST / "person" body personJson execute ()
val id = r2.headers.get("X-Person-Id").get.head
val r3 = GET / "person" / id execute ()
val r4 = GET / "person" execute ()
val r5 = DELETE / "person" / id execute ()
val r6 = GET / "person" :? ('page -> 2, 'per_page -> 100)
execute ()
}
Extracting values
val r1 = GET url "http://api.rest.org/person/" execute ()
val code1 = r1.statusCode
val list1 = jsonToList[Person](Json.parse(r1.body), JsPath)
Extracting values
val r1 = GET url "http://api.rest.org/person/" execute ()
val code1 = r1.statusCode
val list1 = jsonToList[Person](Json.parse(r1.body), JsPath)
Get from url http://api.rest.org/person/ returning the
status code and the body as list of persons
Extracting values
val r1 = GET url "http://api.rest.org/person" execute ()
val code1 = r1.statusCode
val list1 = jsonToValue[Person](Json.parse(r1.body), JsPath)
val code1 = GET url "http://api.rest.org/person"
returning (StatusCode)
Extracting values
val r1 = GET url "http://api.rest.org/person" execute ()
val code1 = r1.statusCode
val list1 = jsonToValue[Person](Json.parse(r1.body), JsPath)
val (code1, list1) = GET url "http://api.rest.org/person"
returning (StatusCode, BodyAsPersonList)
Extractors
Extracting values
trait ExtractorLike[+A] {
def name: String
def value(res: Response): Try[A]
def unapply(res: Response): Option[A] =
value(res).toOption
}
Extracting values
trait ExtractorLike[+A] {
def name: String
def value(res: Response): Try[A]
def unapply(res: Response): Option[A] =
value(res).toOption
}
Extracting values
trait ExtractorLike[+A] {
def name: String
def value(res: Response): Try[A]
def unapply(res: Response): Option[A] =
value(res).toOption
}
Extracting values
trait ExtractorLike[+A] {
def name: String
def value(res: Response): Try[A]
def unapply(res: Response): Option[A] =
value(res).toOption
}
Extracting values
case class Extractor[+A](name: String, op: Response => A)
extends ExtractorLike[A] {
override def value(implicit res: Response): Try[A] = {
Try { op(res) } recoverWith {
case e =>
Failure[A](new ExtractorFailedException(
s"Cannot extract $name from Response: $e",
e))
}
}
def andThen[B](nextOp: A => B): Extractor[B] =
copy(name = name + ".andThen ?", op = op andThen nextOp)
def as(newName: String) = copy(name = newName)
}
Extracting values
case class Extractor[+A](name: String, op: Response => A)
extends ExtractorLike[A] {
override def value(implicit res: Response): Try[A] = {
Try { op(res) } recoverWith {
case e =>
Failure[A](new ExtractorFailedException(
s"Cannot extract $name from Response: $e",
e))
}
}
def andThen[B](nextOp: A => B): Extractor[B] =
copy(name = name + ".andThen ?", op = op andThen nextOp)
def as(newName: String) = copy(name = newName)
}
Extracting values
case class Extractor[+A](name: String, op: Response => A)
extends ExtractorLike[A] {
override def value(implicit res: Response): Try[A] = {
Try { op(res) } recoverWith {
case e =>
Failure[A](new ExtractorFailedException(
s"Cannot extract $name from Response: $e",
e))
}
}
def andThen[B](nextOp: A => B): Extractor[B] =
copy(name = name + ".andThen ?", op = op andThen nextOp)
def as(newName: String) = copy(name = newName)
}
Extracting values
case class Extractor[+A](name: String, op: Response => A)
extends ExtractorLike[A] {
override def value(implicit res: Response): Try[A] = {
Try { op(res) } recoverWith {
case e =>
Failure[A](new ExtractorFailedException(
s"Cannot extract $name from Response: $e",
e))
}
}
def andThen[B](nextOp: A => B): Extractor[B] =
copy(name = name + ".andThen ?", op = op andThen nextOp)
def as(newName: String) = copy(name = newName)
}
Extracting values
val StatusCode = Extractor[Int](
"StatusCode", r => r.statusCode)
Extracting values
val StatusCode = Extractor[Int](
"StatusCode", r => r.statusCode)
val BodyText = Extractor[String](
"BodyText", r => r.body.get)
Extracting values
val StatusCode = Extractor[Int](
"StatusCode", r => r.statusCode)
val BodyText = Extractor[String](
"BodyText", r => r.body.get)
def header(name: String) = {
Extractor[String](s"header($name)",
r => r.headers(name).mkString(", "))
}
Extractors Compose
val JsonBody = BodyText andThen Json.parse as "JsonBody"
Extractors Compose
val JsonBody = BodyText andThen Json.parse as "JsonBody"
def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = {
val tag = implicitly[ClassTag[T]]
JsonBody andThen (jsonToValue(_, path)) as
(s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]")
}
Extractors Compose
val JsonBody = BodyText andThen Json.parse as "JsonBody"
def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = {
val tag = implicitly[ClassTag[T]]
JsonBody andThen (jsonToValue(_, path)) as
(s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]")
}
def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] =
jsonBodyAs(JsPath)
Extractors Compose
val JsonBody = BodyText andThen Json.parse as "JsonBody”
def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = {
val tag = implicitly[ClassTag[T]]
JsonBody andThen (jsonToValue(_, path)) as
(s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]")
}
def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] =
jsonBodyAs(JsPath)
val BodyAsPerson = jsonBodyAs[Person]
val BodyAsName = jsonBodyAs[String](__  "name")
Extracting values
implicit class RichRequestBuilder(builder: RequestBuilder) {
// ...
def returning[T1](ext1: Extractor[T1])
(implicit httpClient: HttpClient): T1 = {
val res = execute()
ext1.value(res)
}
def returning[T1, T2](ext1: Extractor[T1], ext1: Extractor[T2])
(implicit httpClient: HttpClient): (T1, T2) = {
val res = execute()
(ext1.value(res), ext2.value(res))
}
}
Extracting values
implicit class RichRequestBuilder(builder: RequestBuilder) {
// ...
def returning[T1](ext1: Extractor[T1])
(implicit httpClient: HttpClient): T1 = {
val res = execute()
ext1.value(res)
}
def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2])
(implicit httpClient: HttpClient): (T1, T2) = {
val res = execute()
(ext1.value(res), ext2.value(res))
}
}
Extracting values
implicit class RichRequestBuilder(builder: RequestBuilder) {
// ...
def returning[T1](ext1: Extractor[T1])
(implicit httpClient: HttpClient): T1 = {
val res = execute()
ext1.value(res)
}
def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2])
(implicit httpClient: HttpClient): (T1, T2) = {
val res = execute()
(ext1.value(res), ext2.value(res))
}
}
Extracting values
using(_ url "http://api.rest.org") { implicit rb =>
val (code1, list1) = GET / "person" returning
(StatusCode, BodyAsPersonList)
val (code2, id) = POST / "person" body personJson
returning (StatusCode, header("X-Person-Id”)
val (code3, person) = GET / "person" / id returning
(StatusCode, BodyAsPerson)
val (code4, list2) = GET / "person" returning
(StatusCode, BodyAsPersonList)
val code5 = DELETE / "person" / id returning StatusCode
val (code6, list3) = GET / "person" returning
(StatusCode, BodyAsPersonList)
}
Asserting values
val (code1, list1) = GET / "person" returning (
StatusCode, BodyAsPersonList)
assert (code1 === Status.OK)
assert (list1 === EmptyList)
Asserting values
val (code1, list1) = GET / "person" returning (
StatusCode, BodyAsPersonList)
assert (code1 === Status.OK)
assert (list1 === EmptyList)
Get from url http://api.rest.org/person/ asserting the
status code is OK and and the body is an empty list of
persons
Asserting values
val (code1, list1) = GET / "person" returning (
StatusCode, BodyAsPersonList)
assert (code1 === Status.OK)
assert (list1 === EmptyList)
GET / "person" asserting (
StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
Assertions
Asserting values
GET / "person" asserting (
StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
type Assertion = Response => Option[String]
Asserting values
implicit class RichExtractor[A](ext: ExtractorLike[A]) {
def ===[B >: A](expected: B): Assertion = { res =>
val maybeValue = ext.value(res)
maybeValue match {
case Success(value) if (value == expected) =>
None
case Success(value) =>
Some(s"${ext.name}: $value != $expected")
case Failure(e) =>
Some(e.getMessage)
}
}
}
Asserting values
implicit class RichExtractor[A](ext: ExtractorLike[A]) {
def ===[B >: A](expected: B): Assertion = ???
def !==[B >: A](expected: B): Assertion = ???
def < [B >: A](expected: B)
(implicit ord: math.Ordering[B]): Assertion = ???
def <=[B >: A](expected: B)
(implicit ord: math.Ordering[B]): Assertion = ???
def > [B >: A](expected: B)
(implicit ord: math.Ordering[B]): Assertion = ???
def >=[B >: A](expected: B)
(implicit ord: math.Ordering[B]): Assertion = ???
}
Asserting values
implicit class RichRequestBuilder(builder: RequestBuilder) {
def asserting(assertions: Assertion*)
(implicit client: HttpClient): Response = {
val response = execute()
val failures = for {
assertion <- assertions
failureMessage <- assertion(response)
} yield failureMessage
if (failures.nonEmpty) {
throw assertionFailed(failures)
}
response
}
}
Asserting values
using(_ url "http://api.rest.org") { implicit rb =>
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
val id = POST / "person" body personJson
asserting (StatusCode === Status.Created)
returning (header("X-Person-Id"))
GET / "person" / id asserting (StatusCode === Status.OK,
BodyAsPerson === Jason)
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === Seq(Jason))
DELETE / "person" / id asserting (StatusCode === Status.OK)
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
}
http://www.flickr.com/photos/ajkohn2001/2532935194/
Wow I can read it!
Asserting values
using(_ url "http://api.rest.org") { implicit rb =>
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
val id = POST / "person" body personJson
asserting (StatusCode === Status.Created)
returning (header("X-Person-Id"))
GET / "person" / id asserting (StatusCode === Status.OK,
BodyAsPerson === Jason)
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === Seq(Jason))
DELETE / "person" / id asserting (StatusCode === Status.OK)
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
}
RTFM? Wheres TFM?
http://wallpaperscraft.com/download/dog_boxer_laptop_lie_face_52801/
Codebase Structure
API
DSL
Codebase Structure
API
DSL
Extractors
Codebase Structure
API
DSL
Extractors
Json
Extractors
Xml
Extractors
Codebase Structure
API
DSL
Extractors
Json
Extractors
Xml
Extractors
Extended
DSL
Extended
Extractors
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r1.statusCode === Status.OK)
r1.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
assert(r2.statusCode === Status.Created)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r3.statusCode === Status.OK)
r3.body match {
Some(body) =>
assert(jsonAs[Person](body) === Jason)
None => fail("Expected a body"))
}
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r4.statusCode === Status.OK)
r4.body match {
Some(body) =>
assert(jsonAsList[Person](body) === Seq(Jason))
None => fail("Expected a body"))
}
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r5.statusCode === Status.OK)
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r6.statusCode === Status.OK)
r6.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
using(_ url "http://api.rest.org") { implicit rb =>
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
val id = POST / "person" body personJson
asserting (StatusCode === Status.Created)
returning (header("X-Person-Id"))
GET / "person" / id asserting (StatusCode === Status.OK,
BodyAsPersonList === Jason)
GET asserting (StatusCode === Status.OK,
BodyAsPersonList === Seq(Jason))
DELETE / "person" / id asserting (StatusCode === Status.OK)
GET / id asserting (StatusCode === Status.NotFound)
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
}
BoilerplateTest
DSLTest
val personJson = """{ "name": "Jason" }"""
val r1 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r1.statusCode === Status.OK)
r1.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
val r2 = httpClient(Request(
POST, new URI("http://api.rest.org/person/"),
Map(), Some(personJson)))
assert(r2.statusCode === Status.Created)
val id = r2.headers("X-Person-Id").head
val r3 = httpClient(Request(
GET, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r3.statusCode === Status.OK)
r3.body match {
Some(body) =>
assert(jsonAs[Person](body) === Jason)
None => fail("Expected a body"))
}
val r4 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r4.statusCode === Status.OK)
r4.body match {
Some(body) =>
assert(jsonAsList[Person](body) === Seq(Jason))
None => fail("Expected a body"))
}
val r5 = httpClient(Request(
DELETE, new URI("http://api.rest.org/person/" + id),
Map(), None))
assert(r5.statusCode === Status.OK)
val r6 = httpClient(Request(
GET, new URI("http://api.rest.org/person/"),
Map(), None))
assert(r6.statusCode === Status.OK)
r6.body match {
Some(body) =>
assert(jsonAsList[Person](body) === EmptyList)
None => fail("Expected a body"))
}
using(_ url "http://api.rest.org") { implicit rb =>
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
val id = POST / "person" body personJson
asserting (StatusCode === Status.Created)
returning (header("X-Person-Id"))
GET / "person" / id asserting (StatusCode === Status.OK,
BodyAsPersonList === Jason)
GET asserting (StatusCode === Status.OK,
BodyAsPersonList === Seq(Jason))
DELETE / "person" / id asserting (StatusCode === Status.OK)
GET / id asserting (StatusCode === Status.NotFound)
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
}
object Extractors {
trait ExtractorLike[+A] {
def name: String
def value(implicit res: Response): Try[A]
def unapply(res: Response): Option[A] = value(res).toOption
}
case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] {
override def value(implicit res: Response): Try[A] = {
Try { op(res) } recoverWith {
case e =>
Failure[A](new ExtractorFailedException(s"Cannot extract $name from Response: $e",e))
}
}
def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp)
def as(newName: String) = copy(name = newName)
}
val StatusCode = Extractor[Int]("StatusCode", r => r.statusCode)
val BodyText = Extractor[String]("BodyText", r => r.body.get)
def header(name: String) = {
Extractor[String](s"header($name)", r => r.headers(name).mkString(", "))
}
val JsonBody = BodyText andThen Json.parse as "JsonBody"
def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = {
val tag = implicitly[ClassTag[T]]
JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]")
}
def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] = jsonBodyAs(JsPath)
val BodyAsPerson = jsonBodyAs[Person]
}
trait Dsl extends Api with Extractors {
implicit def toRequest(b: RequestBuilder): Request = b.toRequest
implicit def methodToRequestBuilder(m: Method)(implicit builder: RequestBuilder): RequestBuilder = builder.withMethod(m)
def using(config: RequestBuilder => RequestBuilder)(process: RequestBuilder => Unit)(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}
type Assertion = Response => Option[String]
implicit class RichRequestBuilder(builder: RequestBuilder) {
def url(u: String) = b.withUrl(u)
def body(b: String) = b.withBody(b)
def /(p: Any) = b.addPath(p.toString)
def :?(params: (Symbol, Any)*) = b.addQuery(params map (p => (p._1.name, p._2.toString)): _*)
def execute()(implicit httpClient: HttpClient): Response = {
httpClient(builder)
}
def returning[T1](ext1: Extractor[T1])(implicit httpClient: HttpClient): T1 = {
val res = execute()
ext1.value(res)
}
def returning[T1, T2](ext1: Extractor[T1], ext1: Extractor[T2])(implicit httpClient: HttpClient): (T1, T2) = {
val res = execute()
(ext1.value(res), ext2.value(res))
}
def asserting(assertions: Assertion*)(implicit client: HttpClient): Response = {
val response = execute()
val assertionFailures = for {
assertion <- assertions
failureMessage <- assertion(response)
} yield failureMessage
if (assertionFailures.nonEmpty) {
throw assertionFailed(assertionFailures)
}
response
}
}
implicit class RichExtractor[A](ext: ExtractorLike[A]) {
def ===[B >: A](expected: B): Assertion = { res =>
val maybeValue = ext.value(res)
maybeValue match {
case Success(value) if (value == expected) =>
None
case Success(value) =>
Some(s"${ext.name}: $value != $expected")
case Failure(e) =>
Some(e.getMessage)
}
}
def !==[B >: A](expected: B): Assertion = ???
def < [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ???
def <=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ???
def > [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ???
def >=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ???
}
}
BoilerplateTestDslTest
DSLImplementation
http://en.wikipedia.org/wiki/Optical_communication
Code is Communication
using(_ url "http://api.rest.org") { implicit rb =>
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
val id = POST / "person" body personJson
asserting (StatusCode === Status.Created)
returning (header("X-Person-Id"))
GET / "person" / id asserting (StatusCode === Status.OK,
BodyAsPersonList === Jason)
GET asserting (StatusCode === Status.OK,
BodyAsPersonList === Seq(Jason))
DELETE / "person" / id asserting (StatusCode === Status.OK)
GET / "person" asserting (StatusCode === Status.OK,
BodyAsPersonList === EmptyList)
}
DSL Resources - Books
DSLs in Action
By Debasish Ghosh
ISBN: 9781935182450
http://www.manning.com/ghosh/
Scala in Depth
By Joshua D. Suereth
ISBN: 9781935182702
http://www.manning.com/suereth/
DSL Resources – Projects
http://www.flickr.com/photos/bilal-kamoon/6835060992/sizes/o/
Thank You!!!
Iain Hull
Email: iain.hull@workday.com
Twitter: @IainHull
Web: http://IainHull.github.io
Workday

More Related Content

Recently uploaded

Hyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdf
Hyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdfHyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdf
Hyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdfPrecisely
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brandgvaughan
 
Gen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdfGen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdfAddepto
 
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxUse of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxLoriGlavin3
 
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek SchlawackFwdays
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubKalema Edgar
 
TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024Lonnie McRorey
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebUiPathCommunity
 
Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Mattias Andersson
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenHervé Boutemy
 
DevEX - reference for building teams, processes, and platforms
DevEX - reference for building teams, processes, and platformsDevEX - reference for building teams, processes, and platforms
DevEX - reference for building teams, processes, and platformsSergiu Bodiu
 
"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr BaganFwdays
 
Digital Identity is Under Attack: FIDO Paris Seminar.pptx
Digital Identity is Under Attack: FIDO Paris Seminar.pptxDigital Identity is Under Attack: FIDO Paris Seminar.pptx
Digital Identity is Under Attack: FIDO Paris Seminar.pptxLoriGlavin3
 
Passkey Providers and Enabling Portability: FIDO Paris Seminar.pptx
Passkey Providers and Enabling Portability: FIDO Paris Seminar.pptxPasskey Providers and Enabling Portability: FIDO Paris Seminar.pptx
Passkey Providers and Enabling Portability: FIDO Paris Seminar.pptxLoriGlavin3
 
New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024
New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024
New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024BookNet Canada
 
Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)
Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)
Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)Mark Simos
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024BookNet Canada
 
The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsPixlogix Infotech
 
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxThe Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxLoriGlavin3
 

Recently uploaded (20)

Hyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdf
Hyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdfHyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdf
Hyperautomation and AI/ML: A Strategy for Digital Transformation Success.pdf
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brand
 
DMCC Future of Trade Web3 - Special Edition
DMCC Future of Trade Web3 - Special EditionDMCC Future of Trade Web3 - Special Edition
DMCC Future of Trade Web3 - Special Edition
 
Gen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdfGen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdf
 
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxUse of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
 
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding Club
 
TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio Web
 
Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache Maven
 
DevEX - reference for building teams, processes, and platforms
DevEX - reference for building teams, processes, and platformsDevEX - reference for building teams, processes, and platforms
DevEX - reference for building teams, processes, and platforms
 
"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan
 
Digital Identity is Under Attack: FIDO Paris Seminar.pptx
Digital Identity is Under Attack: FIDO Paris Seminar.pptxDigital Identity is Under Attack: FIDO Paris Seminar.pptx
Digital Identity is Under Attack: FIDO Paris Seminar.pptx
 
Passkey Providers and Enabling Portability: FIDO Paris Seminar.pptx
Passkey Providers and Enabling Portability: FIDO Paris Seminar.pptxPasskey Providers and Enabling Portability: FIDO Paris Seminar.pptx
Passkey Providers and Enabling Portability: FIDO Paris Seminar.pptx
 
New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024
New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024
New from BookNet Canada for 2024: Loan Stars - Tech Forum 2024
 
Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)
Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)
Tampa BSides - Chef's Tour of Microsoft Security Adoption Framework (SAF)
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
 
The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and Cons
 
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxThe Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
 

Featured

Everything You Need To Know About ChatGPT
Everything You Need To Know About ChatGPTEverything You Need To Know About ChatGPT
Everything You Need To Know About ChatGPTExpeed Software
 
Product Design Trends in 2024 | Teenage Engineerings
Product Design Trends in 2024 | Teenage EngineeringsProduct Design Trends in 2024 | Teenage Engineerings
Product Design Trends in 2024 | Teenage EngineeringsPixeldarts
 
How Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthHow Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthThinkNow
 
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdfAI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdfmarketingartwork
 
PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024Neil Kimberley
 
Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)contently
 
How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024Albert Qian
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsKurio // The Social Media Age(ncy)
 
Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Search Engine Journal
 
5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summarySpeakerHub
 
ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd Clark Boyd
 
Getting into the tech field. what next
Getting into the tech field. what next Getting into the tech field. what next
Getting into the tech field. what next Tessa Mero
 
Google's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentGoogle's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentLily Ray
 
Time Management & Productivity - Best Practices
Time Management & Productivity -  Best PracticesTime Management & Productivity -  Best Practices
Time Management & Productivity - Best PracticesVit Horky
 
The six step guide to practical project management
The six step guide to practical project managementThe six step guide to practical project management
The six step guide to practical project managementMindGenius
 
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...RachelPearson36
 
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Applitools
 

Featured (20)

Everything You Need To Know About ChatGPT
Everything You Need To Know About ChatGPTEverything You Need To Know About ChatGPT
Everything You Need To Know About ChatGPT
 
Product Design Trends in 2024 | Teenage Engineerings
Product Design Trends in 2024 | Teenage EngineeringsProduct Design Trends in 2024 | Teenage Engineerings
Product Design Trends in 2024 | Teenage Engineerings
 
How Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthHow Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental Health
 
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdfAI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
 
Skeleton Culture Code
Skeleton Culture CodeSkeleton Culture Code
Skeleton Culture Code
 
PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024
 
Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)
 
How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie Insights
 
Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024
 
5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary
 
ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd
 
Getting into the tech field. what next
Getting into the tech field. what next Getting into the tech field. what next
Getting into the tech field. what next
 
Google's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentGoogle's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search Intent
 
How to have difficult conversations
How to have difficult conversations How to have difficult conversations
How to have difficult conversations
 
Introduction to Data Science
Introduction to Data ScienceIntroduction to Data Science
Introduction to Data Science
 
Time Management & Productivity - Best Practices
Time Management & Productivity -  Best PracticesTime Management & Productivity -  Best Practices
Time Management & Productivity - Best Practices
 
The six step guide to practical project management
The six step guide to practical project managementThe six step guide to practical project management
The six step guide to practical project management
 
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
 
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
 

REST Test - Exploring DSL design in Scala

  • 1. REST Test Exploring DSL design in Scala
  • 2. Who am I? Iain Hull Email: iain.hull@workday.com Twitter: @IainHull Web: http://IainHull.github.io Workday
  • 3. HTTP Clients … What’s the problem? http://www.hdwpapers.com/pretty_face_of_boxer_dog_wallpaper_hd-wallpapers.html
  • 4. Simple HTTP Client case class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String]) case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String]) type HttpClient = Request => Response
  • 5. Simple HTTP Client case class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String]) case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String]) type HttpClient = Request => Response
  • 6. Simple HTTP Client case class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String]) case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String]) type HttpClient = Request => Response
  • 7. Simple HTTP Client case class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String]) case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String]) type HttpClient = Request => Response
  • 8. Using the Client val request = Request( GET, new URI("http://api.rest.org/person", Map(), None)) val response = httpClient(request) assert(response.code1 === Status.OK) assert(jsonAsList[Person](response.body.get) === EmptyList)
  • 9. Using the Client val request = Request( GET, new URI("http://api.rest.org/person", Map(), None)) val response = httpClient(request) assert(response.code === Status.OK) assert(jsonAsList[Person](response.body.get) === EmptyList)
  • 10. Using the Client val request = Request( GET, new URI("http://api.rest.org/person", Map(), None)) val response = httpClient(request) assert(response.code === Status.OK) assert(jsonAsList[Person](response.body.get) === EmptyList)
  • 11. Using the Client val request = Request( GET, new URI("http://api.rest.org/person", Map(), None)) val response = httpClient(request) assert(response.code === Status.OK) assert(jsonAsList[Person](response.body.get) === EmptyList)
  • 12. Sample API GET /person List all the persons POST /person Create a new person GET /person/{id} Retrieve a person DELETE /person/{id} Delete a person
  • 13. Retrieve the list GET /person Verify list is empty
  • 14. Create a new person GET /person Verify list is empty POST /person Verify it succeeds and returns an id
  • 15. Retrieve the person GET /person Verify list is empty POST /person Verify it succeeds and returns an id GET /person/{id} Verify details are the same
  • 16. Retrieve the list GET /person Verify list is empty POST /person Verify it succeeds and returns an id GET /person/{id} Verify details are the same GET /person Verify list contains the person
  • 17. Delete the person GET /person Verify list is empty POST /person Verify it succeeds and returns an id GET /person/{id} Verify details are the same GET /person Verify list contains the person DELETE /person/{id} Verify the status code
  • 18. Retrieve the list GET /person Verify list is empty POST /person Verify it succeeds and returns an id GET /person/{id} Verify details are the same GET /person Verify list contains the person DELETE /person/{id} Verify the status code GET /person Verify list is empty
  • 19. val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))
  • 20. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head
  • 21. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))
  • 22. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))
  • 23. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))
  • 24. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))
  • 25. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r1.statusCode === Status.OK) r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) } val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) assert(r2.statusCode === Status.Created) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r3.statusCode === Status.OK) r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body")) } val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r4.statusCode === Status.OK) r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body")) } val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r5.statusCode === Status.OK) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r6.statusCode === Status.OK) r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) }
  • 27. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r1.statusCode === Status.OK) r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) } val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) assert(r2.statusCode === Status.Created) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r3.statusCode === Status.OK) r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body")) } val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r4.statusCode === Status.OK) r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body")) } val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r5.statusCode === Status.OK) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r6.statusCode === Status.OK) r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) }
  • 35. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))
  • 36. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))
  • 37. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))
  • 38. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))
  • 39.
  • 40. Applying the builder val personJson = """{ "name": "Jason" }""" val rb = RequestBuilder() .withUrl("http://api.rest.org/person/") val r1 = httpClient(rb.withMethod(GET).toRequest) val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest) val id = r2.headers("X-Person-Id").head val r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest) val r4 = httpClient(rb.withMethod(GET).toRequest) val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest) val r6 = httpClient(rb.withMethod(GET).toRequest)
  • 41. Applying the builder val personJson = """{ "name": "Jason" }""" val rb = RequestBuilder() .withUrl("http://api.rest.org/person/") val r1 = httpClient(rb.withMethod(GET).toRequest) val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest) val id = r2.headers("X-Person-Id").head val r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest) val r4 = httpClient(rb.withMethod(GET).toRequest) val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest) val r6 = httpClient(rb.withMethod(GET).toRequest)
  • 42. Applying the builder val personJson = """{ "name": "Jason" }""" val rb = RequestBuilder() .withUrl("http://api.rest.org/person/") val r1 = httpClient(rb.withMethod(GET).toRequest) val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest) val id = r2.headers("X-Person-Id").head val r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest) val r4 = httpClient(rb.withMethod(GET).toRequest) val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest) val r6 = httpClient(rb.withMethod(GET).toRequest)
  • 43. Applying the builder val personJson = """{ "name": "Jason" }""" val rb = RequestBuilder() .withUrl("http://api.rest.org/person/") val r1 = httpClient(rb.withMethod(GET).toRequest) val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest) val id = r2.headers("X-Person-Id").head val r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest) val r4 = httpClient(rb.withMethod(GET).toRequest) val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest) val r6 = httpClient(rb.withMethod(GET).toRequest)
  • 44. Implicit Conversion val personJson = """{ "name": "Jason" }""" val rb = RequestBuilder() .withUrl("http://api.rest.org/person/") val r1 = httpClient(rb.withMethod(GET).toRequest) val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest) val id = r2.headers("X-Person-Id").head val r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest) val r4 = httpClient(rb.withMethod(GET).toRequest) val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest) val r6 = httpClient(rb.withMethod(GET).toRequest) implicit def toRequest(b: RequestBuilder): Request = b.toRequest
  • 45. Implicit Conversion val personJson = """{ "name": "Jason" }""" val rb = RequestBuilder() .withUrl("http://api.rest.org/person/" val r1 = httpClient(rb.withMethod(GET)) val r2 = httpClient(rb.withMethod(POST) .withBody(personJson)) val id = r2.headers("X-Person-Id").head val r3 = httpClient(rb.withMethod(GET).addPath(id)) val r4 = httpClient(rb.withMethod(GET)) val r5 = httpClient(rb.withMethod(DELETE).addPath(id)) val r6 = httpClient(rb.withMethod(GET)) implicit def toRequest(b: RequestBuilder): Request = b.toRequest
  • 46. Implicit Conversion val personJson = """{ "name": "Jason" }""" val rb = RequestBuilder() .withUrl("http://api.rest.org/person/") val r1 = httpClient(rb.withMethod(GET)) val r2 = httpClient(rb.withMethod(POST) .withBody(personJson)) val id = r2.headers("X-Person-Id").head val r3 = httpClient(rb.withMethod(GET).addPath(id)) val r4 = httpClient(rb.withMethod(GET)) val r5 = httpClient(rb.withMethod(DELETE).addPath(id)) val r6 = httpClient(rb.withMethod(GET))
  • 47. Nice, but show me some DSL
  • 48. Narrate your code • Get from url http://api.rest.org/person/ • Post personJson to url http://api.rest.org/person/ • Get from url http://api.rest.org/person/personId • Delete from url http://api.rest.org/person/personId
  • 49. Narrate your code • Get from url http://api.rest.org/person/ • Post personJson to url http://api.rest.org/person/ • Get from url http://api.rest.org/person/personId • Delete from url http://api.rest.org/person/personId
  • 52. Bootstapping the DSL httpClient(RequestBuilder() .withMethod(GET) .withUrl("http://api.rest.org/person/")) implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)
  • 53. Bootstapping the DSL httpClient(GET.withUrl("http://api.rest.org/person/")) implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)
  • 54. Bootstapping the DSL httpClient(GET.withUrl("http://api.rest.org/person/")) implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method) methodToRequestBuilder(GET) .withUrl("http://api.rest.org/person/")
  • 55. Bootstapping the DSL httpClient(GET.withUrl("http://api.rest.org/person/")) implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)
  • 56. Bootstapping the DSL httpClient(GET withUrl "http://api.rest.org/person/")
  • 57. Initial DSL val personJson = """{ "name": "Jason" }""" val r1 = httpClient( GET withUrl "http://api.rest.org/person/") val r2 = httpClient( POST withUrl "http://api.rest.org/person/" withBody personJson) val id = r2.headers.get("X-Person-Id").get.head val r3 = httpClient( GET withUrl "http://api.rest.org/person/" addPath id) val r4 = httpClient( GET withUrl "http://api.rest.org/person/") val r5 = httpClient( DELETE withUrl "http://api.rest.org/person/" addPath id) val r6 = httpClient( GET withUrl "http://api.rest.org/person/")
  • 58. Initial DSL val personJson = """{ "name": "Jason" }""" val r1 = httpClient( GET withUrl "http://api.rest.org/person/") val r2 = httpClient( POST withUrl "http://api.rest.org/person/" withBody personJson) val id = r2.headers.get("X-Person-Id").get.head val r3 = httpClient( GET withUrl "http://api.rest.org/person/" addPath id) val r4 = httpClient( GET withUrl "http://api.rest.org/person/") val r5 = httpClient( DELETE withUrl "http://api.rest.org/person/" addPath id) val r6 = httpClient( GET withUrl "http://api.rest.org/person/")
  • 59. Initial DSL val personJson = """{ "name": "Jason" }""" val r1 = httpClient(GET withUrl "http://api.rest.org/person/") val r2 = httpClient(POST withUrl "http://api.rest.org/person/" withBody personJson) val id = r2.headers.get("X-Person-Id").get.head val r3 = httpClient(GET withUrl "http://api.rest.org/person/" addPath id) val r4 = httpClient(GET withUrl "http://api.rest.org/person/") val r5 = httpClient(DELETE withUrl "http://api.rest.org/person/" addPath id) implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) } }
  • 60. Initial DSL implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" val r1 = GET withUrl "http://api.rest.org/person/" execute () val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute () val r4 = GET withUrl "http://api.rest.org/person/" execute () val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute () val r6 = GET withUrl "http://api.rest.org/person/" execute () implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) } }
  • 61. Initial DSL implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" val r1 = GET withUrl "http://api.rest.org/person/" execute () val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute () val r4 = GET withUrl "http://api.rest.org/person/" execute () val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute () val r6 = GET withUrl "http://api.rest.org/person/" execute () implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) } }
  • 62. Initial DSL implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" val r1 = GET withUrl "http://api.rest.org/person/" execute () val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute () val r4 = GET withUrl "http://api.rest.org/person/" execute () val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute () val r6 = GET withUrl "http://api.rest.org/person/" execute () implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) } }
  • 63. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""” val r1 = GET withUrl "http://api.rest.org/person/" execute () val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute () val id = r2.headers.get("X-Person-Id”) val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute () val r4 = GET withUrl "http://api.rest.org/person/" execute () val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute () val r6 = GET withUrl "http://api.rest.org/person/" execute ()
  • 64. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" val r1 = GET withUrl "http://api.rest.org/person/" execute () val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute () val r4 = GET withUrl "http://api.rest.org/person/" execute () val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute () val r6 = GET withUrl "http://api.rest.org/person/" execute () implicit def methodToRequestBuilder(m: Method): RequestBuilder = RequestBuilder().withMethod(m)
  • 65. Common Configuration implicit val httpClient = ... implicit val rb = RequestBuilder() withUrl "http://api.rest.org/person/" val personJson = """{ "name": "Jason" }""" val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute () implicit def methodToRequestBuilder(m: Method) (implicit builder: RequestBuilder): RequestBuilder = builder.withMethod(m)
  • 66. Common Configuration implicit val httpClient = ... implicit val rb = RequestBuilder() withUrl "http://api.rest.org/person/" val personJson = """{ "name": "Jason" }""" val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute ()
  • 67. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" using(_ withUrl "http://api.rest.org/person") { implicit rb => val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute () }
  • 68. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" using(_ withUrl "http://api.rest.org/person") { implicit rb => val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute () } def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder)) }
  • 69. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" using(_ withUrl "http://api.rest.org/person") { implicit rb => val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute () } def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder)) }
  • 70. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" using(_ withUrl "http://api.rest.org/person") { implicit rb => val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute () } def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder)) }
  • 71. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" using(_ withUrl "http://api.rest.org/person") { implicit rb => val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute () } def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder)) }
  • 72. Common Configuration object RequestBuilder { implicit val emptyBuilder = RequestBuilder( None, None, Seq(), Seq(), None) }
  • 73. Common Configuration implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" using(_ withUrl "http://api.rest.org/person") { implicit rb => val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute () }
  • 74. A spoonful of sugar
  • 75. A Spoonful of sugar implicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString) def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*) // ... }
  • 76. A Spoonful of sugar implicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString) def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*) // ... }
  • 77. A Spoonful of sugar implicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString) def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*) // ... }
  • 78. A Spoonful of sugar implicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString) def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*) // ... }
  • 79. A Spoonful of sugar implicit val httpClient = ... val personJson = """{ "name": "Jason" }""" using(_ url "http://api.rest.org") { implicit rb => val r1 = GET / "person" execute () val r2 = POST / "person" body personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET / "person" / id execute () val r4 = GET / "person" execute () val r5 = DELETE / "person" / id execute () val r6 = GET / "person" :? ('page -> 2, 'per_page -> 100) execute () }
  • 80. Extracting values val r1 = GET url "http://api.rest.org/person/" execute () val code1 = r1.statusCode val list1 = jsonToList[Person](Json.parse(r1.body), JsPath)
  • 81. Extracting values val r1 = GET url "http://api.rest.org/person/" execute () val code1 = r1.statusCode val list1 = jsonToList[Person](Json.parse(r1.body), JsPath) Get from url http://api.rest.org/person/ returning the status code and the body as list of persons
  • 82. Extracting values val r1 = GET url "http://api.rest.org/person" execute () val code1 = r1.statusCode val list1 = jsonToValue[Person](Json.parse(r1.body), JsPath) val code1 = GET url "http://api.rest.org/person" returning (StatusCode)
  • 83. Extracting values val r1 = GET url "http://api.rest.org/person" execute () val code1 = r1.statusCode val list1 = jsonToValue[Person](Json.parse(r1.body), JsPath) val (code1, list1) = GET url "http://api.rest.org/person" returning (StatusCode, BodyAsPersonList)
  • 85. Extracting values trait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption }
  • 86. Extracting values trait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption }
  • 87. Extracting values trait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption }
  • 88. Extracting values trait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption }
  • 89. Extracting values case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } } def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) def as(newName: String) = copy(name = newName) }
  • 90. Extracting values case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } } def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) def as(newName: String) = copy(name = newName) }
  • 91. Extracting values case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } } def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) def as(newName: String) = copy(name = newName) }
  • 92. Extracting values case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } } def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) def as(newName: String) = copy(name = newName) }
  • 93. Extracting values val StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode)
  • 94. Extracting values val StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode) val BodyText = Extractor[String]( "BodyText", r => r.body.get)
  • 95. Extracting values val StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode) val BodyText = Extractor[String]( "BodyText", r => r.body.get) def header(name: String) = { Extractor[String](s"header($name)", r => r.headers(name).mkString(", ")) }
  • 96. Extractors Compose val JsonBody = BodyText andThen Json.parse as "JsonBody"
  • 97. Extractors Compose val JsonBody = BodyText andThen Json.parse as "JsonBody" def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]] JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]") }
  • 98. Extractors Compose val JsonBody = BodyText andThen Json.parse as "JsonBody" def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]] JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]") } def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] = jsonBodyAs(JsPath)
  • 99. Extractors Compose val JsonBody = BodyText andThen Json.parse as "JsonBody” def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]] JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]") } def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] = jsonBodyAs(JsPath) val BodyAsPerson = jsonBodyAs[Person] val BodyAsName = jsonBodyAs[String](__ "name")
  • 100. Extracting values implicit class RichRequestBuilder(builder: RequestBuilder) { // ... def returning[T1](ext1: Extractor[T1]) (implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) } def returning[T1, T2](ext1: Extractor[T1], ext1: Extractor[T2]) (implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) } }
  • 101. Extracting values implicit class RichRequestBuilder(builder: RequestBuilder) { // ... def returning[T1](ext1: Extractor[T1]) (implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) } def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2]) (implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) } }
  • 102. Extracting values implicit class RichRequestBuilder(builder: RequestBuilder) { // ... def returning[T1](ext1: Extractor[T1]) (implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) } def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2]) (implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) } }
  • 103. Extracting values using(_ url "http://api.rest.org") { implicit rb => val (code1, list1) = GET / "person" returning (StatusCode, BodyAsPersonList) val (code2, id) = POST / "person" body personJson returning (StatusCode, header("X-Person-Id”) val (code3, person) = GET / "person" / id returning (StatusCode, BodyAsPerson) val (code4, list2) = GET / "person" returning (StatusCode, BodyAsPersonList) val code5 = DELETE / "person" / id returning StatusCode val (code6, list3) = GET / "person" returning (StatusCode, BodyAsPersonList) }
  • 104. Asserting values val (code1, list1) = GET / "person" returning ( StatusCode, BodyAsPersonList) assert (code1 === Status.OK) assert (list1 === EmptyList)
  • 105. Asserting values val (code1, list1) = GET / "person" returning ( StatusCode, BodyAsPersonList) assert (code1 === Status.OK) assert (list1 === EmptyList) Get from url http://api.rest.org/person/ asserting the status code is OK and and the body is an empty list of persons
  • 106. Asserting values val (code1, list1) = GET / "person" returning ( StatusCode, BodyAsPersonList) assert (code1 === Status.OK) assert (list1 === EmptyList) GET / "person" asserting ( StatusCode === Status.OK, BodyAsPersonList === EmptyList)
  • 108. Asserting values GET / "person" asserting ( StatusCode === Status.OK, BodyAsPersonList === EmptyList) type Assertion = Response => Option[String]
  • 109. Asserting values implicit class RichExtractor[A](ext: ExtractorLike[A]) { def ===[B >: A](expected: B): Assertion = { res => val maybeValue = ext.value(res) maybeValue match { case Success(value) if (value == expected) => None case Success(value) => Some(s"${ext.name}: $value != $expected") case Failure(e) => Some(e.getMessage) } } }
  • 110. Asserting values implicit class RichExtractor[A](ext: ExtractorLike[A]) { def ===[B >: A](expected: B): Assertion = ??? def !==[B >: A](expected: B): Assertion = ??? def < [B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? def <=[B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? def > [B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? def >=[B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? }
  • 111. Asserting values implicit class RichRequestBuilder(builder: RequestBuilder) { def asserting(assertions: Assertion*) (implicit client: HttpClient): Response = { val response = execute() val failures = for { assertion <- assertions failureMessage <- assertion(response) } yield failureMessage if (failures.nonEmpty) { throw assertionFailed(failures) } response } }
  • 112. Asserting values using(_ url "http://api.rest.org") { implicit rb => GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPerson === Jason) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) }
  • 114. Asserting values using(_ url "http://api.rest.org") { implicit rb => GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPerson === Jason) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) }
  • 120. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r1.statusCode === Status.OK) r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) } val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) assert(r2.statusCode === Status.Created) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r3.statusCode === Status.OK) r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body")) } val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r4.statusCode === Status.OK) r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body")) } val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r5.statusCode === Status.OK) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r6.statusCode === Status.OK) r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) } using(_ url "http://api.rest.org") { implicit rb => GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPersonList === Jason) GET asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / id asserting (StatusCode === Status.NotFound) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) } BoilerplateTest DSLTest
  • 121. val personJson = """{ "name": "Jason" }""" val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r1.statusCode === Status.OK) r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) } val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) assert(r2.statusCode === Status.Created) val id = r2.headers("X-Person-Id").head val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r3.statusCode === Status.OK) r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body")) } val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r4.statusCode === Status.OK) r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body")) } val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) assert(r5.statusCode === Status.OK) val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None)) assert(r6.statusCode === Status.OK) r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body")) } using(_ url "http://api.rest.org") { implicit rb => GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPersonList === Jason) GET asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / id asserting (StatusCode === Status.NotFound) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) } object Extractors { trait ExtractorLike[+A] { def name: String def value(implicit res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption } case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException(s"Cannot extract $name from Response: $e",e)) } } def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) def as(newName: String) = copy(name = newName) } val StatusCode = Extractor[Int]("StatusCode", r => r.statusCode) val BodyText = Extractor[String]("BodyText", r => r.body.get) def header(name: String) = { Extractor[String](s"header($name)", r => r.headers(name).mkString(", ")) } val JsonBody = BodyText andThen Json.parse as "JsonBody" def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]] JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]") } def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] = jsonBodyAs(JsPath) val BodyAsPerson = jsonBodyAs[Person] } trait Dsl extends Api with Extractors { implicit def toRequest(b: RequestBuilder): Request = b.toRequest implicit def methodToRequestBuilder(m: Method)(implicit builder: RequestBuilder): RequestBuilder = builder.withMethod(m) def using(config: RequestBuilder => RequestBuilder)(process: RequestBuilder => Unit)(implicit builder: RequestBuilder): Unit = { process(config(builder)) } type Assertion = Response => Option[String] implicit class RichRequestBuilder(builder: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString) def :?(params: (Symbol, Any)*) = b.addQuery(params map (p => (p._1.name, p._2.toString)): _*) def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) } def returning[T1](ext1: Extractor[T1])(implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) } def returning[T1, T2](ext1: Extractor[T1], ext1: Extractor[T2])(implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) } def asserting(assertions: Assertion*)(implicit client: HttpClient): Response = { val response = execute() val assertionFailures = for { assertion <- assertions failureMessage <- assertion(response) } yield failureMessage if (assertionFailures.nonEmpty) { throw assertionFailed(assertionFailures) } response } } implicit class RichExtractor[A](ext: ExtractorLike[A]) { def ===[B >: A](expected: B): Assertion = { res => val maybeValue = ext.value(res) maybeValue match { case Success(value) if (value == expected) => None case Success(value) => Some(s"${ext.name}: $value != $expected") case Failure(e) => Some(e.getMessage) } } def !==[B >: A](expected: B): Assertion = ??? def < [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? def <=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? def > [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? def >=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? } } BoilerplateTestDslTest DSLImplementation
  • 123. Code is Communication using(_ url "http://api.rest.org") { implicit rb => GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPersonList === Jason) GET asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) }
  • 124. DSL Resources - Books DSLs in Action By Debasish Ghosh ISBN: 9781935182450 http://www.manning.com/ghosh/ Scala in Depth By Joshua D. Suereth ISBN: 9781935182702 http://www.manning.com/suereth/
  • 125. DSL Resources – Projects
  • 126.
  • 128. Thank You!!! Iain Hull Email: iain.hull@workday.com Twitter: @IainHull Web: http://IainHull.github.io Workday

Editor's Notes

  1. The problem Narrate your you code DSL should just orchestrate an existing API Understand your context Documentation and Tests
  2. We are going to look at testing REST web services – How a DSL can help Not demo of a new open source library – using it explore DSL design But there is one question
  3. Standard Http Clients Fine for business logic – Boxing and unboxing data Talking to other systems like databases or other web services Not REST specific – REST system tests are very verbose Lets look at…
  4. This could be: Jersey, Dispatch, Rapture What does a system test using this client look like?
  5. How do test written in this style scale? Lets look at simple use case…
  6. Swagger What would a simple system test look like?
  7. Now lets look at the code
  8. This is just the first request there are no code to test the response
  9. Now we verify the results I don’t know about you but I’m like…
  10. And I wrote it So the question is why?
  11. What is wrong with this??
  12. Random pie chart from the internet We all know that we read code more than we write it, so But what is wrong with the code?
  13. Boiler plate code Hides the intent of your code But its harder to read code than write it Copy and Paste – you could write that test in 15-20 mins
  14. I program in Scala and expect to eat cake Worked on services whose system tests have grown to 1500 lines New to the team – small modification – read and understand all the test code? What can we do?
  15. Mini programming language Specific problem domain Standalone Embedded in general purpose language Thinly Embedded or deeply embedded
  16. Expresses the solution in the vocabulary of the domain Highlights the intent Reduces the boiler plate code
  17. We are gong to look at two approaches for designing a DSL How does they improve the intent of the code And reduce the boiler plate
  18. Scala has a lot of features and techniques to enable DSL construction Going to show some of these techniques and how they are assembled? (Not how those techniques work)
  19. Our test code created 6 http requests and validated their responses, here are the requests Lets use this code to design our DSL Start by – Calling out any code obscuring the intent
  20. Executing the request
  21. Positional parameters and unused parameters
  22. Repeating prefix These are the boiler plate we want our DSL to address The most popular tool for addressing these issues is …
  23. The builder pattern.
  24. Create a default builder with repeating properties
  25. Override this default and execute the request Little easier to write and maybe easier to read But I think we can do better …
  26. .. I want to point out The calls to toRequest are don’t add anything to our understanding If answer is NO, we should remove them, but how?
  27. Create an implicit conversion We are going to use implicits three ways today * converting one type to another – implicit views * add a new method to an existing types – pimp my library * pass smart default parameters into methods
  28. Now when we remove the toRequest method call The compiler sees a Request where is expects a RequestBuilder And our implicit conversion is in scope And calls our conversion automatically
  29. * Implicit conversions can reduces typing and boilerplate, and help expose the intent of code * Easier to read than write – must be discoverable – like worm holes to people reading your code
  30. Not here to learn the builder pattern The first and MOST IMPORTANT step – Writing the code in boilerplate Narrate the code as if explaining it to a colleague – now look for patterns ** Remember, Good design is just discovering the design that is already present in problem
  31. Notice that each sentence begins with the HTTP Method Make these the starting point to our DSL
  32. Starting with a single request
  33. How can we remove the highlighted code to bring the method to the front?
  34. Adding an implicit function converting http methods to RequestBuilders Explain function
  35. Now the HTTP method is closer to the start of our expression
  36. We can start all requests by calling a RequestBuilder operation on the appropriate Method object Now when we call a withUrl on the GET object the compiler expands the code tothe bubble
  37. This punctuation does not improve the intent of our code
  38. Infix notation Now lets look at the complete use case
  39. When narrating our code Do you execute the request before or after you describe it?
  40. Pimp my library pattern Create a new type to add the execute method to RequestBuilder
  41. The httpClient parameter is marked implicit
  42. The empty parameter list is required to terminate the expression It also highlights the side effects
  43. Can we pass this implicitly?
  44. Create an implicit RequestBuilder with the common url Add an implicit RequestBuilder parameter to the function that converts methods to RequestBuilders Now the common configuration is used to bootstrap each expression
  45. Nice but, no visual connection No where to document this feature We can solve both of …
  46. … these problems with a new method which applies the configuration to a block of code I would like like to call the function“with” but that’s a key word
  47. The using function takes 3 parameters
  48. The config parameter applies the common configuration This is simply a function that takes a takes a RequestBuilder and returns a new RequestBuilder with the configuration applied This function is easily constructed with scala’s underscore notation
  49. The process parameter is the code bock which is just a function taking a RequetBuilder and returning Unit
  50. Finally the implicit builder parameter allows using statements to be nested However where does the first implicit RequestBuilder come from?
  51. We also modify the RequestBuilder companion object, making the emptyBuilder available implicitly, when there is not other builder defined implicitly.
  52. The using function improves intent The code block provides visual connection Avoid pollution of implicit values Finally it provides a name for this feature - documentation
  53. Our DSL is starting to take shape
  54. Remember the RichRequestBuilder? Where we defined our execute method?
  55. Shorten the builder method names
  56. Use slash to concatenate url paths
  57. Question mark to add query parameters Colon is required to fiddle Scala operator precedents
  58. Now the intent of our requests is clear ** Approach rooted in the implantation – working at syntax level – have to be careful - personal specific language** What about the response?
  59. Starting with the first request – what do we do with the response Lets narrate it….
  60. Now lets just make up some code to express this
  61. Narrating the code has exposed a new domain concept – so what are StatusCode and BodyAsPersonList??
  62. These are Extractors – we haven’t decided on an impl Initially simple functions – then more powerful objects What do they look like?
  63. ExtractorLike specifies the core operations of an Extractor
  64. Extractor is the basic implementation of the trait.
  65. It takes a function from Response to any type A And implements the value method, with error handling.
  66. AndThen enables to be composed of other functions
  67. As renames composed extractors Now we can define some Extractors
  68. An Extractor has a name and a function to perform the extraction Here “r” is the response and it returns the statusCode
  69. Calling get on an Option is not safe However it is ok here because its context
  70. Header is a method to generate extractors for specified header Of course the standard headers are defined as constants. rfc2616
  71. jsonBodyAs generates an extractor that converts a json document Or part there of To any object
  72. Overloading to provide default parameters – view bounds
  73. How does our DSL use these?
  74. The returning method executes the Request in a RequestBuilder And extracts a result from the response
  75. An overloaded returning function takes a pair of extractors and returns their values in a pair There are versions of this method for 3 and 4 member tuples as well.
  76. Now instead of just returning a response Our DSL extracts values we can use But what are these values for?
  77. Lets narrate this code
  78. Again we can make up some code to express this
  79. So StatusCode is an extractors – but what is StatusCode is Status.OK We have discovered a new domain concept: an assertion But what is it?
  80. So what does an Assertion looking
  81. Is simply a function that takes a Response and returns an optional failure message Now we need a way to construct them
  82. This is the pimp my library pattern again RichExtractor adds the is method to extractors to convert them to assertions
  83. Also support the not equals and comparison operators Interesting – these operations do not perform the assertion – they create a function that performs the assertion later. We need something to execute these assertions
  84. Asserting first executes the request to retrieve the response. Then evaluates a sequence of Assertions looking for failures If any fail the assertionFailues sequence will be nonEmpty
  85. Now lets take some time to read the results
  86. Lets compare this with the original test
  87. We changed our design methodology to discover Extractors and Assertions ** Second approach rooted in the domain – relies more on narration – and invented syntax then though of implementation ** We have designed our DSL – Why is it important
  88. Samples User guide Reference documentation
  89. DSLs are complicated not mix them with business logic Implement the business logic in a generic OO or functional API The DSL should only orchestrate this API – Simple as possible / Testing / Code reuse
  90. I also kept the extractors separate to the DSL, However the DSL includes the vanilla HTTP Extractors This means custom extractors are orthogonal to the DSL
  91. Json and XML extractors are not part of the code DSL Other extractors can be defined, similarly
  92. This enables others to extend the DSL maybe adding support for Oauth and Json Covers our structure
  93. Why is smaller code better?? Why is it important that the intent is easily to follow? 52 v 16
  94. Why is smaller code better?? Why is it important that the intent is easily to follow? 100 lines – so break even after 3 times
  95. Code is communication The next person to work on your service Your testing team Your Product manager or customer
  96. QA don’t check 404 after deleting the person Architect, custom header is not very resty should use the location header
  97. We are going to look at testing REST web services And how a DSL can help But there is one question