Upload
iain-hull
View
1.718
Download
1
Embed Size (px)
DESCRIPTION
A step by step guide to creating a DSL to test rest web services. I presented this talk to the Scala Days in Berlin the 18th of June 2014 http://www.scaladays.org/#schedule/RESTTest--exploring-DSL-design-in-Scala Watch this presentation online https://www.parleys.com/play/53a7d2d1e4b0543940d9e56c
Citation preview
REST Test
Exploring DSL design in Scala
HTTP Clients … What’s the problem?http://www.hdwpapers.com/pretty_face_of_boxer_dog_wallpaper_hd-wallpapers.html
Simple HTTP Clientcase 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 Clientcase 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 Clientcase 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 Clientcase 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 Clientval 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 Clientval 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 Clientval 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 Clientval 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").headval 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").headval 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").headval 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").headval 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 CodeModify-ing 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").headval 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").headval 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").headval 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").headval 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 builderval 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").headval 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 builderval 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").headval 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 builderval 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").headval 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 builderval 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").headval 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 Conversionval 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").headval 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 Conversionval 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").headval 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 Conversionval 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").headval 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 DSLhttpClient(RequestBuilder() .withMethod(GET) .withUrl("http://api.rest.org/person/"))
Bootstapping the DSLhttpClient(RequestBuilder() .withMethod(GET) .withUrl("http://api.rest.org/person/"))
Bootstapping the DSLhttpClient(RequestBuilder() .withMethod(GET) .withUrl("http://api.rest.org/person/"))
implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)
Bootstapping the DSLhttpClient(GET.withUrl("http://api.rest.org/person/"))
implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)
Bootstapping the DSLhttpClient(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 DSLhttpClient(GET.withUrl("http://api.rest.org/person/"))
implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)
Bootstapping the DSLhttpClient(GET withUrl "http://api.rest.org/person/")
Initial DSLval 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.headval 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 DSLval 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.headval 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.headval 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 DSLimplicit 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 DSLimplicit 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.headval 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 DSLimplicit 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.headval 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 Configurationimplicit 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 Configurationimplicit 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.headval 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 Configurationimplicit 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.headval 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 Configurationimplicit 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.headval r3 = GET addPath id execute ()val r4 = GET execute ()val r5 = DELETE addPath id execute ()val r6 = GET execute ()
Common Configurationimplicit 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 Configurationimplicit 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 Configurationimplicit 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 Configurationimplicit 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 Configurationimplicit 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 Configurationobject RequestBuilder { implicit val emptyBuilder = RequestBuilder( None, None, Seq(), Seq(), None)}
Common Configurationimplicit 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 sugarimplicit 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 sugarimplicit 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 sugarimplicit 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 sugarimplicit 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 sugarimplicit 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.statusCodeval list1 = jsonToList[Person](Json.parse(r1.body), JsPath)
Extracting values
val r1 = GET url "http://api.rest.org/person/" execute ()val code1 = r1.statusCodeval 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.statusCodeval 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.statusCodeval list1 = jsonToValue[Person](Json.parse(r1.body), JsPath)
val (code1, list1) = GET url "http://api.rest.org/person" returning (StatusCode, BodyAsPersonList)
Extractors
Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}
Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}
Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}
Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}
Extracting valuescase 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 valuescase 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 valuescase 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 valuescase 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 valuesval StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode)
Extracting valuesval StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode)
val BodyText = Extractor[String]( "BodyText", r => r.body.get)
Extracting valuesval 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 Composeval JsonBody = BodyText andThen Json.parse as "JsonBody"
Extractors Composeval 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 Composeval 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 Composeval 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 valuesimplicit 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 valuesimplicit 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 valuesimplicit 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 valuesusing(_ 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 valuesimplicit 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 valuesimplicit 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 valuesimplicit 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 valuesusing(_ 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 valuesusing(_ 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
JsonExtractors
XmlExtractors
Codebase Structure
API
DSL
Extractors
JsonExtractors
XmlExtractors
ExtendedDSL
ExtendedExtractors
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)}
Boile
rpla
te T
est
DSL
Test
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 = ??? }}
Boile
rpla
te T
est
Dsl
Tes
t
DSL
Impl
emen
tatio
n
http://en.wikipedia.org/wiki/Optical_communication
Code is Communicationusing(_ 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 ActionBy Debasish Ghosh
ISBN: 9781935182450http://www.manning.com/ghosh/
Scala in DepthBy Joshua D. Suereth
ISBN: 9781935182702http://www.manning.com/suereth/
DSL Resources – Projects
http://www.flickr.com/photos/bilal-kamoon/6835060992/sizes/o/