Skip to content

Commit

Permalink
dave - more refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton committed May 24, 2015
1 parent cca3c10 commit 91574bd
Show file tree
Hide file tree
Showing 29 changed files with 174 additions and 147 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.github.daviddenton.fintrospect

import com.twitter.finagle.http.Response
import com.twitter.finagle.http.filter.Cors.Policy
import com.twitter.finagle.{Filter, Service}
import com.twitter.util.Future
import io.github.daviddenton.fintrospect.util.ResponseBuilder.Json
import org.jboss.netty.handler.codec.http.{HttpMethod, HttpRequest, HttpResponse}
import org.jboss.netty.handler.codec.http.{HttpMethod, HttpRequest, HttpResponse, HttpResponseStatus}

/**
* This implementation is portef from the Finagle version, in order to add support for the Request type used by
Expand Down Expand Up @@ -83,7 +83,7 @@ class CorsFilter(policy: Policy) extends Filter[HttpRequest, HttpResponse, HttpR
val headers = getHeaders(request)
policy.allowsMethods(method) flatMap { allowedMethods =>
policy.allowsHeaders(headers) map { allowedHeaders =>
setHeaders(setMethod(setMaxAge(setOriginAndCredentials(Json.Ok, origin)), allowedMethods), allowedHeaders)
setHeaders(setMethod(setMaxAge(setOriginAndCredentials(Response(HttpResponseStatus.OK), origin)), allowedMethods), allowedHeaders)
}
}
}
Expand All @@ -92,7 +92,7 @@ class CorsFilter(policy: Policy) extends Filter[HttpRequest, HttpResponse, HttpR
def apply(request: HttpRequest, service: Service[HttpRequest, HttpResponse]): Future[HttpResponse] = {
val response = request match {
case Preflight() => Future {
handlePreflight(request) getOrElse Json.Ok
handlePreflight(request) getOrElse Response(HttpResponseStatus.OK)
}
case _ => service(request) map {
handleSimple(request, _)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import io.github.daviddenton.fintrospect.FintrospectModule._
import io.github.daviddenton.fintrospect.Routing.fromBinding
import io.github.daviddenton.fintrospect.parameters.Requirement._
import io.github.daviddenton.fintrospect.util.ResponseBuilder._
import io.github.daviddenton.fintrospect.util.{ResponseBuilder, TypedResponseBuilder}
import io.github.daviddenton.fintrospect.util.TypedResponseBuilder
import org.jboss.netty.handler.codec.http.HttpMethod.GET
import org.jboss.netty.handler.codec.http.{HttpMethod, HttpRequest, HttpResponse}
import scala.language.existentials

import scala.PartialFunction._

Expand All @@ -22,7 +23,7 @@ object FintrospectModule {
/**
* Combines many modules
*/
def combine(modules: FintrospectModule[_]*): Binding = modules.map(_.totalBinding).reduce(_.orElse(_))
def combine(modules: FintrospectModule*): Binding = modules.map(_.totalBinding).reduce(_.orElse(_))

