Slide 1

Slide 1 text

SMASH YOUR ADAPTER MONOLITH WITH THE CONNECT PATTERN David Denton

Slide 2

Slide 2 text

HELLO, IT’S DAVID TRAINER GDE

Slide 3

Slide 3 text

what’s the plan? • identify a problem with adapters • can we modularise, customise, test? • leverage Kotlin language features

Slide 4

Slide 4 text

… but first, some http4k basics • server as a function • heavily TDD-driven • lightweight (core = 1mb) • run in-memory, or on 10 servers, 6 serverless platforms

Slide 5

Slide 5 text

http4k basics (ii) typealias HttpHandler = (Request) -> Response fun interface Filter : (HttpHandler) -> HttpHandler val http = { req: Request -> Response(OK).body("hello") } val filter = Filter { next -> { req -> next(req.header("foo", “bar")) } } val decoratedHttp: HttpHandler = filter.then(filter).then(http) val response: Response = decoratedHttp(Request(GET, "/uri"))

Slide 6

Slide 6 text

/endpoint2 /endpoint1 /endpoint3 Business logic API Adapter API Adapter Data access /endpoint4 API Adapter a typical http microservice /endpoint2 /endpoint1 /endpoint3 /endpoint4

Slide 7

Slide 7 text

fun MySecureApp(): HttpHandler = BearerAuth("my-very-secure-and-secret-bearer-token") .then( routes( echo(), health() ) ) fun echo() = "/echo" bind POST to { req: Request -> Response(OK).body(req.bodyString()) } fun health() = "/health" bind GET to { req: Request -> Response(OK).body("alive!") } val server = MySecureApp().asServer(Netty(8080)).start() server-side

Slide 8

Slide 8 text

/endpoint2 /endpoint1 /endpoint3 Business logic API Adapter API Adapter Data access /endpoint4 API Adapter a typical http microservice API Adapter API Adapter API Adapter

Slide 9

Slide 9 text

class GitHubApi(client: HttpHandler) { private val http = SetBaseUriFrom(Uri.of("https://api.github.com")) .then(SetHeader("Accept", "application/vnd.github.v3+json")) .then(client) fun getUser(username: String): UserDetails { val response = http(Request(GET, "/users/$username")) return UserDetails(userNameFrom(response), userOrgsFrom(response)) } fun getRepoLatestCommit(owner: String, repo: String) = .... } val gitHub: GitHubApi = GitHubApi(OkHttp()) val user: UserDetails = gitHub.getUser("octocat") the adapter monolith…

Slide 10

Slide 10 text

a real world example: K8S API • OpenApi Spec • 833 endpoints • 87k lines JSON • Kotlin version** • 32k LOC -> 17mb JAR * https://toolbox.http4k.org ** http://github.com/daviddenton/http4k-k8s-api

Slide 11

Slide 11 text

introducing connect • modularise monolithic adapters • simple pattern • testable & composable

Slide 12

Slide 12 text

action Action HTTP Req HTTP Resp Domain Req Domain Resp Action

Slide 13

Slide 13 text

interface GitHubApiAction { fun toRequest(): Request fun fromResponse(response: Response): R } data class GetUser(val username: String) : GitHubApiAction { override fun toRequest() = Request(GET, “/users/$username") override fun fromResponse(response: Response) = UserDetails(userNameFrom(response), userOrgsFrom(response)) } data class UserDetails(val name: String, val orgs: List) declaring actions

Slide 14

Slide 14 text

adapter Adapter Domain Response Action http client Adapter

Slide 15

Slide 15 text

interface GitHubApi { operator fun invoke(action: GitHubApiAction): R companion object } define the adapter

Slide 16

Slide 16 text

fun GitHubApi.Companion.Http(client: HttpHandler) = object : GitHubApi { private val http = SetBaseUriFrom(Uri.of(“https://api.github.com")) .then(SetHeader("Accept", "application/vnd.github.v3+json")) .then(client) override fun invoke(action: GitHubApiAction) = action.fromResponse(http(action.toRequest())) } val gitHub: GitHubApi = GitHubApi.Http(OkHttp()) val user: UserDetails = gitHub(GetUser("octocat")) reimplementing the adapter

Slide 17

Slide 17 text

fun GitHubApi.getUser(username: String): UserDetails = invoke(GetUser(username)) fun GitHubApi.getLatestRepoCommit(owner: String, repo: String): Commit = invoke(GetRepoLatestCommit(owner, repo)) val user: UserDetails = gitHub.getUser(“octocat") recreating our api

Slide 18

Slide 18 text

fun GitHubApi.getLatestUser(org: String, repo: String): UserDetails { val commit = getLatestRepoCommit(org, repo) return getUser(commit.author) } val latestUser: UserDetails = gitHub.getLatestUser("http4k", "http4k-connect") custom composite actions

Slide 19

Slide 19 text

testing the connect pattern

Slide 20

Slide 20 text

@Test fun `translates request`() { assertThat(GetUser("foobar").toRequest(), equalTo(Request(GET, "/users/foobar"))) } @Test fun `translates response`() { assertThat(GetUser("foobar").fromResponse(Response(OK).body("foobar/admin,mgmt")), equalTo(UserDetails("foobar", listOf("admin", "mgmt")))) } testing actions

Slide 21

Slide 21 text

@Test fun `get user details`() { val githubApi = mockk() val userDetails = UserDetails("bob", listOf("http4k")) every { githubApi(any()) } returns userDetails assertThat(githubApi.getUser("bob"), equalTo(userDetails)) } mock the adapter

Slide 22

Slide 22 text

class StubGitHubApi(private val users: Map) : GitHubApi { override fun invoke(action: GitHubApiAction): R = when (action) { is GetUser -> getUser(action, users) as R is GetRepoLatestCommit -> getRepoLatestCommit(action) as R else -> throw UnsupportedOperationException() } } private fun getUser(action: GetUser, users: Map) = users[action.username] private fun getRepoLatestCommit(action: GetRepoLatestCommit) = Commit(action.owner) stub the adapter

Slide 23

Slide 23 text

class RecordingGitHubApi(private val delegate: GitHubApi) : GitHubApi { val recorded = mutableListOf>() override fun invoke(action: GitHubApiAction): R { recorded += action return delegate(action) } } decorate the adapter

Slide 24

Slide 24 text

vary your programming model

Slide 25

Slide 25 text

interface GitHubApiAction { fun toRequest(): Request fun fromResponse(response: Response): Result } data class GetUser(val username: String) : GitHubApiAction { override fun toRequest() = Request(GET, "/users/$username") override fun fromResponse(response: Response) = when { response.status.successful -> Success(UserDetails( userNameFrom(response), userOrgsFrom(response)) ) else -> Failure(RuntimeException("Status: " + response.status)) } } result4k

Slide 26

Slide 26 text

summary • simple, modular adapters • testable and composable • flexible on model • works with any req/resp protocol

Slide 27

Slide 27 text

• featherweight Kotlin-API adapters • zero reflection —> serverless & graal • in-memory fakes / storage http4k/http4k-connect

Slide 28

Slide 28 text

THANKS! @daviddenton [email protected] https://dentondav.id http4k/http4k http4k/http4k-connect