Skip to content

Commit

Permalink
Merge 1acbffc into e72a7b3
Browse files Browse the repository at this point in the history
  • Loading branch information
tylersouthwick committed Oct 3, 2017
2 parents e72a7b3 + 1acbffc commit d043715
Show file tree
Hide file tree
Showing 24 changed files with 663 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
target
drafter
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ language: scala
scala:
- 2.12.3

before_script:
- "npm install protagonist"
- "./blueprint/setup_drafter"
script:
- "sbt +test"
- "sbt clean coverage test coverageReport"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
70 changes: 70 additions & 0 deletions blueprint/README.md
Original file line number Diff line number Diff line change
@@ -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)
```
7 changes: 7 additions & 0 deletions blueprint/setup_drafter
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions blueprint/src/main/scala/com/nike/redwiggler/blueprint/Ast.scala
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.nike.redwiggler.blueprint

import com.nike.redwiggler.core.models.{LiteralPathComponent, Path, PathComponent, PlaceHolderPathComponent}

object BlueprintPathParser {

def apply(path : String) : Path = {
if (path.contains("{?")) {
apply(path.substring(0, path.indexOf("{?")))
} else {
Path(Path(path).components.map(parseComponent))
}
}

private def parseComponent(component : PathComponent) : PathComponent = component match {
case LiteralPathComponent(c) if c.startsWith(":") => PlaceHolderPathComponent(c.substring(1))
case _ => component
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.nike.redwiggler.blueprint

import java.io.File
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._
import scala.io.Source

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)))
}
}

object BlueprintSpecificationProvider {
def apply(apiMd: File, blueprintParser: BlueprintParser) : BlueprintSpecificationProvider = {
BlueprintSpecificationProvider(Source.fromFile(apiMd).mkString, blueprintParser)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
)
}

0 comments on commit d043715

Please sign in to comment.