From 01de24fb57804aa38843055260069821641bd41c Mon Sep 17 00:00:00 2001 From: Tyler Southwick Date: Mon, 2 Oct 2017 08:56:13 -0700 Subject: [PATCH 1/5] add API.md blueprint support --- .travis.yml | 3 + blueprint/setup_drafter | 7 ++ .../com/nike/redwiggler/blueprint/Ast.scala | 28 +++++++ .../blueprint/BlueprintPathParser.scala | 18 +++++ .../BlueprintSpecificationProvider.scala | 40 ++++++++++ .../blueprint/parser/BlueprintParser.scala | 9 +++ .../parser/DrafterBlueprintParser.scala | 57 ++++++++++++++ .../parser/ProtagonistBlueprintParser.scala | 78 +++++++++++++++++++ .../com/nike/redwiggler/blueprint/test1.md | 78 +++++++++++++++++++ .../blueprint/BlueprintPathParserSpec.scala | 16 ++++ .../BlueprintSpecificationProviderSpec.scala | 63 +++++++++++++++ build.sbt | 11 ++- 12 files changed, 407 insertions(+), 1 deletion(-) create mode 100755 blueprint/setup_drafter create mode 100644 blueprint/src/main/scala/com/nike/redwiggler/blueprint/Ast.scala create mode 100644 blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala create mode 100644 blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala create mode 100644 blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/BlueprintParser.scala create mode 100644 blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/DrafterBlueprintParser.scala create mode 100644 blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/ProtagonistBlueprintParser.scala create mode 100644 blueprint/src/test/resources/com/nike/redwiggler/blueprint/test1.md create mode 100644 blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala create mode 100644 blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProviderSpec.scala diff --git a/.travis.yml b/.travis.yml index 155b2be..75c0f9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: scala scala: - 2.12.3 +before_script: + - "npm install protagonist" + - "./blueprint/setup_drafter" script: - "sbt +test +integration-tests/test +readme/test" - "sbt clean coverage test integration-tests/test readme/test coverageReport" diff --git a/blueprint/setup_drafter b/blueprint/setup_drafter new file mode 100755 index 0000000..5bb836c --- /dev/null +++ b/blueprint/setup_drafter @@ -0,0 +1,7 @@ +#!/bin/sh + +git clone --branch v3.2.2 --recursive git://github.com/apiaryio/drafter.git +cd drafter +./configure +make test +make drafter diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/Ast.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/Ast.scala new file mode 100644 index 0000000..4dc6b62 --- /dev/null +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/Ast.scala @@ -0,0 +1,28 @@ +package com.nike.redwiggler.blueprint + +import spray.json.DefaultJsonProtocol + +case class Action(attributes: Attributes, method : String, examples : Seq[Example], parameters : Seq[Parameter]) +case class Ast(name : String, description: String, resourceGroups : Seq[ResourceGroup]) +case class AstHolder(ast : Ast) +case class Attributes(uriTemplate : String) +case class Example(name : String, requests : Seq[Request], responses : Seq[Response]) +case class Parameter(name : String, `type` : String, required : Boolean) +case class Request(schema : String) +case class Resource(uriTemplate : String, actions : Seq[Action]) +case class ResourceGroup(resources : Seq[Resource]) +case class Response(name : String, schema : String) + + +trait AstProtocol extends DefaultJsonProtocol { + implicit val response = jsonFormat2(Response.apply) + implicit val request = jsonFormat1(Request.apply) + implicit val parameter = jsonFormat3(Parameter.apply) + implicit val example = jsonFormat3(Example.apply) + implicit val attributes = jsonFormat1(Attributes.apply) + implicit val action = jsonFormat4(Action.apply) + implicit val resource = jsonFormat2(Resource.apply) + implicit val resourceGroup = jsonFormat1(ResourceGroup.apply) + implicit val ast = jsonFormat3(Ast.apply) + implicit val astHolder = jsonFormat1(AstHolder.apply) +} \ No newline at end of file diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala new file mode 100644 index 0000000..e322986 --- /dev/null +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala @@ -0,0 +1,18 @@ +package com.nike.redwiggler.blueprint + +import com.nike.redwiggler.core.models.{LiteralPathComponent, Path, PathComponent} + +object BlueprintPathParser { + + def apply(path : String) : Path = { + val seq = Path(path).components.reverse + Path(seq.tail.reverse) / cleanupLast(seq.headOption) + } + + def cleanupLast(component : Option[PathComponent]) : Path = component match { + case Some(LiteralPathComponent(path)) if path.contains("{?") => Path(Seq(LiteralPathComponent(path.substring(0, path.indexOf("{?"))))) + case Some(LiteralPathComponent(path)) => Path(Seq(LiteralPathComponent(path))) + case None => Path() + case Some(c) => Path(Seq(c)) + } +} diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala new file mode 100644 index 0000000..f291aef --- /dev/null +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala @@ -0,0 +1,40 @@ +package com.nike.redwiggler.blueprint + +import java.util + +import com.nike.redwiggler.blueprint.parser.BlueprintParser +import com.nike.redwiggler.core.EndpointSpecificationProvider +import com.nike.redwiggler.core.models._ + +import collection.JavaConverters._ + +case class BlueprintSpecificationProvider(blueprint : String, blueprintParser: BlueprintParser) extends EndpointSpecificationProvider { + + private lazy val ast = blueprintParser.parse(blueprint) + + override def getEndPointSpecs: util.List[EndpointSpecification] = (for { + resourceGroup <- ast.resourceGroups + resource <- resourceGroup.resources + action <- resource.actions + example <- action.examples + response <- example.responses + } yield { + EndpointSpecification( + verb = HttpVerb.from(action.method), + path = BlueprintPathParser(action.attributes.uriTemplate), + code = Integer.parseInt(response.name), + responseSchema = Option(response.schema).flatMap(parseSchema), + requestSchema = example.requests.headOption.map(_.schema).flatMap(parseSchema) + ) + }).asJava + + private def parseSchema(schema : String) = if (schema.trim.isEmpty) { + None + } else { + import org.everit.json.schema.loader.SchemaLoader + import org.json.JSONObject + import org.json.JSONTokener + val rawSchema = new JSONObject(new JSONTokener(schema)) + Some(JsonSchema(SchemaLoader.load(rawSchema))) + } +} diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/BlueprintParser.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/BlueprintParser.scala new file mode 100644 index 0000000..ac9f342 --- /dev/null +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/BlueprintParser.scala @@ -0,0 +1,9 @@ +package com.nike.redwiggler.blueprint.parser + +import com.nike.redwiggler.blueprint.Ast + +trait BlueprintParser { + def parse(blueprint : String) : Ast + + def name : String +} diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/DrafterBlueprintParser.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/DrafterBlueprintParser.scala new file mode 100644 index 0000000..3dd224a --- /dev/null +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/DrafterBlueprintParser.scala @@ -0,0 +1,57 @@ +package com.nike.redwiggler.blueprint.parser + +import java.io.{File, FileOutputStream, PrintWriter} +import java.util.concurrent.TimeUnit + +import com.nike.redwiggler.blueprint.{Ast, AstHolder, AstProtocol} +import spray.json.JsonParser + +import scala.io.Source + +object DrafterBlueprintParser extends BlueprintParser with AstProtocol { + + private val LOGGER = org.slf4j.LoggerFactory.getLogger(getClass) + + override def name: String = "drafter" + + override def parse(blueprint: String): Ast = JsonParser(invokeDrafter(blueprint)).convertTo[AstHolder].ast + + private def invokeDrafter(blueprint: String): String = { + val inputFile = File.createTempFile("redwiggler_drafter", ".js") + val inputFileOut = new PrintWriter(new FileOutputStream(inputFile)) + inputFileOut.write(blueprint) + inputFileOut.close() + + val outputFile = File.createTempFile("redwiggler_drafter", ".js") + val process = Runtime.getRuntime.exec(Array(script, + "-o", outputFile.getAbsolutePath, + "-f", "json", + "-t", "ast", + inputFile.getAbsolutePath + )) + process.waitFor(10, TimeUnit.SECONDS) + if (process.exitValue() == 0) { + val x = Source.fromInputStream(process.getInputStream).mkString + LOGGER.debug(x) + Source.fromFile(outputFile).mkString + } else { + LOGGER.error("drafter exit value: " + process.exitValue()) + val x = Source.fromInputStream(process.getInputStream).mkString + LOGGER.error(x) + val error = Source.fromInputStream(process.getErrorStream).mkString + LOGGER.error(error) + throw new RuntimeException("Unable to execute drafter: " + error) + } + } + + private lazy val script = { + val drafter = new File("drafter/bin/drafter") + if (drafter.exists()) { + LOGGER.info("using local drafter: " + drafter.getAbsolutePath) + drafter.getAbsolutePath + } else { + LOGGER.info("using drafter in path") + "drafter" + } + } +} diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/ProtagonistBlueprintParser.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/ProtagonistBlueprintParser.scala new file mode 100644 index 0000000..9e12038 --- /dev/null +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/parser/ProtagonistBlueprintParser.scala @@ -0,0 +1,78 @@ +package com.nike.redwiggler.blueprint.parser + +import java.io.{File, FileOutputStream, PrintWriter} +import java.util.concurrent.TimeUnit + +import com.nike.redwiggler.blueprint.{Ast, AstHolder, AstProtocol} +import spray.json._ + +import scala.io.Source + +case class ProtagonistBlueprintParser(nodePath : Seq[String]) extends BlueprintParser with AstProtocol { + + import ProtagonistBlueprintParser.LOGGER + override def name: String = "protagonist" + + private def invokeNode(blueprint: String): String = { + val script = + """ + |var protagonist = require('protagonist'); + |var options = { + | generateSourceMap: false, + | type: 'ast' + |} + |var blueprint = process.argv[2] + |var fs = require('fs'); + |var blueprintData = fs.readFileSync(blueprint).toString() + |protagonist.parse(blueprintData, options, function(error, result) { + | if (error) { + | console.log(error); + | return; + | } + | + | console.log(JSON.stringify(result)); + |}); + """.stripMargin + + val inputFile = File.createTempFile("redwiggler_protagonist", ".js") + val inputFileOut = new PrintWriter(new FileOutputStream(inputFile)) + inputFileOut.write(blueprint) + inputFileOut.close() + + val sourceFile = File.createTempFile("redwiggler_protagonist", ".js") + val out = new PrintWriter(new FileOutputStream(sourceFile)) + out.write(script) + out.close() + + LOGGER.info("using node path: " + nodePath.mkString(";")) + val process = Runtime.getRuntime.exec(Array("node", sourceFile.getAbsolutePath, inputFile.getAbsolutePath), Array("NODE_PATH=" + nodePath.mkString(";"))) + process.waitFor(10, TimeUnit.SECONDS) + if (process.exitValue() == 0) { + Source.fromInputStream(process.getInputStream).mkString + } else { + LOGGER.error("protagonist exit value: " + process.exitValue()) + val x = Source.fromInputStream(process.getInputStream).mkString + LOGGER.error(x) + val error = Source.fromInputStream(process.getErrorStream).mkString + LOGGER.error(error) + throw new RuntimeException("Unable to execute protagonist: " + error) + } + } + + + override def parse(blueprint : String) : Ast = { + JsonParser(invokeNode(blueprint)).convertTo[AstHolder].ast + } +} + +object ProtagonistBlueprintParser { + private val LOGGER = org.slf4j.LoggerFactory.getLogger(getClass) + + def apply() : ProtagonistBlueprintParser = this( + Seq(System.getProperty("user.home") + "/node_modules", "node_modules") + .map(f => new File(f)) + .filter(_.exists()) + .map(_.getAbsolutePath) + ) +} + diff --git a/blueprint/src/test/resources/com/nike/redwiggler/blueprint/test1.md b/blueprint/src/test/resources/com/nike/redwiggler/blueprint/test1.md new file mode 100644 index 0000000..c1e9f1e --- /dev/null +++ b/blueprint/src/test/resources/com/nike/redwiggler/blueprint/test1.md @@ -0,0 +1,78 @@ +FORMAT: 1A + +# My Api Api + +## Overview +**MyAPI** is a sample. + + +### Search [GET /my/api/v1{?anchor,count,filter}] + ++ Parameters + + anchor (optional, string) - Anchor points to the last element of the previous page. + + count: 100 (optional, string) - total number of index objects to return. Default is 25. + + filter (optional, string) - list of filters. + ++ Request + + + Headers + + Accept: application/json + Authorization: Bearer + ++ Response 200 (application/json; charset=UTF-8) + + + Body + + { + "pages": { + "next": "/my/api/v1?anchor=ad123dq!!2casd&count=5" + }, + "objects": [ + { + "id": "myid", + } + ] + } + + + Schema + + { + "$schema":"http://json-schema.org/draft-04/schema#", + "type":"object", + "properties":{ + "pages":{ + "type":"object", + "properties":{ + "next":{ + "type":"string" + } + }, + "required":[ + "next" + ] + }, + "objects":{ + "type":"array", + "items":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + }, + "foo":{ + "type":"string" + } + }, + "required":[ + "id" + ] + } + } + }, + "required":[ + "pages", + "objects" + ] + } ++ Response 304 diff --git a/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala b/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala new file mode 100644 index 0000000..cb976e1 --- /dev/null +++ b/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala @@ -0,0 +1,16 @@ +package com.nike.redwiggler.blueprint + +import org.scalatest.{FunSpec, Matchers} + +class BlueprintPathParserSpec extends FunSpec with Matchers { + + it("should parse literal path") { + val path = BlueprintPathParser("/my/api/v1") + path.asString should equal ("/my/api/v1") + } + + it("should parse path with query parameters") { + val path = BlueprintPathParser("/my/api/v1{?foo,bar}") + path.asString should equal ("/my/api/v1") + } +} diff --git a/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProviderSpec.scala b/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProviderSpec.scala new file mode 100644 index 0000000..4251a9d --- /dev/null +++ b/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProviderSpec.scala @@ -0,0 +1,63 @@ +package com.nike.redwiggler.blueprint + +import com.nike.redwiggler.blueprint.parser.{DrafterBlueprintParser, ProtagonistBlueprintParser} +import com.nike.redwiggler.core.models._ +import org.everit.json.schema.{ArraySchema, ObjectSchema, StringSchema} +import org.scalatest.{FunSpec, Matchers} + +import scala.io.Source +import collection.JavaConverters._ + +class BlueprintSpecificationProviderSpec extends FunSpec with Matchers { + + for { + parser <- Seq( + ProtagonistBlueprintParser(), + DrafterBlueprintParser + ) + } { + describe(parser.name) { + it("parses simple md") { + val blueprint = Source.fromInputStream(getClass.getResourceAsStream("test1.md")).mkString + val provider = BlueprintSpecificationProvider(blueprint, parser) + val specs = provider.getEndPointSpecs.asScala + + specs should contain only( + EndpointSpecification( + verb = HttpVerb.GET, + path = Path(Seq(LiteralPathComponent("my"), LiteralPathComponent("api"), LiteralPathComponent("v1"))), + code = 200, + responseSchema = Some(JsonSchema( + ObjectSchema.builder() + .addPropertySchema("pages", ObjectSchema.builder() + .addPropertySchema("next", StringSchema.builder().build()) + .addRequiredProperty("next") + .build() + ) + .addPropertySchema("objects", ArraySchema.builder() + .allItemSchema(ObjectSchema.builder() + .addPropertySchema("foo", StringSchema.builder().build()) + .addPropertySchema("id", StringSchema.builder().build()) + .addRequiredProperty("id") + .build() + ) + .build() + ) + .addRequiredProperty("pages") + .addRequiredProperty("objects") + .build() + )), + requestSchema = None + ), + EndpointSpecification( + verb = HttpVerb.GET, + path = Path(Seq(LiteralPathComponent("my"), LiteralPathComponent("api"), LiteralPathComponent("v1"))), + code = 304, + responseSchema = None, + requestSchema = None + ) + ) + } + } + } +} diff --git a/build.sbt b/build.sbt index 0ff3908..67f23b3 100644 --- a/build.sbt +++ b/build.sbt @@ -40,6 +40,15 @@ lazy val swagger = (project in file("swagger")) ) ) +lazy val blueprint = (project in file("blueprint")) + .dependsOn(core) + .settings( + name := "redwiggler-blueprint", + libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % "3.0.1" % "test" + ) + ) + lazy val restassured = (project in file("restassured")) .dependsOn(core) .settings( @@ -62,7 +71,7 @@ lazy val html = (project in file("html")) ) lazy val root = (project in file(".")) - .aggregate(core, swagger, restassured, html) + .aggregate(core, swagger, restassured, html, blueprint) .dependsOn(core, swagger, html) .settings( libraryDependencies ++= Seq( From b9dc98293ba387943b03fa970aeeb6d9ceaa5c7b Mon Sep 17 00:00:00 2001 From: Tyler Southwick Date: Mon, 2 Oct 2017 16:17:41 -0700 Subject: [PATCH 2/5] add integration tests for API.md --- .../blueprint/BlueprintPathParser.scala | 17 +++---- .../BlueprintSpecificationProvider.scala | 8 ++++ .../blueprint/BlueprintPathParserSpec.scala | 20 ++++++++ build.sbt | 2 +- .../integrationtests/blueprint/basic/API.md | 48 +++++++++++++++++++ .../blueprint/basic/expected.json | 20 ++++++++ .../basic/requests.invalidGetResponse.json | 7 +++ .../blueprint/basic/requests.unMatched.json | 6 +++ .../blueprint/basic/requests.validGet.json | 7 +++ .../integrationtests/BlueprintSpec.scala | 11 +++++ .../integrationtests/End2EndSpecBase.scala | 46 ++++++++++++++++++ .../integrationtests/SwaggerSpec.scala | 38 ++------------- 12 files changed, 187 insertions(+), 43 deletions(-) create mode 100644 src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/API.md create mode 100644 src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/expected.json create mode 100644 src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.invalidGetResponse.json create mode 100644 src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.unMatched.json create mode 100644 src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.validGet.json create mode 100644 src/test/scala/com/nike/redwiggler/integrationtests/BlueprintSpec.scala create mode 100644 src/test/scala/com/nike/redwiggler/integrationtests/End2EndSpecBase.scala diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala index e322986..0d91f0c 100644 --- a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintPathParser.scala @@ -1,18 +1,19 @@ package com.nike.redwiggler.blueprint -import com.nike.redwiggler.core.models.{LiteralPathComponent, Path, PathComponent} +import com.nike.redwiggler.core.models.{LiteralPathComponent, Path, PathComponent, PlaceHolderPathComponent} object BlueprintPathParser { def apply(path : String) : Path = { - val seq = Path(path).components.reverse - Path(seq.tail.reverse) / cleanupLast(seq.headOption) + if (path.contains("{?")) { + apply(path.substring(0, path.indexOf("{?"))) + } else { + Path(Path(path).components.map(parseComponent)) + } } - def cleanupLast(component : Option[PathComponent]) : Path = component match { - case Some(LiteralPathComponent(path)) if path.contains("{?") => Path(Seq(LiteralPathComponent(path.substring(0, path.indexOf("{?"))))) - case Some(LiteralPathComponent(path)) => Path(Seq(LiteralPathComponent(path))) - case None => Path() - case Some(c) => Path(Seq(c)) + private def parseComponent(component : PathComponent) : PathComponent = component match { + case LiteralPathComponent(c) if c.startsWith(":") => PlaceHolderPathComponent(c.substring(1)) + case _ => component } } diff --git a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala index f291aef..5834619 100644 --- a/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala +++ b/blueprint/src/main/scala/com/nike/redwiggler/blueprint/BlueprintSpecificationProvider.scala @@ -1,5 +1,6 @@ package com.nike.redwiggler.blueprint +import java.io.File import java.util import com.nike.redwiggler.blueprint.parser.BlueprintParser @@ -7,6 +8,7 @@ import com.nike.redwiggler.core.EndpointSpecificationProvider import com.nike.redwiggler.core.models._ import collection.JavaConverters._ +import scala.io.Source case class BlueprintSpecificationProvider(blueprint : String, blueprintParser: BlueprintParser) extends EndpointSpecificationProvider { @@ -38,3 +40,9 @@ case class BlueprintSpecificationProvider(blueprint : String, blueprintParser: B Some(JsonSchema(SchemaLoader.load(rawSchema))) } } + +object BlueprintSpecificationProvider { + def apply(apiMd: File, blueprintParser: BlueprintParser) : BlueprintSpecificationProvider = { + BlueprintSpecificationProvider(Source.fromFile(apiMd).mkString, blueprintParser) + } +} diff --git a/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala b/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala index cb976e1..ade6b24 100644 --- a/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala +++ b/blueprint/src/test/scala/com/nike/redwiggler/blueprint/BlueprintPathParserSpec.scala @@ -9,8 +9,28 @@ class BlueprintPathParserSpec extends FunSpec with Matchers { path.asString should equal ("/my/api/v1") } + it("should parse literal base path") { + val path = BlueprintPathParser("/") + path.asString should equal ("/") + } + it("should parse path with query parameters") { val path = BlueprintPathParser("/my/api/v1{?foo,bar}") path.asString should equal ("/my/api/v1") } + + it("should parse path with path parameters") { + val path = BlueprintPathParser("/my/api/:id") + path.asString should equal ("/my/api/{id}") + } + + it("should parse path with path parameters in middle") { + val path = BlueprintPathParser("/my/api/:id/v1") + path.asString should equal ("/my/api/{id}/v1") + } + + it("should parse path with path and query parameters") { + val path = BlueprintPathParser("/my/api/:id{?foo,bar}") + path.asString should equal ("/my/api/{id}") + } } diff --git a/build.sbt b/build.sbt index e78ae80..483fd1f 100644 --- a/build.sbt +++ b/build.sbt @@ -78,7 +78,7 @@ lazy val root = (project in file(".")) .aggregate(core, swagger, restassured, html, blueprint) .enablePlugins(ReadmeTests) .settings(ReadmeTests.projectSettings) - .dependsOn(core, swagger, html) + .dependsOn(core, swagger, html, blueprint) .settings( testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-h", new File(target.value, "/test-reports-html").getAbsolutePath) ) diff --git a/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/API.md b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/API.md new file mode 100644 index 0000000..d43d4aa --- /dev/null +++ b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/API.md @@ -0,0 +1,48 @@ +FORMAT: 1A + +# My Api Api + +## Overview +**MyAPI** is a sample. + + +### GetItem [GET /my/resource/v2/:id] + ++ Response 200 (application/json; charset=UTF-8) + + + Schema + + { + "$schema":"http://json-schema.org/draft-04/schema#", + "type":"object", + "properties":{ + "name":{ + "type":"string", + }, + "id":{ + "type":"string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + } + } + +### CreateItem [POST /my/resource/v2] + ++ Request + + + Schema + + { + "$schema":"http://json-schema.org/draft-04/schema#", + "type":"object", + "properties":{ + "name":{ + "type":"string", + }, + "id":{ + "type":"string", + }, + } + } + ++ Response 201 (application/json; charset=UTF-8) diff --git a/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/expected.json b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/expected.json new file mode 100644 index 0000000..27bb3b2 --- /dev/null +++ b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/expected.json @@ -0,0 +1,20 @@ +[ + { + "verb": "POST", + "path": "/my/resource/v2", + "passed": 0, + "total": 1 + }, + { + "verb": "GET", + "path": "/foobar", + "passed": 0, + "total": 1 + }, + { + "verb": "GET", + "path": "/my/resource/v2/{id}", + "passed": 1, + "total": 2 + } +] diff --git a/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.invalidGetResponse.json b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.invalidGetResponse.json new file mode 100644 index 0000000..397ecd9 --- /dev/null +++ b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.invalidGetResponse.json @@ -0,0 +1,7 @@ +{ + "verb": "GET", + "path": "/my/resource/v2/25", + "responseHeaders": [], + "responseBody": "{\"id\": \"25\", \"name\": \"foo\"}", + "code": 200 +} \ No newline at end of file diff --git a/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.unMatched.json b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.unMatched.json new file mode 100644 index 0000000..c0a2ce7 --- /dev/null +++ b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.unMatched.json @@ -0,0 +1,6 @@ +{ + "verb": "GET", + "path": "/foobar", + "responseHeaders": [], + "code": 200 +} \ No newline at end of file diff --git a/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.validGet.json b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.validGet.json new file mode 100644 index 0000000..52849d9 --- /dev/null +++ b/src/test/resources/com/nike/redwiggler/integrationtests/blueprint/basic/requests.validGet.json @@ -0,0 +1,7 @@ +{ + "verb": "GET", + "path": "/my/resource/v2/53b78f35-2fbb-4207-9777-1b9aa1a05327", + "responseHeaders": [], + "responseBody": "{\"id\": \"53b78f35-2fbb-4207-9777-1b9aa1a05327\", \"name\": \"foo\"}", + "code": 200 +} \ No newline at end of file diff --git a/src/test/scala/com/nike/redwiggler/integrationtests/BlueprintSpec.scala b/src/test/scala/com/nike/redwiggler/integrationtests/BlueprintSpec.scala new file mode 100644 index 0000000..8f011cf --- /dev/null +++ b/src/test/scala/com/nike/redwiggler/integrationtests/BlueprintSpec.scala @@ -0,0 +1,11 @@ +package com.nike.redwiggler.integrationtests +import java.io.File + +import com.nike.redwiggler.blueprint.BlueprintSpecificationProvider +import com.nike.redwiggler.blueprint.parser.ProtagonistBlueprintParser +import com.nike.redwiggler.core.EndpointSpecificationProvider + +class BlueprintSpec extends End2EndSpecBase("blueprint") { + override def specificationProvider(testDir: File): EndpointSpecificationProvider = + BlueprintSpecificationProvider(new File(testDir, "API.md"), ProtagonistBlueprintParser()) +} diff --git a/src/test/scala/com/nike/redwiggler/integrationtests/End2EndSpecBase.scala b/src/test/scala/com/nike/redwiggler/integrationtests/End2EndSpecBase.scala new file mode 100644 index 0000000..e04adba --- /dev/null +++ b/src/test/scala/com/nike/redwiggler/integrationtests/End2EndSpecBase.scala @@ -0,0 +1,46 @@ +package com.nike.redwiggler.integrationtests + + +import java.util + +import com.nike.redwiggler.core._ +import com.nike.redwiggler.core.models.RedwigglerReport +import com.nike.redwiggler.html.HtmlReportProcessor +import org.scalatest.{FunSpec, Matchers} +import spray.json.{DefaultJsonProtocol, JsonParser} + +import scala.io.Source + +abstract class End2EndSpecBase(name : String) extends FunSpec with Matchers with DefaultJsonProtocol { + + import java.io._ + + private val testDirs = new File(getClass.getResource(name).getFile).listFiles() + + def specificationProvider(testDir: File): EndpointSpecificationProvider + + for { + testDir <- testDirs + } { + it(s"should process ${testDir.getName}") { + Redwiggler( + callProvider = new GlobEndpointCallProvider(testDir, "requests.*.json"), + specificationProvider = specificationProvider(testDir), + reportProcessor = new ReportProcessor { + val file = File.createTempFile("redwiggler", ".json") + val writer = JsonReportProcessor(file) + + override def process(reports: util.List[RedwigglerReport]): Unit = { + writer.process(reports) + + val expected = JsonParser(Source.fromFile(new File(testDir, "expected.json")).mkString).convertTo[Seq[RedwigglerReportDetails]] + val actual = JsonParser(Source.fromFile(file).mkString).convertTo[Seq[RedwigglerReportDetails]] + + expected should contain theSameElementsAs actual + } + }.andThen(HtmlReportProcessor(new File("/tmp/" + testDir.getName + ".html"))) + ) + } + } + +} diff --git a/src/test/scala/com/nike/redwiggler/integrationtests/SwaggerSpec.scala b/src/test/scala/com/nike/redwiggler/integrationtests/SwaggerSpec.scala index ec95d66..9511966 100644 --- a/src/test/scala/com/nike/redwiggler/integrationtests/SwaggerSpec.scala +++ b/src/test/scala/com/nike/redwiggler/integrationtests/SwaggerSpec.scala @@ -1,42 +1,12 @@ package com.nike.redwiggler.integrationtests -import java.util +import java.io.File -import collection.JavaConverters._ -import com.nike.redwiggler.core.models.RedwigglerReport import com.nike.redwiggler.core._ -import com.nike.redwiggler.html.HtmlReportProcessor import com.nike.redwiggler.swagger.SwaggerEndpointSpecificationProvider -import org.scalatest.{FunSpec, Matchers} -import spray.json.{DefaultJsonProtocol, JsonParser} -import scala.io.Source +class SwaggerSpec extends End2EndSpecBase("swagger") { -class SwaggerSpec extends FunSpec with Matchers with DefaultJsonProtocol { - - import java.io._ - private val testDirs = new File(getClass.getResource("swagger").getFile).listFiles() - - for { - testDir <- testDirs - } { - it(s"should process ${testDir.getName}") { - Redwiggler( - callProvider = new GlobEndpointCallProvider(testDir, "requests.*.json"), - specificationProvider = SwaggerEndpointSpecificationProvider(new File(testDir, "swagger.yaml")), - reportProcessor = new ReportProcessor { - val file = File.createTempFile("redwiggler", ".json") - val writer = JsonReportProcessor(file) - override def process(reports: util.List[RedwigglerReport]): Unit = { - writer.process(reports) - - val expected = JsonParser(Source.fromFile(new File(testDir, "expected.json")).mkString).convertTo[Seq[RedwigglerReportDetails]] - val actual = JsonParser(Source.fromFile(file).mkString).convertTo[Seq[RedwigglerReportDetails]] - - expected should contain theSameElementsAs actual - } - }.andThen(HtmlReportProcessor(new File("/tmp/" + testDir.getName + ".html" ))) - ) - } - } + override def specificationProvider(testDir: File): EndpointSpecificationProvider = + SwaggerEndpointSpecificationProvider(new File(testDir, "swagger.yaml")) } From 6b26d76c36c8ba88f22855af2c511fe6ddf1dde7 Mon Sep 17 00:00:00 2001 From: Tyler Southwick Date: Mon, 2 Oct 2017 16:26:09 -0700 Subject: [PATCH 3/5] add blueprint README.md --- README.md | 1 + blueprint/README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++ build.sbt | 3 ++ 3 files changed, 74 insertions(+) create mode 100644 blueprint/README.md diff --git a/README.md b/README.md index b85fb9d..2013271 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ RedWiggler has 3 main components: 1. EndpointSpecifications: defines the endpoint contract. Defined from the API documentation * Swagger Support: [ ![Download](https://api.bintray.com/packages/nike/maven/redwiggler-swagger/images/download.svg) ](https://bintray.com/nike/maven/redwiggler-swagger/_latestVersion) + * [Blueprint Support](blueprint/README.md): [ ![Download](https://api.bintray.com/packages/nike/maven/redwiggler-blueprint/images/download.svg) ](https://bintray.com/nike/maven/redwiggler-blueprint/_latestVersion) 2. EndpointCall: An instance of a request/response to the service to be matched against the EndpointSpecification 3. ReportProcessor: Takes the result of the analysis and generates output (such as an html page) diff --git a/blueprint/README.md b/blueprint/README.md new file mode 100644 index 0000000..0c77be1 --- /dev/null +++ b/blueprint/README.md @@ -0,0 +1,70 @@ +# Redwiggler API.md Blueprint support + +# API.md parsers +## Protagonist +To use the [protagonist](https://github.com/apiaryio/protagonist) parser, it must be installed first via npm: + +```shell +npm install protagonist +``` + +```scala +import com.nike.redwiggler.core._ +import com.nike.redwiggler.blueprint._ +import com.nike.redwiggler.html._ + +val blueprint = BlueprintSpecificationProvider( + """ + | # My Api Api + | + | ## Overview + | **MyAPI** is a sample. + | + | ### Search [GET /my/api/v1{?anchor,count,filter}] + """.stripMargin, parser.ProtagonistBlueprintParser()) + +import java.io._ +val requestDir = File.createTempFile("redwiggler", "requests") +requestDir.delete() +requestDir.mkdir() +val requests = GlobEndpointCallProvider(requestDir, ".*.json") + +val htmlReportFile = File.createTempFile("redwiggler", ".html") +val htmlRender = HtmlReportProcessor(htmlReportFile) + +Redwiggler(callProvider = requests, specificationProvider = blueprint , reportProcessor = htmlRender) + +htmlReportFile.exists() should be(true) +``` + +## Drafter +The [drafter](https://github.com/apiaryio/drafter) cli can also be used. This must be installed as well and available on the PATH. + +```scala +import com.nike.redwiggler.core._ +import com.nike.redwiggler.blueprint._ +import com.nike.redwiggler.html._ + +val blueprint = BlueprintSpecificationProvider( + """ + | # My Api Api + | + | ## Overview + | **MyAPI** is a sample. + | + | ### Search [GET /my/api/v1{?anchor,count,filter}] + """.stripMargin, parser.DrafterBlueprintParser) + +import java.io._ +val requestDir = File.createTempFile("redwiggler", "requests") +requestDir.delete() +requestDir.mkdir() +val requests = GlobEndpointCallProvider(requestDir, ".*.json") + +val htmlReportFile = File.createTempFile("redwiggler", ".html") +val htmlRender = HtmlReportProcessor(htmlReportFile) + +Redwiggler(callProvider = requests, specificationProvider = blueprint , reportProcessor = htmlRender) + +htmlReportFile.exists() should be(true) +``` diff --git a/build.sbt b/build.sbt index 483fd1f..ccb0645 100644 --- a/build.sbt +++ b/build.sbt @@ -48,6 +48,9 @@ lazy val swagger = (project in file("swagger")) lazy val blueprint = (project in file("blueprint")) .dependsOn(core) + .dependsOn(html % "compile->test") + .enablePlugins(ReadmeTests) + .settings(ReadmeTests.projectSettings) .settings( name := "redwiggler-blueprint", libraryDependencies ++= Seq( From 552d31c8fe2f5f83ba52ea268fb6173d7de07b95 Mon Sep 17 00:00:00 2001 From: Tyler Southwick Date: Mon, 2 Oct 2017 16:26:22 -0700 Subject: [PATCH 4/5] update version --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 7b689f1..3f478e0 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "0.5.4-SNAPSHOT" +version in ThisBuild := "0.6.0-SNAPSHOT" From 83f28b8342a09207ae861b88d0c464792efe4a31 Mon Sep 17 00:00:00 2001 From: Tyler Southwick Date: Tue, 3 Oct 2017 08:32:26 -0700 Subject: [PATCH 5/5] add test options for blueprint --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index ccb0645..c444097 100644 --- a/build.sbt +++ b/build.sbt @@ -53,6 +53,7 @@ lazy val blueprint = (project in file("blueprint")) .settings(ReadmeTests.projectSettings) .settings( name := "redwiggler-blueprint", + testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-h", new File(target.value, "/test-reports-html").getAbsolutePath), libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.0.1" % "test" )