Skip to content

Commit

Permalink
First pass at Http4s docs
Browse files Browse the repository at this point in the history
  • Loading branch information
codejudas authored and Devon Stewart committed Jul 4, 2021
1 parent 7d3b185 commit 9f31ed4
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 9 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Useful commands inside sbt console
- `cli`: Useful for scripting: `sbt 'cli --client ...'`
- `format`: Runs scalafmt against codebase
- `checkFormatting`: Verifies formatting, run as part of CI against PRs
- `mdoc`: Generate the doc microsite locally

Resources
=========
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ As all parameters are provided as arguments to the function stubs in the trait,
Separation of business logic
----------------------------

Providing an implementating of a function with a well-defined set of inputs and outputs is natural for any developer. By reducing the scope of the interface a developer writes against, implementations are more clear and concise.
Providing an implementation of a function with a well-defined set of inputs and outputs is natural for any developer. By reducing the scope of the interface a developer writes against, implementations are more clear and concise.

Furthermore, by providing business logic as an implementation of an abstract class, unit tests can test the routing layer and business logic independently, by design.

Expand Down
4 changes: 2 additions & 2 deletions modules/microsite/docs/scala/akka-http/generating-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: "Generating Clients - akka-http - scala - guardrail"
Generating clients
==================

As we've seen in [Generating a Server](generating-a-server), guardrail-generated servers establish a mapping between our business logic and a cordoned off a subset of HTTP. This permits us to focus on our business logic, without getting overloaded with the complexities of managing such a large protocol. The same is true with guardrail generated HTTP Clients: from a consumer's standpoint, HTTP calls should look like regular function calls, accepting domain-specific arguments and producing domain-specific results.
As we've seen in [Generating a Server](generating-a-server), guardrail-generated servers establish a mapping between our business logic and a cordoned off subset of HTTP. This permits us to focus on our business logic, without getting overloaded with the complexities of managing such a large protocol. The same is true with guardrail generated HTTP Clients: from a consumer's standpoint, HTTP calls should look like regular function calls, accepting domain-specific arguments and producing domain-specific results.

By generating minimal clients that only have enough business knowledge to map domain types to and from HTTP, opportunities for logical errors are effectively removed. While this does not eliminate logical errors entirely, establishing a firm boundary between the underlying protocol and hand-written code drastically reduces the scope of possible bugs.

