Skip to content

Commit

Permalink
Move examples and DSL for it to the separate project
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksei Shashev committed Aug 1, 2023
1 parent 643aa1b commit 80fd925
Show file tree
Hide file tree
Showing 19 changed files with 386 additions and 293 deletions.
2 changes: 1 addition & 1 deletion backend/.sbtopts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-J-Xmx1536m
-J-XX:+AlwaysPreTouch
-J-XX:ReservedCodeCacheSize=128M
-J-XX:MaxMetaspaceSize=512M
-J-XX:MaxMetaspaceSize=1024M
-J-Xss2m
-J-XX:+TieredCompilation
-J-XX:+UseParallelGC
Expand Down
38 changes: 38 additions & 0 deletions backend/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,43 @@ val circeUtils = (project in file("circe-utils"))
libraryDependencies ++= Dependencies.json ++ Dependencies.zio ++ Dependencies.scalatest
)

val examples = (project in file("examples"))
.enablePlugins(
JavaAppPackaging
)
.dependsOn(circeUtils)
.settings(Settings.common)
.settings(
libraryDependencies ++= Seq(
Dependencies.cats,
Dependencies.tofu,
Dependencies.mouse,
Dependencies.enumeratum,
Dependencies.scalatestMain,
Dependencies.refined,
).flatten,
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "armeria-backend-zio" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "circe" % Versions.sttp,
"org.scalactic" %% "scalactic" % "3.2.2",
"pl.muninn" %% "scala-md-tag" % "0.2.3",
),
)
.settings(
addCommandAlias(
"fixCheck",
"scalafixAll --check; scalafmtCheck"
),
addCommandAlias(
"lintAll",
"scalafixAll; scalafmtAll"
),
addCommandAlias(
"simulacrum",
"scalafixEnable;scalafix AddSerializable;scalafix AddImplicitNotFound;scalafix TypeClassSupport;"
)
)

val dataAccess = (project in file("dataAccess"))
.settings(Settings.common)
.settings(
Expand Down Expand Up @@ -166,6 +203,7 @@ lazy val `mockingbird-native` = (project in file("mockingbird-native"))
)

