-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adding support for environment variables as markup (#151)
* adding support for environment variables as markup Fixes #149 * adding build variable to use output transformers * adding docs regarding output transformers * adding key value for environment output transformer * improving docs
- Loading branch information
1 parent
f17c236
commit a01def3
Showing
10 changed files
with
308 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
core/src/main/scala/com/iheart/playSwagger/OutputTransformer.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package com.iheart.playSwagger | ||
|
||
import java.util.regex.Pattern | ||
|
||
import com.iheart.playSwagger.OutputTransformer.SimpleOutputTransformer | ||
import play.api.libs.json.{JsArray, JsString, JsValue, JsObject} | ||
|
||
import scala.util.matching.Regex | ||
import scala.util.{Success, Failure, Try} | ||
|
||
/** Specialization of a Kleisli function (A => M[B])*/ | ||
trait OutputTransformer extends (JsObject ⇒ Try[JsObject]) { | ||
|
||
/** alias for `andThen` as defined monadic function */ | ||
def >=>(b: JsObject ⇒ Try[JsObject]): OutputTransformer = SimpleOutputTransformer { value: JsObject ⇒ | ||
this.apply(value).flatMap(b) | ||
} | ||
} | ||
|
||
object OutputTransformer { | ||
final case class SimpleOutputTransformer(run: (JsObject ⇒ Try[JsObject])) extends OutputTransformer { | ||
override def apply(value: JsObject): Try[JsObject] = run(value) | ||
} | ||
|
||
def traverseTransformer(vals: JsArray)(transformer: JsValue ⇒ Try[JsValue]): Try[JsArray] = { | ||
val tryElements = vals.value.map { | ||
case value: JsObject ⇒ traverseTransformer(value)(transformer) | ||
case value: JsArray ⇒ traverseTransformer(value)(transformer) | ||
case value: JsValue ⇒ transformer(value) | ||
} | ||
|
||
val failures: Seq[Failure[JsValue]] = tryElements.filter(_.isInstanceOf[Failure[_]]).asInstanceOf[Seq[Failure[JsValue]]] | ||
if (failures.nonEmpty) { | ||
Failure(failures.head.exception) | ||
} else { | ||
Success(JsArray(tryElements.asInstanceOf[Seq[Success[JsValue]]].map(_.value))) | ||
} | ||
} | ||
|
||
def traverseTransformer(obj: JsObject)(transformer: JsValue ⇒ Try[JsValue]): Try[JsObject] = { | ||
val tryFields = obj.fields.map { | ||
case (key, value: JsObject) ⇒ (key, traverseTransformer(value)(transformer)) | ||
case (key, values: JsArray) ⇒ (key, traverseTransformer(values)(transformer)) | ||
case (key, value: JsValue) ⇒ (key, transformer(value)) | ||
} | ||
val failures: Seq[(String, Failure[JsValue])] = tryFields | ||
.filter(_._2.isInstanceOf[Failure[_]]) | ||
.asInstanceOf[Seq[(String, Failure[JsValue])]] | ||
if (failures.nonEmpty) { | ||
Failure(failures.head._2.exception) | ||
} else { | ||
Success(JsObject(tryFields.asInstanceOf[Seq[(String, Success[JsValue])]].map { | ||
case (key, Success(result)) ⇒ (key, result) | ||
})) | ||
} | ||
} | ||
} | ||
|
||
class PlaceholderVariablesTransformer(map: String ⇒ Option[String], pattern: Regex = "^\\$\\{(.*)\\}$".r) extends OutputTransformer { | ||
def apply(value: JsObject) = OutputTransformer.traverseTransformer(value) { | ||
case JsString(pattern(key)) ⇒ map(key) match { | ||
case Some(result) ⇒ Success(JsString(result)) | ||
case None ⇒ Failure(new IllegalStateException(s"Unable to find variable $key")) | ||
} | ||
case e: JsValue ⇒ Success(e) | ||
} | ||
} | ||
|
||
final case class MapVariablesTransformer(map: Map[String, String]) extends PlaceholderVariablesTransformer(map.get) | ||
class EnvironmentVariablesTransformer extends PlaceholderVariablesTransformer((key: String) ⇒ Option(System.getenv(key))) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
### | ||
# summary: last track | ||
# description: big deal | ||
# parameters: | ||
# - name: sid | ||
# description: station id | ||
# format: int | ||
# responses: | ||
# 200: | ||
# description: ${LAST_TRACK_DESCRIPTION} | ||
# schema: | ||
# $ref: '#/definitions/com.iheart.playSwagger.Track' | ||
### | ||
GET /api/station/:sid/playedTracks/last @controllers.LiveMeta.playedByStation(sid: Int) | ||
|
||
### | ||
# summary: Add track | ||
# parameters: | ||
# - name: body | ||
# description: ${PLAYED_TRACKS_DESCRIPTION} | ||
# schema: | ||
# $ref: '#/definitions/com.iheart.playSwagger.Track' | ||
# responses: | ||
# 200: | ||
# description: success | ||
### | ||
POST /api/station/playedTracks controllers.LiveMeta.addPlayedTracks() |
150 changes: 150 additions & 0 deletions
150
core/src/test/scala/com/iheart/playSwagger/OutputTransformerSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package com.iheart.playSwagger | ||
|
||
import com.iheart.playSwagger.OutputTransformer.SimpleOutputTransformer | ||
import org.specs2.mutable.Specification | ||
import play.api.libs.json.{JsNumber, JsObject, JsString, Json} | ||
|
||
import scala.util.{Failure, Success} | ||
|
||
class OutputTransformerSpec extends Specification { | ||
"OutputTransformer.traverseTransformer" >> { | ||
|
||
"traverse and transform object and update simple paths" >> { | ||
val result = OutputTransformer.traverseTransformer(Json.obj( | ||
"a" → 1, | ||
"b" → "c" | ||
)) { _ ⇒ Success(JsNumber(10)) } | ||
result === Success(Json.obj("a" → 10, "b" → 10)) | ||
} | ||
|
||
"traverse and transform object and update nested paths" >> { | ||
val result = OutputTransformer.traverseTransformer(Json.obj( | ||
"a" → 1, | ||
"b" → Json.obj( | ||
"c" → 1 | ||
) | ||
)) { _ ⇒ Success(JsNumber(10)) } | ||
result === Success(Json.obj("a" → 10, "b" → Json.obj("c" → 10))) | ||
} | ||
|
||
"traverse and transform object and update array paths" >> { | ||
val result = OutputTransformer.traverseTransformer(Json.obj( | ||
"a" → 1, | ||
"b" → Json.arr( | ||
Json.obj("c" → 1), | ||
Json.obj("d" → 1), | ||
Json.obj("e" → 1) | ||
) | ||
)) { _ ⇒ Success(JsNumber(10)) } | ||
result === Success(Json.obj("a" → 10, "b" → Json.arr( | ||
Json.obj("c" → 10), | ||
Json.obj("d" → 10), | ||
Json.obj("e" → 10) | ||
))) | ||
} | ||
|
||
"return a failure when there's a problem transforming data" >> { | ||
val err: IllegalArgumentException = new scala.IllegalArgumentException("failed") | ||
val result = OutputTransformer.traverseTransformer(Json.obj( | ||
"a" → 1, | ||
"b" → Json.obj( | ||
"c" → 1 | ||
) | ||
)) { _ ⇒ Failure(err) } | ||
result === Failure(err) | ||
} | ||
} | ||
"OutputTransformer.>=>" >> { | ||
"return composed function" >> { | ||
val a = SimpleOutputTransformer(OutputTransformer.traverseTransformer(_) { | ||
case JsString(content) ⇒ Success(JsString(content + "a")) | ||
case _ ⇒ Failure(new IllegalStateException()) | ||
}) | ||
val b = SimpleOutputTransformer(OutputTransformer.traverseTransformer(_) { | ||
case JsString(content) ⇒ Success(JsString(content + "b")) | ||
case _ ⇒ Failure(new IllegalStateException()) | ||
}) | ||
|
||
val g = a >=> b | ||
g(Json.obj( | ||
"A" → "Z", | ||
"B" → "Y" | ||
)) must beSuccessfulTry.withValue(Json.obj( | ||
"A" → "Zab", | ||
"B" → "Yab" | ||
)) | ||
} | ||
|
||
"fail if one composed function fails" >> { | ||
val a = SimpleOutputTransformer(OutputTransformer.traverseTransformer(_) { | ||
case JsString(content) ⇒ Success(JsString("a" + content)) | ||
case _ ⇒ Failure(new IllegalStateException()) | ||
}) | ||
val b = SimpleOutputTransformer(OutputTransformer.traverseTransformer(_) { | ||
case JsString(content) ⇒ Failure(new IllegalStateException("not strings")) | ||
case _ ⇒ Failure(new IllegalStateException()) | ||
}) | ||
|
||
val g = a >=> b | ||
g(Json.obj( | ||
"A" → "Z", | ||
"B" → "Y" | ||
)) must beFailedTry[JsObject].withThrowable[IllegalStateException]("not strings") | ||
} | ||
} | ||
} | ||
|
||
class EnvironmentVariablesSpec extends Specification { | ||
"EnvironmentVariables" >> { | ||
"transform json with markup values" >> { | ||
val envs = Map("A" → "B", "C" → "D") | ||
val instance = MapVariablesTransformer(envs) | ||
instance(Json.obj( | ||
"a" → "${A}", | ||
"b" → Json.obj( | ||
"c" → "${C}" | ||
) | ||
)) === Success(Json.obj("a" → "B", "b" → Json.obj("c" → "D"))) | ||
} | ||
|
||
"return failure when using non present environment variables" >> { | ||
val envs = Map("A" → "B", "C" → "D") | ||
val instance = MapVariablesTransformer(envs) | ||
instance(Json.obj( | ||
"a" → "${A}", | ||
"b" → Json.obj( | ||
"c" → "${NON_EXISTING}" | ||
) | ||
)) must beFailedTry[JsObject].withThrowable[IllegalStateException]("Unable to find variable NON_EXISTING") | ||
} | ||
} | ||
} | ||
|
||
class EnvironmentVariablesIntegrationSpec extends Specification { | ||
implicit val cl = getClass.getClassLoader | ||
|
||
"integration" >> { | ||
"generate api with placeholders in place" >> { | ||
val envs = Map("LAST_TRACK_DESCRIPTION" → "Last track", "PLAYED_TRACKS_DESCRIPTION" → "Add tracks") | ||
val json = SwaggerSpecGenerator( | ||
PrefixDomainModelQualifier("com.iheart"), | ||
outputTransformers = MapVariablesTransformer(envs) :: Nil | ||
).generate("env.routes").get | ||
val pathJson = json \ "paths" | ||
val stationJson = (pathJson \ "/api/station/{sid}/playedTracks/last" \ "get").as[JsObject] | ||
val addTrackJson = (pathJson \ "/api/station/playedTracks" \ "post").as[JsObject] | ||
|
||
(addTrackJson \ "parameters" \ (0) \ "description").as[String] === "Add tracks" | ||
(stationJson \ "responses" \ "200" \ "description").as[String] === "Last track" | ||
} | ||
} | ||
|
||
"fail to generate API if environment variable is not found" >> { | ||
val envs = Map("LAST_TRACK_DESCRIPTION" → "Last track") | ||
val json = SwaggerSpecGenerator( | ||
PrefixDomainModelQualifier("com.iheart"), | ||
outputTransformers = MapVariablesTransformer(envs) :: Nil | ||
).generate("env.routes") | ||
json must beFailedTry[JsObject].withThrowable[IllegalStateException]("Unable to find variable PLAYED_TRACKS_DESCRIPTION") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters