Skip to content

Commit

Permalink
Update documentation for 2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexITC committed Dec 10, 2018
1 parent 95b7727 commit d562bba
Showing 1 changed file with 94 additions and 57 deletions.
151 changes: 94 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Join the chat at https://gitter.im/playsonify/Lobby](https://badges.gitter.im/playsonify/Lobby.svg)](https://gitter.im/playsonify/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Maven Central](https://img.shields.io/maven-central/v/com.alexitc/playsonify_2.12.svg)](https://maven-badges.herokuapp.com/maven-central/com.alexitc/playsonify_2.12)

An opinionated library to help you build JSON APIs in a practical way using Play Framework
An opinionated micro-framework that helps you to build JSON APIs in a practical way, currently supporting Play Framework, also, there is experimental support for akka-http.



Expand Down Expand Up @@ -34,20 +34,25 @@ An opinionated library to help you build JSON APIs in a practical way using Play
- [Development](#development)
- [Compile](#compile)
- [Test](#test)
- [Integrate with IntelliJ](#integrate-with-intellij)
- [Integrate with IntelliJ](#integrate-with-intellij)````



## State
while there are not too many commits in the project, the library has been used for some months on the [Crypto Coin Alerts project](https://github.com/AlexITC/crypto-coin-alerts) and it's in a stable state.
This library has been used for a year on the [Crypto Coin Alerts project](https://github.com/AlexITC/crypto-coin-alerts) project.

## Support
The library has been tested with the following versions, it might work with other versions while that is not official support for versions that are not in this list.
The library has been tested with the following versions, it might work with other versions that are not officially supported.
- Scala 2.12
- Play Framework 2.6
- akka-http 10.1.5

Please notice that the documentation is specific to play-framework, while most concepts apply, you can look into the akka-http tests to see what's different:
- [akka-http TestController](playsonify-akka-http/test/src/com/alexitc/playsonify/akka/controllers/TestController.scala)


## Name
The name `playsonify` was inspired by mixing the `JSON.stringify` function from JavaScript and the Play Framework which is what it is built for.
The name `playsonify` was inspired by mixing the `JSON.stringify` function from JavaScript and the Play Framework which is what it is built for (it might be worth considering another name now that akka-http is supported).


## Features
Expand All @@ -59,7 +64,8 @@ The name `playsonify` was inspired by mixing the `JSON.stringify` function from
- Keeps error responses consistent.
- Authenticate requests easily.
- HTTP Idiomatic controller tests.

- Primitives for offset-based pagination.
- Primitives for sorting results.


## What can playsonify do?
Expand Down Expand Up @@ -89,30 +95,35 @@ Define a controller:
class HelloWorldController @Inject() (components: MyJsonControllerComponents)
extends MyJsonController(components) {

def hello = publicWithInput { context: PublicContextWithModel[Person] =>
import Context._

def hello = publicInput { context: HasModel[Person] =>
val msg = s"Hello ${context.model.name}, you are ${context.model.age} years old"
val helloMessage = HelloMessage(msg)
val goodResult = Good(helloMessage)

Future.successful(goodResult)
}

def authenticatedHello = authenticatedNoInput { context: AuthenticatedContext[Int] =>
def authenticatedHello = authenticated { context: Authenticated =>
val msg = s"Hello user with id ${context.auth}"
val helloMessage = HelloMessage(msg)
val goodResult = Good(helloMessage)

Future.successful(goodResult)
}

def failedHello = publicNoInput[HelloMessage] { context: PublicContext =>
val errors = Every(UserEmailIncorrectError, UserAlreadyExistError, UserNotFoundError)
val badResult = Bad(errors)
def failedHello = public[HelloMessage] { context: Context =>
val errors = Every(
UserError.UserEmailIncorrect,
UserError.UserAlreadyExist,
UserError.UserNotFound)

val badResult = Bad(errors)
Future.successful(badResult)
}

def exceptionHello = publicNoInput[HelloMessage] { context: PublicContext =>
def exceptionHello = public[HelloMessage] { context: Context =>
Future.failed(new RuntimeException("database unavailable"))
}
}
Expand Down Expand Up @@ -165,12 +176,13 @@ Response:
```
< HTTP/1.1 500 Internal Server Error
{
"errors":[
{
"type":"server-error",
"errorId":"bc49d715fb8d4255a62c30d13322205b"
}
]
"errors": [
{
"errorId": "ab5beaf9307a4e1ab90d242786a84b29",
"message": "Internal error",
"type": "server-error"
}
]
}
```

Expand Down Expand Up @@ -234,16 +246,21 @@ class HelloWorldControllerSpec extends MyPlayAPISpec {
```

## Usage
The documentation might be incomplete, you can take a look to these examples:
- The [tests](playsonify/test/src/com/alexitc/playsonify/controllers).
The documentation assumes that you are already familiar with play-framework and it might be incomplete, you can always look into these applications:
- The [example application](examples/simple-app).
- The controllers from the [Crypto Coin Alerts project](https://github.com/AlexITC/crypto-coin-alerts/tree/master/alerts-server/app/controllers).

- The [Crypto Coin Alerts project](https://github.com/AlexITC/crypto-coin-alerts/tree/master/alerts-server/app/controllers).
- The [XSN Block Explorer project](https://github.com/X9Developers/block-explorer/tree/master/server/app/controllers)

### Add dependencies
Add these lines to your `build.sbt` file:
- `libraryDependencies += "com.alexitc" %% "playsonify" % "1.2.0"`
- `libraryDependencies += "com.alexitc" %% "playsonifytest" % "1.2.0" % Test` (optional, useful for testing).
```scala
libraryDependencies ++= Seq(
"com.alexitc" %% "playsonify-core" % playsonifyVersion,
"com.alexitc" %% "playsonify-play" % playsonifyVersion,
"com.alexitc" %% "playsonify-sql" % playsonifyVersion,
"com.alexitc" %% "playsonify-play-test" % playsonifyVersion % Test // optional, useful for testing
)
```


### Familiarize with scalactic Or and Every
Expand All @@ -253,61 +270,66 @@ As you might have noted, the use of scalactic could be easily replaced with `sca


### Familiarize with our type aliases
There are some type aliases that are helpful to not be nesting a lot of types on the method signatures, see the [core package](playsonify/src/com/alexitc/playsonify/core/package.scala), it looks like this:
There are some type aliases that are helpful to not be nesting a lot of types on the method signatures, see the [core package](playsonify-core/src/com/alexitc/playsonify/core/package.scala), it looks like this:

```scala
type ApplicationErrors = Every[ApplicationError]
type ApplicationResult[+A] = A Or ApplicationErrors
type FutureApplicationResult[+A] = Future[ApplicationResult[A]]
type FuturePaginatedResult[+A] = FutureApplicationResult[PaginatedResult[A]]
```

- `ApplicationErrors` represents a non-empty list of errors.
- `ApplicationResult` represents a result or a non-empty list of errors.
- `FutureApplicationResult` represents a result or a non-empty list of error that will be available in the future (asynchronous result).

### Create your application specific errors
We have already defined some top-level [application errors](playsonify/src/com/alexitc/playsonify/models/applicationErrors.scala), you are required to extend them in your error classes, this is crucial to get the correct mapping from an error to the HTTP status.
We have already defined some top-level [application errors](playsonify-core/src/com/alexitc/playsonify/models/applicationErrors.scala), you are required to extend them in your error classes, this is crucial to get the correct mapping from an error to the HTTP status.
```scala
trait InputValidationError extends ApplicationError
trait ConflictError extends ApplicationError
trait NotFoundError extends ApplicationError
trait AuthenticationError extends ApplicationError
trait ServerError extends ApplicationError {
// contains data private to the server
def cause: Throwable
def cause: Option[Throwable]
}
```

For example, let's say that we want to define the possible errors related to a user, we could define some errors:
```scala

sealed trait UserError

case object UserAlreadyExistError extends UserError with ConflictError {
override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = {
val message = messagesApi("user.error.alreadyExist")
val error = FieldValidationError("email", message)
List(error)
object UserError {

case object UserAlreadyExist extends UserError with ConflictError {
override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {
val message = i18nService.render("user.error.alreadyExist")
val error = FieldValidationError("email", message)
List(error)
}
}
}

case object UserNotFoundError extends UserError with NotFoundError {
override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = {
val message = messagesApi("user.error.notFound")
val error = FieldValidationError("userId", message)
List(error)
case object UserNotFound extends UserError with NotFoundError {
override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {
val message = i18nService.render("user.error.notFound")
val error = FieldValidationError("userId", message)
List(error)
}
}
}

case object UserEmailIncorrectError extends UserError with InputValidationError {
override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = {
val message = messagesApi("user.error.incorrectEmail")
val error = FieldValidationError("email", message)
List(error)
case object UserEmailIncorrect extends UserError with InputValidationError {
override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {
val message = i18nService.render("user.error.incorrectEmail")
val error = FieldValidationError("email", message)
List(error)
}
}
}
```

Then, when playsonify detect a `Bad` result, it will map the error to an HTTP status code in the following way:
Then, when playsonify detects a `Bad` result, it will map the error to an HTTP status code in the following way:
- InputValidationError -> 404 (BAD_REQUEST).
- ConflictError -> 409 (CONFLICT).
- NotFoundError -> 404 (NOT_FOUND).
Expand All @@ -322,21 +344,25 @@ Notice that the you have the preferred user language to render errors in that la


### Define your authenticataion mechanism
You are required to define your own [AbstractAuthenticatorService](playsonify/src/com/alexitc/playsonify/AbstractAuthenticatorService.scala), this service have the responsibility to decide which requests are aunthenticated and which ones are not, you first task is to define a model to represent an authenticated request, it is common to take the user or the user id for this, this model will be available in your controllers while dealing with authenticated requests.
You are required to define your own [AbstractAuthenticatorService](playsonify-play/src/com/alexitc/playsonify/play/AbstractAuthenticatorService.scala), this service have the responsibility to decide which requests are authenticated and which ones are not, you first task is to define a model to represent an authenticated request, it is common to take the user or the user id for this, this model will be available in your controllers while dealing with authenticated requests.

For example, suppose that we'll use an `Int` to represent the id of the user performing the request, at first, define the errors that represents that a request wasn't authenticated, like this:

```scala
sealed trait SimpleAuthError

case object InvalidAuthorizationHeaderError extends SimpleAuthError with AuthenticationError {
object SimpleAuthError {

case object InvalidAuthorizationHeader extends SimpleAuthError with AuthenticationError {

override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = {
val message = messagesApi("auth.error.invalidToken")
val error = HeaderValidationError("Authorization", message)
List(error)
override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {
val message = i18nService.render("auth.error.invalidToken")
val error = HeaderValidationError("Authorization", message)
List(error)
}
}
}

```

You could have defined the errors without the `SimpleAuthError` trait, I prefer to define a parent trait just in case that I need to use the errors in another part of the application.
Expand All @@ -352,7 +378,7 @@ class DummyAuthenticatorService extends AbstractAuthenticatorService[Int] {
.get(HeaderNames.AUTHORIZATION)
.flatMap { header => Try(header.toInt).toOption }

val result = Or.from(userIdMaybe, One(InvalidAuthorizationHeaderError))
val result = Or.from(userIdMaybe, One(SimpleAuthError.InvalidAuthorizationHeader))
Future.successful(result)
}
}
Expand All @@ -371,26 +397,35 @@ class MyJsonControllerComponents @Inject() (
override val messagesControllerComponents: MessagesControllerComponents,
override val executionContext: ExecutionContext,
override val publicErrorRenderer: PublicErrorRenderer,
override val i18nService: I18nPlayService,
override val authenticatorService: DummyAuthenticatorService)
extends JsonControllerComponents[Int]

```

Here you have a real example: [MyJsonControllerComponents](https://github.com/AlexITC/crypto-coin-alerts/blob/master/alerts-server/app/controllers/MyJsonControllerComponents.scala).



### Define your AbstractJsonController
Last, we need to define your customized [AbstractJsonController](playsonify/src/com/alexitc/playsonify/AbstractJsonController.scala), using guice dependency injection could lead us to this example:
Last, we need to define your customized [AbstractJsonController](playsonify-play/src/com/alexitc/playsonify/play/JsonControllerComponents.scala), using guice dependency injection could lead us to this example:

```scala
abstract class MyJsonController(components: MyJsonControllerComponents) extends AbstractJsonController(components) {

protected val logger = LoggerFactory.getLogger(this.getClass)

override protected def onServerError(error: ServerError, errorId: ErrorId): Unit = {
logger.error(s"Unexpected internal error = ${errorId.string}", error.cause)
override protected def onServerError(error: ServerError): Unit = {
error.cause match {
case Some(cause) =>
logger.error(s"Unexpected internal error, id = ${error.id.string}, error = $error", cause)

case None =>
logger.error(s"Unexpected internal error, id = ${error.id.string}, error = $error}")
}
}
}

```

Here you have a real example: [MyJsonController](https://github.com/AlexITC/crypto-coin-alerts/blob/master/alerts-server/app/controllers/MyJsonController.scala).
Expand Down Expand Up @@ -420,7 +455,9 @@ And the controller:
class HelloWorldController @Inject() (components: MyJsonControllerComponents)
extends MyJsonController(components) {

def hello = publicWithInput { context: PublicContextWithModel[Person] =>
import Context._

def hello = publicInput { context: HasModel[Person] =>
val msg = s"Hello ${context.model.name}, you are ${context.model.age} years old"
val helloMessage = HelloMessage(msg)
val goodResult = Good(helloMessage)
Expand All @@ -433,7 +470,7 @@ class HelloWorldController @Inject() (components: MyJsonControllerComponents)
What about authenticating the request?
```scala
...
def authenticatedHello = authenticatedNoInput { context: AuthenticatedContext[Int] =>
def authenticatedHello = authenticated { context: Authenticated =>
val msg = s"Hello user with id ${context.auth}"
val helloMessage = HelloMessage(msg)
val goodResult = Good(helloMessage)
Expand Down

0 comments on commit d562bba

Please sign in to comment.