lazy val integration = (project in file("integration"))
.dependsOn(examples)
.dependsOn(mockingbird)
.dependsOn(`mockingbird-api`)
.enablePlugins(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ru.tinkoff.tcb.mockingbird.edsl
package ru.tinkoff.tcb.mockingbird.edsl.interpreter

import scala.concurrent.Future

Expand All @@ -13,13 +13,10 @@ import org.scalatest.Assertion
import org.scalatest.funsuite.AsyncFunSuiteLike
import sttp.client3.*

import ru.tinkoff.tcb.mockingbird.edsl.makeUri
import ru.tinkoff.tcb.mockingbird.edsl.ExampleSet
import ru.tinkoff.tcb.mockingbird.edsl.model.*
import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.Delete
import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.Get
import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.Post

trait ScalaTestInterpretator extends AsyncFunSuiteLike {
trait AsyncScalaTestSuite extends AsyncFunSuiteLike {

type HttpResponseR = sttp.client3.Response[String]

Expand All @@ -37,7 +34,7 @@ trait ScalaTestInterpretator extends AsyncFunSuiteLike {
fa match {
case Describe(text, pos) => Future(info(text)(pos))
case SendHttp(request, pos) =>
buildRequest(request).send(sttpbackend).map(_.asInstanceOf[A])
buildRequest(host, request).send(sttpbackend).map(_.asInstanceOf[A])
case CheckHttp(response, expects, pos) =>
Future {
val resp = response.asInstanceOf[HttpResponseR]
Expand Down Expand Up @@ -79,17 +76,6 @@ trait ScalaTestInterpretator extends AsyncFunSuiteLike {
case Valid(_) => succeed
}

private def buildRequest(m: HttpRequest): Request[String, Any] = {
var req = m.body.fold(quickRequest)(quickRequest.body)
req = m.headers.foldLeft(req) { case (r, (k, v)) => r.header(k, v) }
val url = makeUri(host, m)
m.method match {
case Delete => req.delete(url)
case Get => req.get(url)
case Post => req.post(url)
}
}

object Checker {
def apply(checks: Map[String, Check])(vs: Map[String, Any]): ValidatedNel[String, Unit] =
checks.toSeq
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package ru.tinkoff.tcb.mockingbird.edsl.interpreter

import cats.arrow.FunctionK
import cats.data.Writer
import io.circe.Json
import mouse.boolean.*
import pl.muninn.scalamdtag.*
import pl.muninn.scalamdtag.tags.Markdown

import ru.tinkoff.tcb.mockingbird.edsl.ExampleSet
import ru.tinkoff.tcb.mockingbird.edsl.interpreter.buildRequest
import ru.tinkoff.tcb.mockingbird.edsl.model.*
import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.Delete
import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.Get
import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.Post

object MarkdownGenerator {
type HttpResponseR = {}
private val httpResponseR: HttpResponseR = new {}

def apply(host: String): MarkdownGenerator =
new MarkdownGenerator(host)

private object implicits {
implicit val httpMethodShow: Show[HttpMethod] = new Show[HttpMethod] {
override def show(m: HttpMethod): String =
m match {
case Delete => "DELETE"
case Get => "GET"
case Post => "POST"
}
}

implicit def valueMatcherShow[T: Show]: Show[ValueMatcher[T]] =
(vm: ValueMatcher[T]) =>
vm match {
case AnyValue(example) => example.show
case FixedValue(value) => value.show
}

implicit class ValueMatcherOps[T](private val vm: ValueMatcher[T]) extends AnyVal {
def value: T = vm match {
case AnyValue(example) => example
case FixedValue(value) => value
}
}

implicit val checkShow: Show[Check] = (check: Check) =>
check match {
case CheckAny(example) => example
case CheckInteger(matcher) => matcher.show
case CheckString(matcher) => matcher.show
case cj: CheckJson => buildJson(cj).spaces2
}

def buildJson(cj: CheckJson): Json =
cj match {
case CheckJsonAny(example) => example
case CheckJsonArray(items*) => Json.arr(items.map(buildJson): _*)
case CheckJsonNull => Json.Null
case CheckJsonNumber(matcher) => Json.fromDoubleOrNull(matcher.value)
case CheckJsonObject(fields*) => Json.obj(fields.map { case (n, v) => n -> buildJson(v) }: _*)
case CheckJsonString(matcher) => Json.fromString(matcher.value)
}

}

}

class MarkdownGenerator(host: String) {
import MarkdownGenerator.HttpResponseR
import MarkdownGenerator.httpResponseR
import MarkdownGenerator.implicits.*
import cats.syntax.writer.*

type W[A] = Writer[Vector[Markdown], A]

def generate(set: ExampleSet[HttpResponseR]): String = {
val tags = for {
_ <- Vector(h1(set.name)).tell
_ <- set.examples.traverse(generate)
} yield ()

markdown(tags.written).md
}

def generate(desc: ExampleDescription): W[Unit] =
for {
_ <- Vector[Markdown](h2(desc.name)).tell
_ <- desc.steps.foldMap(stepsPrinterW)
} yield ()

def stepsPrinterW: FunctionK[Step, W] = new (Step ~> W) {
def apply[A](fa: Step[A]): W[A] =
fa match {
case Describe(text, pos) => Vector(p(text)).tell

case SendHttp(request, pos) =>
val skipCurlStrings = Seq("--max-redirs", "--location", "Content-Length")
val sreq = buildRequest(host, request).toCurl
.split("\n")
.filterNot(s => skipCurlStrings.exists(r => s.contains(r)))
.mkString("", "\n", "\n")
Writer(Vector(codeBlock(sreq)), httpResponseR.asInstanceOf[A])

case CheckHttp(_, HttpResponseExpected(None, None, Seq()), _) =>
Writer value HttpResponse(0, None, Seq.empty)

case CheckHttp(_, HttpResponseExpected(code, body, headers), _) =>
val bodyStr = body.map(_.show)
val cb = Vector(
code.map(c => s"Код ответа: ${c.matcher.show}\n"),
headers.nonEmpty.option {
headers.map { case (k, v) => s"$k: '${v.matcher.show}'\n" }.mkString("Заголовки ответа:\n", "\n", "")
},
bodyStr.map("Тело ответа:\n" ++ _ ++ "\n"),
).flatten.mkString("\n")

Writer(
Vector(
p("Ответ:"),
codeBlock(cb)
),
HttpResponse(
code.fold(0L)(_.matcher.value).toInt,
bodyStr,
headers.map { case (k, c) => k -> c.matcher.value },
)
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ru.tinkoff.tcb.mockingbird.edsl

import sttp.client3.*
import sttp.model.Uri

import ru.tinkoff.tcb.mockingbird.edsl.model.HttpRequest
import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.*

package object interpreter {
def makeUri(host: String, req: HttpRequest): Uri = {
val endpoint = s"${host}${req.path}"
val query = req.query
uri"$endpoint?$query"
}

def buildRequest(host: String, m: HttpRequest): Request[String, Any] = {
var req = m.body.fold(quickRequest)(quickRequest.body)
req = m.headers.foldLeft(req) { case (r, (k, v)) => r.header(k, v, replaceExisting = true) }
val url = makeUri(host, m)
m.method match {
case Delete => req.delete(url)
case Get => req.get(url)
case Post => req.post(url)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ru.tinkoff.tcb.mockingbird.example
package ru.tinkoff.tcb.mockingbird.examples

import cats.syntax.all.*
import io.circe.parser
Expand All @@ -7,7 +7,7 @@ import ru.tinkoff.tcb.mockingbird.edsl.ExampleSet
import ru.tinkoff.tcb.mockingbird.edsl.model.*
import ru.tinkoff.tcb.utils.circe.optics.*

class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] {
import ValueMatcherSyntax.*

val name = "Базовые примеры работы с HTTP заглушками"
Expand All @@ -26,9 +26,7 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
path = "/api/internal/mockingbird/v2/stub",
body = """{
| "path": "/alpha/handler1",
| "pathPattern": null,
| "name": "Persistent HTTP Stub",
| "labels": [],
| "method": "GET",
| "scope": "persistent",
| "request": {
Expand All @@ -42,10 +40,7 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
| "Content-Type": "text/plain"
| },
| "code": "451"
| },
| "state": null,
| "persist": null,
| "seed": null
| }
|}""".stripMargin.some,
headers = Seq(
"Content-Type" -> "application/json",
Expand Down Expand Up @@ -84,9 +79,7 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
path = "/api/internal/mockingbird/v2/stub",
body = """{
| "path": "/alpha/handler1",
| "pathPattern": null,
| "name": "Ephemeral HTTP Stub",
| "labels": [],
| "method": "GET",
| "scope": "ephemeral",
| "request": {
Expand All @@ -100,10 +93,7 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
| "Content-Type": "text/plain"
| },
| "code": "200"
| },
| "state": null,
| "persist": null,
| "seed": null
| }
|}""".stripMargin.some,
headers = Seq(
"Content-Type" -> "application/json",
Expand All @@ -127,10 +117,8 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
path = "/api/internal/mockingbird/v2/stub",
body = """{
| "path": "/alpha/handler1",
| "pathPattern": null,
| "times": 2,
| "name": "Countdown Stub",
| "labels": [],
| "method": "GET",
| "scope": "countdown",
| "request": {
Expand All @@ -144,10 +132,7 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
| "Content-Type": "text/plain"
| },
| "code": "429"
| },
| "state": null,
| "persist": null,
| "seed": null
| }
|}""".stripMargin.some,
headers = Seq(
"Content-Type" -> "application/json",
Expand Down Expand Up @@ -280,7 +265,6 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
| "pathPattern": "/alpha/handler2/(?<obj>[-_A-z0-9]+)/(?<id>[0-9]+)",
| "times": 2,
| "name": "Simple HTTP Stub with path pattern",
| "labels": [],
| "method": "GET",
| "scope": "countdown",
| "request": {
Expand All @@ -298,10 +282,7 @@ class HttpStubSet[HttpResponseR] extends ExampleSet[HttpResponseR] {
| "Content-Type": "application/json"
| },
| "code": "200"
| },
| "state": null,
| "persist": null,
| "seed": null
| }
|}""".stripMargin.some,
headers = Seq(
"Content-Type" -> "application/json",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.tinkoff.tcb.mockingbird.examples

import ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator

object Main extends ZIOAppDefault {
def run: ZIO[Environment with ZIOAppArgs with Scope, Any, Any] = program

val basicHttpStub = new BasicHttpStub[MarkdownGenerator.HttpResponseR]()
val interpretator = MarkdownGenerator("http://localhost:8228")

val program =
for {
_ <- Console.print(interpretator.generate(basicHttpStub))
} yield ()
}

0 comments on commit 80fd925

Please sign in to comment.