Expand All @@ -28,7 +28,7 @@ DocsHelpers.renderScalaSnippet(AkkaHttp, GeneratingClients)(
Separation of protocol-concerns from API-level concerns
-------------------------------------------------------

As guardrail clients are built ontop of the function type `HttpRequest => Future[HttpResponse]`, client configuration is reduced to function composition. Some ideas:
As guardrail clients are built on top of the function type `HttpRequest => Future[HttpResponse]`, client configuration is reduced to function composition. Some ideas:

```scala
val singleRequestHttpClient = { (req: HttpRequest) =>
Expand Down
61 changes: 61 additions & 0 deletions modules/microsite/docs/scala/http4s/generating-a-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
layout: docs
title: "Generating a Server - http4s - scala - guardrail"
---

Generating a Server
===================

guardrail-generated servers come in two parts: a `Resource` and a `Handler`. The `Resource` contains all the routing logic, accepting a `Handler` as an argument to the `route` function in order to provide an HTTP service in whichever supported HTTP framework you're hosting your service in.

The following is an example from the [http4s](https://github.com/http4s/http4s) server generator:

```scala mdoc:passthrough
import com.twilio.guardrail.generators.Scala.Http4s
import com.twilio.guardrail.docs._
DocsHelpers.renderScalaSnippet(Http4s, GeneratingAServer)("""
|// The `Handler` trait is fully abstracted from the underlying http framework. As a result, with the exception of some
|// structural alterations (`F[_]` instead of `Future[_]` as the return type) the same handlers can be used with
|// different `Resource` implementations from different framework generators. This permits greater compatibility between
|// different frameworks without changing your business logic.
""".stripMargin,
""
)
```

As all parameters are provided as arguments to the function stubs in the trait, there's no concern of forgetting to extract a query string parameter, introducing a typo in a form parameter name, or forgetting to close the bytestream for the streaming HTTP Request.

The routes and resources generated by guardrail can be hooked up into your HTTP4s server like so:

```scala
val usersApi = new UsersApi() // Class which implements the generated UsersHandler from guardrail
val usersService = new UsersResource[IO]().routes(usersApi)
val httpApp = Router("/" -> usersService).orNotFound

// Same basic server setup as in the http4s quickstart
BlazeServerBuilder[IO]
.bindHttp(5000, "localhost")
.withHttpApp(httpApp)
.resource
.use(_ => IO.never)
.as(ExitCode.Success)
```

Separation of business logic
----------------------------

Providing an implementation of a function with a well-defined set of inputs and outputs is natural for any developer. By reducing the scope of the interface a developer writes against, implementations are more clear and concise.

Furthermore, by providing business logic as an implementation of an abstract class, unit tests can test the routing layer and business logic independently, by design.

API structure slip is impossible
--------------------------------

As parameters are explicitly provided as arguments to functions in `Handler`s, any alteration to parameters constitute a new function interface that must be implemented. As a result, if providing an implementation for an externally managed specification, the compiler informs when a previously written function is no longer sufficient.

By representing different response codes and structures as members of a sealed trait, it's impossible to return a structure that violates the specification, even for less frequently used response codes.

Finally, describing an endpoint in your specification without providing an implementation for it is a compiler error. This prevents reduction of functionality due to refactors, human error, or miscommunication with other teams.

<span style="float: left">[Prev: Sample API specification](sample-api-specification)</span>
<span style="float: right">[Next: Generating clients](generating-clients)</span>
35 changes: 35 additions & 0 deletions modules/microsite/docs/scala/http4s/generating-clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
layout: docs
title: "Generating Clients - http4s - scala - guardrail"
---

Generating clients
==================

As we've seen in [Generating a Server](generating-a-server), guardrail-generated servers establish a mapping between our business logic and a cordoned off subset of HTTP. This permits us to focus on our business logic, without getting overloaded with the complexities of managing such a large protocol. The same is true with guardrail generated HTTP Clients: from a consumer's standpoint, HTTP calls should look like regular function calls, accepting domain-specific arguments and producing domain-specific results.

By generating minimal clients that only have enough business knowledge to map domain types to and from HTTP, opportunities for logical errors are effectively removed. While this does not eliminate logical errors entirely, establishing a firm boundary between the underlying protocol and hand-written code drastically reduces the scope of possible bugs.

The following is an example from the [http4s](https://github.com/http4s/http4s) client generator:

```scala mdoc:passthrough
import com.twilio.guardrail.generators.Scala.Http4s
import com.twilio.guardrail.docs._
DocsHelpers.renderScalaSnippet(Http4s, GeneratingClients)(
"""|// Two constructors are provided, one accepting the `httpClient` and `Async`
|// implicitly, the other accepting an explicit `httpClient`, but still
|// accepting the `Async` implicitly
""".stripMargin,
""
)
```

Separation of protocol-concerns from API-level concerns
-------------------------------------------------------

As guardrail clients are built on top of any Http4s client type, client configuration is done the same way as you are
already familiar with when using Http4s.

Check out the docs for [Http4s Clients](https://http4s.org/v0.20/client/).

<span style="float: left">[Prev: Generating a Server](generating-a-server)</span>
22 changes: 17 additions & 5 deletions modules/microsite/docs/scala/http4s/index.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
---
layout: docs
title: "guardrail[http4s <- scala]"
title: "http4s - scala - guardrail"
---

These docs are under construction!
Table of Contents
=================

In the meantime, please refer to the [akka-http](../akka-http) docs for broad context and getting started.

Note that the `framework` for http4s is `"http4s"`, so using that in your SBT, Gradle, or Maven plugin configuration should get you started.
1. [What is guardrail](what-is-guardrail.md)
1. [Single Point of Truth](what-is-guardrail.md#single-point-of-truth)
1. [Unexpected API changes are compiler errors](what-is-guardrail.md#unexpected-api-changes-are-compiler-errors)
1. [Fewer binary dependencies](what-is-guardrail.md#fewer-binary-dependencies)
1. [Installation](installation)
1. [Sample API specification](sample-api-specification)
1. [Generating a Server](generating-a-server)
1. [Separation of business logic](generating-a-server#separation-of-business-logic)
1. [API structure slip is impossible](generating-a-server#api-structure-slip-is-impossible)
1. [Generating test-only (real) server mocks for unit tests](generating-a-server#generating-test-only-real-server-mocks-for-unit-tests)
1. [A note about scalatest integration](generating-a-server#a-note-about-scalatest-integration)
1. [Generating clients](generating-clients)
1. [Separation of protocol-concerns from API-level concerns](generating-clients#separation-of-protocol-concerns-from-api-level-concerns)
1. [guardrail Extensions](guardrail-extensions)
35 changes: 35 additions & 0 deletions modules/microsite/docs/scala/http4s/installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
layout: docs
title: "Installation - http4s - scala - guardrail"
---

Installation
============

guardrail is available as a modular core, with both [sbt](https://github.com/twilio/sbt-guardrail) and [Maven](https://github.com/twilio/guardrail-maven-plugin) integration. The core can also be run as a stand-alone [CLI](https://github.com/twilio/guardrail/blob/978a92db3dd46812aa19f05050995f864cbb5bb3/build.sbt#L33-L48) application, with full support for all features.

To generate servers or clients using the `http4s` framework, set `http4s` as the framework in the generation configuration in either sbt or maven.

If compiling with Scala < 2.13.x, you'll need to enable `-Ypartial-unification`:

```scala
scalacOptions += "-Ypartial-unification"
```

If compiling with Scala < 2.12.x, you'll additionally need the `-Xexperimental` flag:

```scala
scalacOptions += "-Xexperimental"
```

Additionally, you will need to manually include dependencies in your project for the following packages:
- `http4s`, dsl, server, and client dependencies
- `http4s-circe` for JSON decoding and encoding support
- `circe-generic` for JSON decoding and encoding support
- `cats-effect` for http4s integration
- `cats-core` for http4s integration

Versions of these libraries should be picked by checking out the [Compatibility Matrix](https://github.com/guardrail-dev/guardrail/blob/master/COMPATIBILITY.md).

<span style="float: left">[Prev: What is guardrail?](what-is-guardrail)</span>
<span style="float: right">[Next: Sample API specification](sample-api-specification)</span>
131 changes: 131 additions & 0 deletions modules/microsite/docs/scala/http4s/sample-api-specification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
layout: docs
title: "Sample API Specification - http4s - scala - guardrail"
---

Sample API specification
========================

The following is a complete, annotated OpenAPI specification file: (guardrail extensions are documented in [guardrail Extensions](#guardrail-extensions))

```yaml
swagger: "2.0" # Which version of the OpenAPI/Swagger specification we are following
info: # Primarily for consumption by documentation generation tools
title: My Service
version: 0.1.0
host: localhost:1234 # Default host (and optional port) to connect to for generated clients
schemes:
- http
paths: # All HTTP paths are direct children of the `paths` field

/user/{id}: # Paths can have variable patterns in paths

get: # HTTP method

operationId: getUser # Friendly name, ends up as the function name (in clients and servers)

x-jvm-package: users # Relative package for this client to live in. For convenience, the
# last package parameter is turned into a class name for clients and
# servers. In this case, `UsersClient`.

parameters: # All parameters (including path parameters) are listed here.

- name: id # The field name (case matters!), used to both identify the correct
# field to match, as well as generate a best-guess idiomatic Scala
# parameter name.

in: path # Where to look for the parameter

description: The ID of the user # The optional `description` parameter is not used in guardrail,
# but is useful for providing a detailed explanation on what is
# expected as a value for the parameter. For example:
# `description: User IDs are strings comprised of the concatenation
# of the two upper-case letters ID and a UUID stripped of any dashes
# i.e. ID4d9b1c54e4664c9d92aba94151a7f59f`

required: true # Required fields cannot be missing. `required: false` fields are
# represented as `Option[T]`

type: string # One of the primitive types supported in the OpenAPI specification.
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#dataTypes

x-scala-type: CustomString # Escape hatch to explicitly introduce a custom type. This is an
# advanced technique to introduce completely custom
# marshalling/unmarshalling/validation logic. Keep in mind, everyone
# else will just see a plain string!

responses: # All response codes that are possible are listed here

200: # Each HTTP status code is mapped to the corresponding textual
# representation in guardrail-generated servers.

schema: # The optional `schema` parameter describes what's possible to return
# as the body of a response

$ref: '#/definitions/User' # In the generated `UsersHandler` `getUser` function, we can call
# `respond.OK(user)`, where `user: definitions.User`

404: # We must represent our failure cases as well, otherwise we can
# never express failure!

description: Not found # The optional `description` parameter is not used in guardrail,
# but is useful here as an indicator that we don't have a response
# body for `404 Not Found` responses.

definitions: # All non-primitive structures are defined inside `definitions`

User: # This identifies a symbolic structure name. Not all names are
# translated into classes when rendered, depending on whether they
# identify classes with structure, or defer to standard classes
# like `Vector` for `type: array`.

type: object # will generate a `User` case class in the `definitions` package

required: # A list of which parameters are required. This is enforced for
# clients by having non-optional parameters, and for servers by
# ensuring all submitted data to the endpoint validates the schema
# before getting to your `Handler` function.

- id # These names must match the `properties` names exactly
- user_addresses

properties: # `object`s are permitted to have `properties`. These are translated
# into fields in the generated case classes.

id: # Case matters for `properties`! A heuristic determines whether it's
# possible to translate a property name into a unique, non-reserved
# camelCase identifier.

type: string # One of the primitive types supported in the OpenAPI specification.
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#dataTypes

user_addresses: # Similar to `id`, though `user_addresses` can be safely transformed into
# `userAddress`, so this is done to expose idiomatic Scala. The underlying
# marshallers and unmarshallers maintain this mapping for you though,
# so no chance of protocol violations.

$ref: '#/definitions/UserAddresses' # Ensures that the type of `userAddress` will be `Vector[UserAddress]`

UserAddresses:
type: array

items: # `items` is a special key for `type: array`, identifying the structure of the
# sequence members

$ref: '#/definitions/UserAddress' # Primitive types could be listed here, but as we're referring to another class,
# we need to explicitly use a `$ref`. This may change in the future,
# see https://github.com/twilio/guardrail/issues/76

UserAddress:
type: object
properties:
line1:
type: string
line2:
type: string
line3:
type: string
```
<span style="float: left">[Prev: Installation](installation)</span>
<span style="float: right">[Next: Generating a Server](generating-a-server)</span>
Loading

0 comments on commit 9f31ed4

Please sign in to comment.