/**
* Convert a Binding to a Finagle Service
Expand All @@ -32,11 +33,11 @@ object FintrospectModule {
/**
* Create a module using the given base-path and description renderer.
*/
def apply[T](basePath: Path, descRenderer: DescriptionRenderer[T], responseRenderer: TypedResponseBuilder[T] = ResponseBuilder.Json): FintrospectModule[T] = {
new FintrospectModule[T](basePath, descRenderer, responseRenderer, Nil, empty[(HttpMethod, Path), Service[HttpRequest, HttpResponse]])
def apply(basePath: Path, responseRenderer: TypedResponseBuilder[_]): FintrospectModule = {
new FintrospectModule(basePath, responseRenderer, Nil, empty[(HttpMethod, Path), Service[HttpRequest, HttpResponse]])
}

private case class ValidateParams[T](route: Route, responseRenderer: TypedResponseBuilder[T]) extends SimpleFilter[HttpRequest, HttpResponse]() {
private case class ValidateParams(route: Route, responseRenderer: TypedResponseBuilder[_]) extends SimpleFilter[HttpRequest, HttpResponse]() {
override def apply(request: HttpRequest, service: Service[HttpRequest, HttpResponse]): Future[HttpResponse] = {
val paramsAndParseResults = route.describedRoute.params.map(p => (p, p.parseFrom(request)))
val withoutMissingOptionalParams = paramsAndParseResults.filterNot(pr => pr._1.requirement == Optional && pr._2.isEmpty)
Expand All @@ -57,25 +58,25 @@ object FintrospectModule {
/**
* Self-describing module builder (uses the immutable builder pattern).
*/
class FintrospectModule[T] private(basePath: Path, descRenderer: DescriptionRenderer[T], responseRenderer: TypedResponseBuilder[T], theRoutes: List[Route], private val currentBinding: Binding) {
class FintrospectModule private(basePath: Path, responseRenderer: TypedResponseBuilder[_], theRoutes: List[Route], private val currentBinding: Binding) {
private def withDefault() = withRoute(DescribedRoute("Description route").at(GET).bindTo(() => {
Service.mk((req) => responseRenderer.Ok(descRenderer(basePath, theRoutes)))
Service.mk((req) => responseRenderer.Description(basePath, theRoutes))
}))

private def totalBinding = withDefault().currentBinding

/**
* Attach described Route to the module.
*/
def withRoute(route: Route): FintrospectModule[T] = {
new FintrospectModule(basePath, descRenderer, responseRenderer, route :: theRoutes,
def withRoute(route: Route): FintrospectModule = {
new FintrospectModule(basePath, responseRenderer, route :: theRoutes,
currentBinding.orElse(route.toPf(basePath)(ValidateParams(route, responseRenderer).andThen(Identify(route, basePath)))))
}

/**
* Finaliser for the module builder to combine itself with another module into a Partial Function which matches incoming requests.
*/
def combine(that: FintrospectModule[_]): Binding = totalBinding.orElse(that.totalBinding)
def combine(that: FintrospectModule): Binding = totalBinding.orElse(that.totalBinding)

/**
* Finaliser for the module builder to convert itself to a Finagle Service. Use this function when there is only one module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package io.github.daviddenton.fintrospect
import com.twitter.finagle.Service
import com.twitter.finagle.http.path.Path
import com.twitter.util.Future
import io.github.daviddenton.fintrospect.util.JsonResponseBuilder.Error
import io.github.daviddenton.fintrospect.util.ResponseBuilder._
import org.jboss.netty.handler.codec.http.HttpResponseStatus._
import org.jboss.netty.handler.codec.http.{HttpMethod, HttpRequest, HttpResponse}

class Routing private(routes: PartialFunction[HttpRequest, Service[HttpRequest, HttpResponse]]) extends Service[HttpRequest, HttpResponse] {
private val notFoundPf: PartialFunction[HttpRequest, Service[HttpRequest, HttpResponse]] = {
case _ => new Service[HttpRequest, HttpResponse] {
def apply(request: HttpRequest): Future[HttpResponse] = Json.Error(NOT_FOUND, "No such route")
def apply(request: HttpRequest): Future[HttpResponse] = Error(NOT_FOUND, "No such route")
}
}
private val requestToService = routes orElse notFoundPf
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ package io.github.daviddenton.fintrospect.renderers
import argo.jdom.JsonNodeFactories.string
import argo.jdom.JsonRootNode
import com.twitter.finagle.http.path.Path
import io.github.daviddenton.fintrospect.Route
import io.github.daviddenton.fintrospect.util.ArgoJsonResponseBuilder
import io.github.daviddenton.fintrospect.util.ArgoUtil._
import io.github.daviddenton.fintrospect.{DescriptionRenderer, Route}

class SimpleJson private() extends DescriptionRenderer[JsonRootNode] {
/**
* Ultra-basic Renderer implementation that only supports the route paths and the main descriptions of each.
*/
object SimpleJson {

private def render(basePath: Path, route: Route): Field = {
route.method + ":" + route.describeFor(basePath) -> string(route.describedRoute.summary)
}

def apply(basePath: Path, routes: Seq[Route]): JsonRootNode = obj("resources" -> obj(routes.map(r => render(basePath, r))))
}
private def render(basePath: Path, routes: Seq[Route]): JsonRootNode = obj("resources" -> obj(routes.map(r => render(basePath, r))))

/**
* Ultra-basic Renderer implementation that only supports the route paths and the main descriptions of each.
*/
object SimpleJson {
def apply(): DescriptionRenderer[JsonRootNode] = new SimpleJson()
def apply(): ArgoJsonResponseBuilder = new ArgoJsonResponseBuilder(render)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import argo.jdom.{JsonNode, JsonRootNode}
import com.twitter.finagle.http.path.Path
import io.github.daviddenton.fintrospect._
import io.github.daviddenton.fintrospect.parameters.{Parameter, Requirement}
import io.github.daviddenton.fintrospect.util.ArgoJsonResponseBuilder
import io.github.daviddenton.fintrospect.util.ArgoUtil._

import scala.collection.JavaConversions._

class Swagger1dot1Json private() extends DescriptionRenderer[JsonRootNode] {
/**
* Renderer that provides basic Swagger v1.1 support. No support for bodies or schemas.
*/
object Swagger1dot1Json {

private def render(requirementAndParameter: (Requirement, Parameter[_])): JsonNode = obj(
"name" -> string(requirementAndParameter._2.name),
Expand All @@ -30,18 +34,13 @@ class Swagger1dot1Json private() extends DescriptionRenderer[JsonRootNode] {
.map(resp => obj("code" -> number(resp.status.getCode), "reason" -> string(resp.description))).toSeq)
)

def apply(basePath: Path, routes: Seq[Route]): JsonRootNode = {
private def render(basePath: Path, routes: Seq[Route]): JsonRootNode = {
val api = routes
.groupBy(_.describeFor(basePath))
.map { case (path, routesForPath) => obj("path" -> string(path), "operations" -> array(routesForPath.map(render(_)._2)))}

obj("swaggerVersion" -> string("1.1"), "resourcePath" -> string("/"), "apis" -> array(asJavaIterable(api)))
}
}

/**
* Renderer that provides basic Swagger v1.1 support. No support for bodies or schemas.
*/
object Swagger1dot1Json {
def apply(): DescriptionRenderer[JsonRootNode] = new Swagger1dot1Json()
def apply(): ArgoJsonResponseBuilder = new ArgoJsonResponseBuilder(render)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import argo.jdom.{JsonNode, JsonRootNode}
import com.twitter.finagle.http.path.Path
import io.github.daviddenton.fintrospect._
import io.github.daviddenton.fintrospect.parameters.{Body, Parameter, Requirement}
import io.github.daviddenton.fintrospect.util.ArgoJsonResponseBuilder
import io.github.daviddenton.fintrospect.util.ArgoUtil._

class Swagger2dot0Json private(apiInfo: ApiInfo) extends DescriptionRenderer[JsonRootNode] {
/**
* Renderer that provides Swagger v2.0 support
*/
object Swagger2dot0Json {

private val schemaGenerator = new JsonToJsonSchema()

Expand Down Expand Up @@ -65,29 +69,25 @@ class Swagger2dot0Json private(apiInfo: ApiInfo) extends DescriptionRenderer[Jso
obj("title" -> string(apiInfo.title), "version" -> string(apiInfo.version), "description" -> string(apiInfo.description.getOrElse("")))
}

def apply(basePath: Path, routes: Seq[Route]): JsonRootNode = {
val pathsAndDefinitions = routes
.groupBy(_.describeFor(basePath))
.foldLeft(FieldsAndDefinitions()) {
case (memo, (path, routesForThisPath)) =>
val routeFieldsAndDefinitions = routesForThisPath.foldLeft(FieldsAndDefinitions()) {
case (memoFields, route) => memoFields.add(render(basePath, route))
}
memo.add(path -> obj(routeFieldsAndDefinitions.fields), routeFieldsAndDefinitions.definitions)
}
obj(
"swagger" -> string("2.0"),
"info" -> render(apiInfo),
"basePath" -> string("/"),
"paths" -> obj(pathsAndDefinitions.fields),
"definitions" -> obj(pathsAndDefinitions.definitions)
)
private def rendererFor(apiInfo: ApiInfo): (Path, Seq[Route]) => JsonRootNode = {
(basePath: Path, routes: Seq[Route]) =>
val pathsAndDefinitions = routes
.groupBy(_.describeFor(basePath))
.foldLeft(FieldsAndDefinitions()) {
case (memo, (path, routesForThisPath)) =>
val routeFieldsAndDefinitions = routesForThisPath.foldLeft(FieldsAndDefinitions()) {
case (memoFields, route) => memoFields.add(render(basePath, route))
}
memo.add(path -> obj(routeFieldsAndDefinitions.fields), routeFieldsAndDefinitions.definitions)
}
obj(
"swagger" -> string("2.0"),
"info" -> render(apiInfo),
"basePath" -> string("/"),
"paths" -> obj(pathsAndDefinitions.fields),
"definitions" -> obj(pathsAndDefinitions.definitions)
)
}
}

/**
* Renderer that provides Swagger v2.0 support
*/
object Swagger2dot0Json {
def apply(apiInfo: ApiInfo): DescriptionRenderer[JsonRootNode] = new Swagger2dot0Json(apiInfo)
def apply(apiInfo: ApiInfo): ArgoJsonResponseBuilder = new ArgoJsonResponseBuilder(rendererFor(apiInfo))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.daviddenton.fintrospect.util

import argo.jdom.JsonRootNode
import com.twitter.finagle.http.path.Path
import io.github.daviddenton.fintrospect.Route
import io.github.daviddenton.fintrospect.util.ArgoUtil._

import scala.language.implicitConversions

class ArgoJsonResponseBuilder(descBuilder: (Path, Seq[Route]) => JsonRootNode) extends TypedResponseBuilder[JsonRootNode](
() => new JsonResponseBuilder,
descBuilder,
badParameters => {
val messages = badParameters.map(p => obj(
"name" -> string(p.name),
"type" -> string(p.where),
"datatype" -> string(p.paramType.name),
"required" -> boolean(p.requirement.required)
))

obj("message" -> string("Missing/invalid parameters"), "params" -> array(messages))
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.daviddenton.fintrospect.util

import argo.format.PrettyJsonFormatter
import argo.jdom.JsonRootNode
import io.github.daviddenton.fintrospect.ContentTypes
import io.github.daviddenton.fintrospect.util.ArgoUtil._
import org.jboss.netty.handler.codec.http.{HttpResponseStatus, HttpResponse}
import org.jboss.netty.handler.codec.http.HttpResponseStatus._

class JsonResponseBuilder extends ResponseBuilder[JsonRootNode](
new PrettyJsonFormatter().format,
errorMessage => obj("message" -> string(errorMessage)),
throwable => string(Option(throwable.getMessage).getOrElse(throwable.getClass.getName)).asInstanceOf[JsonRootNode],
ContentTypes.APPLICATION_JSON)

object JsonResponseBuilder {

def Response() = apply()

def Ok: HttpResponse = apply().withCode(OK).build

def Ok(content: JsonRootNode): HttpResponse = apply().withCode(OK).withContent(content).build

def Ok(content: String): HttpResponse = apply().withCode(OK).withContent(content).build

def Error(status: HttpResponseStatus, content: JsonRootNode): HttpResponse = apply().withCode(status).withContent(content).build

def Error(status: HttpResponseStatus, message: String): HttpResponse = apply().withCode(status).withErrorMessage(message).build

def Error(status: HttpResponseStatus, error: Throwable): HttpResponse = apply().withCode(status).withError(error).build

// bin?!?!
def apply() = new JsonResponseBuilder()
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package io.github.daviddenton.fintrospect.util

import argo.format.PrettyJsonFormatter
import argo.jdom.JsonRootNode
import com.twitter.finagle.http.Response
import com.twitter.util.Future
import io.github.daviddenton.fintrospect.util.ArgoUtil._
import io.github.daviddenton.fintrospect.{ContentType, ContentTypes}
import io.github.daviddenton.fintrospect.ContentType
import org.jboss.netty.buffer.ChannelBuffers._
import org.jboss.netty.handler.codec.http.{HttpResponse, HttpResponseStatus}
import org.jboss.netty.util.CharsetUtil._

import scala.language.implicitConversions

class ResponseBuilder[T](toFormat: T => String, contentType: ContentType) {
class ResponseBuilder[T](toFormat: T => String,
errorFormat: String => T,
exceptionFormat: Throwable => T,
contentType: ContentType) {
private val response = Response()

def withError(e: Throwable) = withContent(exceptionFormat(e))

def withErrorMessage(e: String) = withContent(errorFormat(e))

def withCode(code: HttpResponseStatus): ResponseBuilder[T] = {
response.setStatus(code)
this
Expand All @@ -38,20 +42,4 @@ object ResponseBuilder {
implicit def toFuture(builder: ResponseBuilder[_]): Future[HttpResponse] = builder.toFuture

implicit def toFuture(response: HttpResponse): Future[HttpResponse] = Future.value(response)

def Json = new TypedResponseBuilder[JsonRootNode](
() => new ResponseBuilder[JsonRootNode](new PrettyJsonFormatter().format, ContentTypes.APPLICATION_JSON),
errorMessage => obj("message" -> string(errorMessage)),
throwable => string(Option(throwable.getMessage).getOrElse(throwable.getClass.getName)).asInstanceOf[JsonRootNode],
badParameters => {
val messages = badParameters.map(p => obj(
"name" -> string(p.name),
"type" -> string(p.where),
"datatype" -> string(p.paramType.name),
"required" -> boolean(p.requirement.required)
))

ArgoUtil.obj("message" -> string("Missing/invalid parameters"), "params" -> array(messages))
})

}

0 comments on commit 91574bd

Please sign in to comment.