From 43602bcda8c32f716fce7c422eaf1aa2d9099a64 Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Fri, 23 Jan 2015 11:59:52 +0900 Subject: [PATCH 01/17] Bump minor versions --- project/Dependencies.scala | 47 ++++++++++++++++---------------------- project/plugins.sbt | 4 ++-- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 663d7fe6..d11dec88 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,29 +5,20 @@ import play.Play.autoImport._ object Dependencies { val resolverSettings = { - // Use in-house Maven repo instead of Maven central if env var is set - sys.env.get("INHOUSE_MAVEN_REPO").fold[Seq[sbt.Def.Setting[_]]] { - Seq( - resolvers += "Typesafe Releases" at "http://repo.typesafe.com/typesafe/releases/", - resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" + // Use in-house Maven repo before other remote repos if env var is set + resolvers ++= Seq(Resolver.defaultLocal) ++ sys.env.get("INHOUSE_MAVEN_REPO").map("Inhouse".at) ++ Seq( + Resolver.typesafeRepo("releases"), + Resolver.sonatypeRepo("snapshots") ) - } { inhouse => - Seq( - // Speed up resolution using local Maven cache - resolvers += "Local Maven" at Path.userHome.asFile.toURI.toURL + ".m2/repository", - resolvers += "Inhouse" at inhouse, - externalResolvers := Resolver.withDefaultResolvers(resolvers.value, mavenCentral = false) - ) - } } val thePlayVersion = play.core.PlayVersion.current - val slf4jVersion = "1.7.7" - val hystrixVersion = "1.3.19" + val slf4jVersion = "1.7.10" + val hystrixVersion = "1.3.20" val httpClientVersion = "4.3.6" - val scalikejdbcVersion = "2.2.1" + val scalikejdbcVersion = "2.2.2" val swaggerVersion = "1.3.12" - val jacksonVersion = "2.4.4" + val jacksonVersion = "2.5.0" // Logging val logbackClassic = "ch.qos.logback" % "logback-classic" % "1.1.2" @@ -35,7 +26,7 @@ object Dependencies { val jclOverSlf4j = "org.slf4j" % "jcl-over-slf4j" % slf4jVersion val log4jOverSlf4j = "org.slf4j" % "log4j-over-slf4j" % slf4jVersion val julToSlf4j = "org.slf4j" % "jul-to-slf4j" % slf4jVersion - val ravenLogback = "net.kencochrane.raven" % "raven-logback" % "5.0.1" % Runtime + val ravenLogback = "net.kencochrane.raven" % "raven-logback" % "5.0.2" % Runtime val janino = "org.codehaus.janino" % "janino" % "2.7.7" val ltsvLogger = "com.beachape" %% "ltsv-logger" % "0.0.8" @@ -45,26 +36,27 @@ object Dependencies { val rxJavaScala = "com.netflix.rxjava" % "rxjava-scala" % "0.20.3" // matches version used in hystrix-core // HTTP clients - val asyncHttpClient = "com.ning" % "async-http-client" % "1.9.3" + val asyncHttpClient = "com.ning" % "async-http-client" % "1.9.6" val httpClient = "org.apache.httpcomponents" % "httpclient" % httpClientVersion val httpClientCache = "org.apache.httpcomponents" % "httpclient-cache" % httpClientVersion val metricsHttpClient = "io.dropwizard.metrics" % "metrics-httpclient" % "3.1.0" // DB val postgres = "org.postgresql" % "postgresql" % "9.3-1102-jdbc41" % Runtime - val skinnyOrm = "org.skinny-framework" %% "skinny-orm" % "1.3.8" + val skinnyOrm = "org.skinny-framework" %% "skinny-orm" % "1.3.10" val scalikeJdbc = "org.scalikejdbc" %% "scalikejdbc" % scalikejdbcVersion - val scalikeJdbcPlay = "org.scalikejdbc" %% "scalikejdbc-play-plugin" % "2.3.4" + val scalikeJdbcConfig = "org.scalikejdbc" %% "scalikejdbc-config" % scalikejdbcVersion + val scalikeJdbcPlay = "org.scalikejdbc" %% "scalikejdbc-play-plugin" % "2.3.5" val dbcp2 = "org.apache.commons" % "commons-dbcp2" % "2.0.1" // Memcached val shade = "com.bionicspirit" %% "shade" % "1.6.0" - val spyMemcached = "net.spy" % "spymemcached" % "2.11.5" + val spyMemcached = "net.spy" % "spymemcached" % "2.11.6" // Play plugins - val playFlyway = "com.github.tototoshi" %% "play-flyway" % "1.2.0" + val playFlyway = "com.github.tototoshi" %% "play-flyway" % "1.2.1" val scaldiPlay = "org.scaldi" %% "scaldi-play" % "0.4.1" - val metricsPlay = "com.kenshoo" %% "metrics-play" % "2.3.0_0.1.7" + val metricsPlay = "com.kenshoo" %% "metrics-play" % "2.3.0_0.1.8" val providedPlay = "com.typesafe.play" %% "play" % thePlayVersion % Provided // Swagger @@ -81,13 +73,13 @@ object Dependencies { val scalatest = "org.scalatest" %% "scalatest" % "2.2.3" % Test val scalatestPlay = "org.scalatestplus" %% "play" % "1.2.0" % Test val scalacheck = "org.scalacheck" %% "scalacheck" % "1.12.1" % Test - val groovy = "org.codehaus.groovy" % "groovy" % "2.3.9" % Test + val groovy = "org.codehaus.groovy" % "groovy" % "2.4.0" % Test val scalikeJdbcTest = "org.scalikejdbc" %% "scalikejdbc-test" % scalikejdbcVersion % Test // Misc utils - val commonsValidator = "commons-validator" % "commons-validator" % "1.4.0" % Runtime + val commonsValidator = "commons-validator" % "commons-validator" % "1.4.1" % Runtime val jta = "javax.transaction" % "jta" % "1.1" - val scalaUri = "com.netaporter" %% "scala-uri" % "0.4.4" + val scalaUri = "com.netaporter" %% "scala-uri" % "0.4.5" val findbugs = "com.google.code.findbugs" % "jsr305" % "3.0.0" val withoutExcluded = { (m: ModuleID) => @@ -124,6 +116,7 @@ object Dependencies { postgres, skinnyOrm, scalikeJdbc, + scalikeJdbcConfig, scalikeJdbcPlay, dbcp2, diff --git a/project/plugins.sbt b/project/plugins.sbt index 6b0fbfd6..a4eb0c28 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ // The Typesafe repository resolvers ++= Seq( - "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/", + Resolver.typesafeRepo("releases"), "jgit-repo" at "http://download.eclipse.org/jgit/maven", Classpaths.sbtPluginReleases ) @@ -8,7 +8,7 @@ resolvers ++= Seq( // The Play plugin addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.7") // scoverage for test coverage -addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "1.0.1") +addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "1.0.2") // to show transitive dependencies as a graph addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.4") // for code formatting From b44a3787b92c3cacb3a6f804cc950e5f645c551e Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Fri, 23 Jan 2015 12:24:17 +0900 Subject: [PATCH 02/17] scala-uri behavior changed slightly: https://github.com/NET-A-PORTER/scala-uri/issues/76 --- .../aggregator/handler/HttpPartRequestHandlerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala b/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala index 3d494946..b0ca4d88 100644 --- a/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala +++ b/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala @@ -50,7 +50,7 @@ class HttpPartRequestHandlerSpec extends FunSpec with Matchers with ScalaFutures output.host shouldBe Some("mock.com") output.protocol shouldBe Some("http") output.path shouldBe "/hi/there" - output.query.params(queryParam1.outputName) shouldBe Seq("scala") + output.query.params(queryParam1.outputName) shouldBe Seq(Some("scala")) output.query.params(queryParam2.outputName).toSet shouldBe Set("lover", "lover2") } } From c384c4037d53976f97fb566acadf58765f11b749 Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Fri, 23 Jan 2015 12:53:45 +0900 Subject: [PATCH 03/17] Also upgrade rxscala --- .../com/m3/octoparts/client/OctopartsApiBuilderTest.scala | 4 +++- project/Dependencies.scala | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/java-client/src/test/scala/com/m3/octoparts/client/OctopartsApiBuilderTest.scala b/java-client/src/test/scala/com/m3/octoparts/client/OctopartsApiBuilderTest.scala index eaed2545..ad4a9ac3 100644 --- a/java-client/src/test/scala/com/m3/octoparts/client/OctopartsApiBuilderTest.scala +++ b/java-client/src/test/scala/com/m3/octoparts/client/OctopartsApiBuilderTest.scala @@ -2,6 +2,7 @@ package com.m3.octoparts.client import java.nio.charset.StandardCharsets +import com.fasterxml.jackson.databind.ObjectReader import com.m3.octoparts.model.config.ParamType import com.m3.octoparts.model.{ HttpMethod, AggregateRequest } import com.m3.octoparts.model.config.json.HttpPartConfig @@ -60,7 +61,8 @@ class OctopartsApiBuilderTest extends FunSpec with BeforeAndAfterAll with Matche | "alertPercentThreshold": 5, | "alertInterval": 60000 | }""".stripMargin - val partConfig = OctopartsApiBuilder.Mapper.reader(classOf[HttpPartConfig]).readValue[HttpPartConfig](source) + val reader: ObjectReader = OctopartsApiBuilder.Mapper.reader(classOf[HttpPartConfig]) + val partConfig = reader.readValue[HttpPartConfig](source) partConfig.method should be(HttpMethod.Put) partConfig.parameters.head.paramType should be(ParamType.Header) } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d11dec88..2d7755be 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -33,7 +33,7 @@ object Dependencies { // Hystrix val hystrixCore = "com.netflix.hystrix" % "hystrix-core" % hystrixVersion val hystrixStream = "com.netflix.hystrix" % "hystrix-metrics-event-stream" % hystrixVersion - val rxJavaScala = "com.netflix.rxjava" % "rxjava-scala" % "0.20.3" // matches version used in hystrix-core + val rxJavaScala = "io.reactivex" %% "rxscala" % "0.23.0" // matches the version rxjava used in hystrix-core // HTTP clients val asyncHttpClient = "com.ning" % "async-http-client" % "1.9.6" @@ -78,6 +78,7 @@ object Dependencies { // Misc utils val commonsValidator = "commons-validator" % "commons-validator" % "1.4.1" % Runtime + val guava = "com.google.guava" % "guava" % "18.0" val jta = "javax.transaction" % "jta" % "1.1" val scalaUri = "com.netaporter" %% "scala-uri" % "0.4.5" val findbugs = "com.google.code.findbugs" % "jsr305" % "3.0.0" @@ -162,6 +163,7 @@ object Dependencies { val javaClientDependncies = Seq( findbugs intransitive(), + guava, slf4jApi, asyncHttpClient, jacksonCore, From cee5d0c8206addd8c263738252e063cc5f8f4bac Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Fri, 23 Jan 2015 13:12:46 +0900 Subject: [PATCH 04/17] Also fix the next line --- .../aggregator/handler/HttpPartRequestHandlerSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala b/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala index b0ca4d88..8f122b25 100644 --- a/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala +++ b/test/com/m3/octoparts/aggregator/handler/HttpPartRequestHandlerSpec.scala @@ -50,8 +50,8 @@ class HttpPartRequestHandlerSpec extends FunSpec with Matchers with ScalaFutures output.host shouldBe Some("mock.com") output.protocol shouldBe Some("http") output.path shouldBe "/hi/there" - output.query.params(queryParam1.outputName) shouldBe Seq(Some("scala")) - output.query.params(queryParam2.outputName).toSet shouldBe Set("lover", "lover2") + output.query.params(queryParam1.outputName).flatten shouldBe Seq("scala") + output.query.params(queryParam2.outputName).flatten.toSet shouldBe Set("lover", "lover2") } } describe("when providing only some params") { From 4b9c434d7a40516924559b3c8a942723252902bf Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 23 Jan 2015 14:54:56 +0900 Subject: [PATCH 05/17] Release 2.3.2 --- project/Version.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Version.scala b/project/Version.scala index 47f7cfe7..72588bd7 100644 --- a/project/Version.scala +++ b/project/Version.scala @@ -1,7 +1,7 @@ object Version { - val octopartsVersion = "2.4-SNAPSHOT" + val octopartsVersion = "2.3.2" val theScalaVersion = "2.11.5" } From 801001b2119f058e8cd3a557c070e4046b7fe003 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 23 Jan 2015 14:55:49 +0900 Subject: [PATCH 06/17] SNAPSHOT --- project/Version.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Version.scala b/project/Version.scala index 72588bd7..47f7cfe7 100644 --- a/project/Version.scala +++ b/project/Version.scala @@ -1,7 +1,7 @@ object Version { - val octopartsVersion = "2.3.2" + val octopartsVersion = "2.4-SNAPSHOT" val theScalaVersion = "2.11.5" } From 2c4b53d5703ad1cb855595261f93fcecbe2c5213 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 23 Jan 2015 15:06:16 +0900 Subject: [PATCH 07/17] Add documentation update to rollout procedure --- maintainers-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maintainers-guide.md b/maintainers-guide.md index 71c1e523..97ecc049 100644 --- a/maintainers-guide.md +++ b/maintainers-guide.md @@ -32,4 +32,4 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") - Run `./scripts/publish_libs.sh` - Use `sbt sonatypeRelease` from `sbt-sonatype` plugin or access sonatype console (https://oss.sonatype.org/) - Set version as "{next-version}-SNAPSHOT" on develop branch - +- Update documentation (https://github.com/m3dev/octoparts-site/blob/develop/data/versions.yml) From 479dde6dcdae0a32a4abe877053b42695e73e87e Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Tue, 27 Jan 2015 11:17:42 +0900 Subject: [PATCH 08/17] Rework the AggregateResponse enrichments --- .../ws/AggregateResponseEnrichment.scala | 143 ++++++++++++------ .../ws/RichAggregateResponseSpec.scala | 33 ++-- .../scala/com/m3/octoparts/ws/Sample.scala | 27 ++-- 3 files changed, 135 insertions(+), 68 deletions(-) diff --git a/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala b/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala index 0ea0b5a7..4186f10e 100644 --- a/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala +++ b/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala @@ -3,18 +3,69 @@ package com.m3.octoparts.ws import java.io.IOException import com.m3.octoparts.model.{ AggregateResponse, PartResponse } -import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.{ StringUtils, SystemUtils } import play.api.Logger import play.api.data.validation.ValidationError import play.api.i18n.Messages -import play.api.libs.json.{ JsPath, JsValue, Json, Reads } +import play.api.libs.json._ -import scala.util.{ Try, Success, Failure } +import scala.util.{ Failure, Success, Try } object AggregateResponseEnrichment { + case class OctopartsException(errors: Seq[String]) extends RuntimeException(errors.mkString(SystemUtils.LINE_SEPARATOR)) + private val logger = Logger("com.m3.octoparts.AggregateResponseEnrichment") + implicit class RichPartResponse(val part: PartResponse) extends AnyVal { + + def fullId: String = if (part.id == part.partId) part.id else s"${part.partId} (${part.id})" + + def tryContents: Try[String] = { + part.contents match { + case Some(contents) if StringUtils.isNotBlank(contents) => Success(contents) + case _ => Failure(new IOException("No content")) + } + } + + def printWarnings(): Unit = for (warning <- part.warnings) { + logger.warn(s"In part $fullId: $warning") + } + + def tryContentsIfNoError: Try[String] = { + printWarnings() + if (part.errors.isEmpty) tryContents else Failure(OctopartsException(part.errors)) + } + + /** + * Will return a failure if + * - the part has no content + * - the JSON parsing fails (e.g. the content is not JSON, or the JSON is broken in some way) + * - the JSON deserialization fails (i.e. the JSON is valid, but cannot be deserialized into an [[A]]) + * + * @tparam A the result type, i.e. the type of the JSON-serialized object + */ + def tryJson[A: Reads]: Try[A] = { + for { + contents <- tryContents + json <- Try(Json.parse(contents)) + a <- mapJson(json) + } yield { + a + } + } + + def tryJsonIfNoError[A: Reads]: Try[A] = { + for { + contents <- tryContentsIfNoError + json <- Try(Json.parse(contents)) + a <- mapJson(json) + } yield { + a + } + } + } + /** * Convenience methods to make it easier to work with [[com.m3.octoparts.model.AggregateResponse]] */ @@ -25,7 +76,7 @@ object AggregateResponseEnrichment { * * Note that an appropriate [[play.api.libs.json.Reads]] must be available in implicit scope. * - * Will return None if: + * Will return a Failure if: * - a part with the given ID is not present in the response * - the part has no content * - the JSON parsing fails (e.g. the content is not JSON, or the JSON is broken in some way) @@ -35,53 +86,55 @@ object AggregateResponseEnrichment { * - The method does not check the status code of the response or the presence of error messages. * * @param id the part request unique id (or partId if the part request did not specify an ID) - * @param recoverWith can be customized to return something even if JSON extraction failed. * @tparam A the result type, i.e. the type of the JSON-serialized object - * @return the object, or None if it could not be found and deserialized for some reason. + * @return the object, or Failure if it could not be found and deserialized for some reason. */ - def getJsonPart[A: Reads](id: String, recoverWith: (String, Throwable) => Option[A] = warnFailure): Option[A] = { - tryJsonPart[A](id) match { - case Failure(e) => recoverWith(id, e) - case Success(v) => Some(v) - } - } - - def getJsonPartOrElse[A: Reads](id: String, default: => A): A = getJsonPart[A](id).getOrElse(default) + def tryJsonPart[A: Reads](id: String): Try[A] = tryFindPart(id).flatMap(_.tryJsonIfNoError) - private def tryJsonPart[A: Reads](id: String): Try[A] = { - for { - part <- findPart(id) - contents <- getContents(id, part) - json <- Try(Json.parse(contents)) - a <- mapJson(json) - } yield { - a - } + /** + * Logs any errors from [[tryJsonPart]] and collapses to an Option + * + * @param id + * @tparam A + * @return + */ + def getJsonPart[A: Reads](id: String): Option[A] = tryJsonPart(id) match { + case Success(a) => Some(a) + case Failure(failure) => + logger.warn(s"Object not retrievable from part response: $id", failure) + None } - private def findPart(id: String): Try[PartResponse] = { - aggResp.findPart(id).fold[Try[PartResponse]] { - Failure(new IllegalArgumentException(s"part with id=$id not found")) - }(Success.apply) - } - } + def getJsonPartOrElse[A: Reads](id: String, default: => A): A = getJsonPart(id).getOrElse(default) - private def getContents(id: String, part: PartResponse): Try[String] = { - part.contents match { - case Some(contents) if StringUtils.isNotBlank(contents) => { - // print remaining errors - printErrors(id, part) - Success(contents) + /** + * Deserializes like [[tryJsonPart]], but will try instead to deserialize to an [[E]] if the part response contained errors + * @tparam A + * @tparam E + */ + def getJsonPartOrError[A: Reads, E: Reads](id: String): Either[Option[E], A] = tryFindPart(id) match { + case Failure(noPart) => { + logger.warn("Could not retrieve object from response", noPart) + Left(None) + } + case Success(part) => part.tryJsonIfNoError[A] match { + case Success(a) => Right(a) + case Failure(_) => { + part.tryJson[E] match { + case Success(e) => Left(Some(e)) + case Failure(failureE) => + logger.warn("Could not deserialize error", failureE) + Left(None) + } + } } - case _ => Failure(new IOException(part.errors.headOption.getOrElse("No content"))) } - } - private def printErrors(id: String, part: PartResponse): Unit = { - for { - error <- part.errors - } { - logger.warn(s"Error in part with id: $id, error: $error") + def tryFindPart(id: String): Try[PartResponse] = { + aggResp.findPart(id) match { + case None => Failure(new NoSuchElementException(s"part with id=$id not found")) + case Some(part) => Success(part) + } } } @@ -103,10 +156,4 @@ object AggregateResponseEnrichment { } }.mkString("; ") } - - val warnFailure: (String, Throwable) => Option[Nothing] = { - (id, failure) => - logger.warn(s"Object not retrievable from part response: $id", failure) - None - } } diff --git a/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichAggregateResponseSpec.scala b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichAggregateResponseSpec.scala index 0642a25d..7d9c31fa 100644 --- a/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichAggregateResponseSpec.scala +++ b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichAggregateResponseSpec.scala @@ -1,28 +1,42 @@ package com.m3.octoparts.ws +import java.io.IOException + import com.m3.octoparts.ws.AggregateResponseEnrichment._ import com.m3.octoparts.model.{ PartResponse, ResponseMeta, AggregateResponse } import org.scalatest.{ Matchers, FlatSpec } import play.api.libs.json.Json import scala.concurrent.duration._ +import scala.util.Success class RichAggregateResponseSpec extends FlatSpec with Matchers { behavior of "#getJsonPart" case class Frisk(mints: Int, mintiness: String) - implicit val reads = Json.reads[Frisk] + object Frisk { + implicit val _ = Json.reads[Frisk] + } + case class Error(msg: String) + object Error { + implicit val _ = Json.reads[Error] + } val aggResp = AggregateResponse(ResponseMeta("foo", 123.millis), Seq( PartResponse("myJsonPart", "myJsonPart", statusCode = Some(200), contents = Some("""{"mints": 50, "mintiness": "very minty"}""")), PartResponse("invalidJsonPart", "invalidJsonPart", statusCode = Some(200), contents = Some("""{"error": "something went wrong!"}""")), PartResponse("brokenJsonPart", "brokenJsonPart", statusCode = Some(200), contents = Some("""{"mints": }""")), PartResponse("noContentPart", "noContentPart", statusCode = Some(200), contents = None), - PartResponse("errorPart", "errorPart", statusCode = Some(200), contents = Some("""{"mints": 50, "mintiness": "very minty"}"""), errors = Seq("boo!")), + PartResponse("errorPart", "errorPart", statusCode = Some(500), contents = Some("""{"mints": 50, "mintiness": "very minty"}"""), errors = Seq("boo1", "boo2")), + PartResponse("errorPartWithValidError", "errorPartWithValidError", statusCode = Some(400), contents = Some("""{"msg": "bad request"}"""), errors = Seq("That was a bad request.")), + PartResponse("errorPartWithBrokenError", "errorPartWithBrokenError", statusCode = Some(500), contents = Some("Internal Server Error"), errors = Seq("boo1", "boo2")), PartResponse("differentStatusCodePart", "differentStatusCodePart", statusCode = Some(404), contents = Some("""{"mints": 50, "mintiness": "very minty"}""")) )) val richAggResp = new RichAggregateResponse(aggResp) + it should "successfully find a PartResponse by partId and deserialize its contents" in { + richAggResp.tryJsonPart[Frisk]("myJsonPart") should be(Success(Frisk(50, "very minty"))) + } it should "find a PartResponse by partId and deserialize its contents" in { richAggResp.getJsonPart[Frisk]("myJsonPart") should be(Some(Frisk(50, "very minty"))) } @@ -35,8 +49,14 @@ class RichAggregateResponseSpec extends FlatSpec with Matchers { it should "return None if the part has no content" in { richAggResp.getJsonPart[Frisk]("noContentPart") should be(None) } - it should "deserialize and return the result even if the part has error messages" in { - richAggResp.getJsonPart[Frisk]("errorPart") should be(Some(Frisk(50, "very minty"))) + it should "give up on deserialization if the result has error messages" in { + richAggResp.getJsonPart[Frisk]("errorPart") should be(None) + } + it should "deserialize errors if asked nicely" in { + richAggResp.getJsonPartOrError[Frisk, Error]("errorPartWithValidError") should be(Left(Some(Error("bad request")))) + } + it should "fail to deserialize errors even if asked nicely" in { + richAggResp.getJsonPartOrError[Frisk, Error]("errorPartWithBrokenError") should be(Left(None)) } it should "return None if the part does not exist" in { richAggResp.getJsonPart[Frisk]("whoAreYou") should be(None) @@ -44,9 +64,4 @@ class RichAggregateResponseSpec extends FlatSpec with Matchers { it should "deserialize and return the result even if the status code is not 200" in { richAggResp.getJsonPart[Frisk]("differentStatusCodePart") should be(Some(Frisk(50, "very minty"))) } - it should "use a custom recovery scheme" in { - richAggResp.getJsonPart[Frisk]("brokenJsonPart", { (id, failure) => Some(Frisk(0, failure.toString)) }) should matchPattern { - case Some(Frisk(0, mintiness)) if mintiness.contains("Exception") => - } - } } diff --git a/scala-ws-client/src/test/scala/com/m3/octoparts/ws/Sample.scala b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/Sample.scala index 4eda9782..c5b5040b 100644 --- a/scala-ws-client/src/test/scala/com/m3/octoparts/ws/Sample.scala +++ b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/Sample.scala @@ -28,36 +28,41 @@ object Sample { // Create a client val octoClient = new OctoClient("http://octoparts/", clientTimeout = 1.second) - // Build an AggregateRequest to send to Octoparts - val aggregateRequest = AggregateRequest( - requestMeta = RequestMeta( + implicit object rmb extends RequestMetaBuilder[Int] { + def apply(userId: Int) = RequestMeta( // Most of these fields are optional id = UUID.randomUUID().toString, serviceId = Some("frontend"), - userId = Some("123"), + userId = Some(userId.toString), requestUrl = Some("http://www.m3.com/foo"), timeout = Some(500.millis) - ), - requests = Seq( - // A list of the endpoints you want to call, with parameters to send - PartRequest(partId = "UserProfile", params = Seq(PartRequestParam("uid", "123"))), - PartRequest(partId = "LatestNews", params = Seq(PartRequestParam("limit", "10"))) ) + } + + // Prepare part requests to send to Octoparts + val requests = Seq( + // A list of the endpoints you want to call, with parameters to send + PartRequest(partId = "UserProfile", params = Seq(PartRequestParam("uid", "123"))), + PartRequest(partId = "LatestNews", params = Seq(PartRequestParam("limit", "10"))) ) - // Send the request, get back a Future - val fResp: Future[AggregateResponse] = octoClient.invoke(aggregateRequest) + // Send the requests, get back a Future + val fResp: Future[AggregateResponse] = octoClient.invoke(123, requests) fResp.map { aggregateResponse => // Result will be None if: // - Octoparts did not return a response for this endpoint (e.g. because the endpoint was too slow to respond) + // - the response contained errors // - the response could not be deserialized into a UserProfile val userProfile: Option[UserProfile] = aggregateResponse.getJsonPart[UserProfile]("UserProfile") // Get the list of latest news articles, falling back to an empty list in case of error val latestNews: Seq[NewsArticle] = aggregateResponse.getJsonPartOrElse[Seq[NewsArticle]]("LatestNews", default = Nil) + // Get the list of latest news article, falling back to another json model in case of error + val userNews: Either[Option[UserProfile], NewsArticle] = aggregateResponse.getJsonPartOrError[NewsArticle, UserProfile]("UserNews") + userNews.right.toOption == aggregateResponse.getJsonPart[NewsArticle]("UserNews") // Do stuff with the user profile and the list of news articles ... } From dab3f8dcd854e8eb7fb29dc67a3ea31caf933ee2 Mon Sep 17 00:00:00 2001 From: Taro Nagasawa Date: Tue, 27 Jan 2015 12:22:19 +0900 Subject: [PATCH 09/17] Added feature for local contents response #76 --- .../service/AggregatorServicesModule.scala | 2 +- .../PartResponseLocalContentSupport.scala | 29 ++++++++++++++++ .../model/config/HttpPartConfig.scala | 8 ++++- .../config/HttpPartConfigRepository.scala | 6 +++- app/controllers/AdminForms.scala | 22 +++++++++++-- app/views/part/edit.scala.html | 33 ++++++++++++++++++- app/views/part/list.scala.html | 8 +++++ app/views/part/show.scala.html | 7 ++++ .../default/V3__Add_local_content.sql | 2 ++ conf/messages.en | 5 +++ conf/messages.ja | 5 +++ .../octoparts/model/AggregateResponse.scala | 4 ++- .../model/config/json/HttpPartConfig.scala | 4 ++- .../model/config/HttpPartConfigSpec.scala | 4 ++- .../config/HttpPartConfigRepositorySpec.scala | 6 ++++ .../support/mocks/ConfigDataMocks.scala | 2 ++ test/controllers/AdminFormsSpec.scala | 5 +-- 17 files changed, 140 insertions(+), 12 deletions(-) create mode 100644 app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala create mode 100644 conf/db/migration/default/V3__Add_local_content.sql diff --git a/app/com/m3/octoparts/aggregator/service/AggregatorServicesModule.scala b/app/com/m3/octoparts/aggregator/service/AggregatorServicesModule.scala index 76cd14b2..096b220a 100644 --- a/app/com/m3/octoparts/aggregator/service/AggregatorServicesModule.scala +++ b/app/com/m3/octoparts/aggregator/service/AggregatorServicesModule.scala @@ -17,7 +17,7 @@ class AggregatorServicesModule extends Module { bind[PartRequestServiceBase] to new PartRequestService( inject[ConfigsRepository], inject[HttpHandlerFactory] - ) with PartResponseCachingSupport { + ) with PartResponseCachingSupport with PartResponseLocalContentSupport { val cacheOps = inject[CacheOps] } diff --git a/app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala b/app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala new file mode 100644 index 00000000..243d4d11 --- /dev/null +++ b/app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala @@ -0,0 +1,29 @@ +package com.m3.octoparts.aggregator.service + +import com.m3.octoparts.aggregator.PartRequestInfo +import com.m3.octoparts.model.PartResponse +import com.m3.octoparts.model.config.{ HttpPartConfig, ShortPartParam } + +import scala.concurrent.Future + +trait PartResponseLocalContentSupport extends PartRequestServiceBase { + + override def processWithConfig(ci: HttpPartConfig, + partRequestInfo: PartRequestInfo, + params: Map[ShortPartParam, Seq[String]]): Future[PartResponse] = { + if (ci.localContentsEnabled) { + Future(createPartResponse(ci, partRequestInfo)) + } else { + super.processWithConfig(ci, partRequestInfo, params) + } + } + + private def createPartResponse(ci: HttpPartConfig, + partRequestInfo: PartRequestInfo) = PartResponse( + ci.partId, + id = ci.partId, + statusCode = Some(200), + contents = ci.localContents, + retrievedFromLocalContents = true + ) +} diff --git a/app/com/m3/octoparts/model/config/HttpPartConfig.scala b/app/com/m3/octoparts/model/config/HttpPartConfig.scala index 6d374e18..259ac3fb 100644 --- a/app/com/m3/octoparts/model/config/HttpPartConfig.scala +++ b/app/com/m3/octoparts/model/config/HttpPartConfig.scala @@ -30,6 +30,8 @@ case class HttpPartConfig(id: Option[Long] = None, // None means that the record alertPercentThreshold: Option[Double], alertInterval: FiniteDuration, alertMailRecipients: Option[String], + localContentsEnabled: Boolean, + localContents: Option[String], createdAt: DateTime, updatedAt: DateTime) extends ConfigModel[HttpPartConfig] { @@ -79,7 +81,9 @@ object HttpPartConfig { alertAbsoluteThreshold = config.alertAbsoluteThreshold, alertPercentThreshold = config.alertPercentThreshold, alertInterval = config.alertInterval, - alertMailRecipients = config.alertMailRecipients + alertMailRecipients = config.alertMailRecipients, + localContentsEnabled = config.localContentsEnabled, + localContents = config.localContents ) } @@ -101,6 +105,8 @@ object HttpPartConfig { alertPercentThreshold = config.alertPercentThreshold, alertInterval = config.alertInterval, alertMailRecipients = config.alertMailRecipients, + localContentsEnabled = config.localContentsEnabled, + localContents = config.localContents, createdAt = DateTime.now, updatedAt = DateTime.now ) diff --git a/app/com/m3/octoparts/repository/config/HttpPartConfigRepository.scala b/app/com/m3/octoparts/repository/config/HttpPartConfigRepository.scala index eec52d39..cbafce52 100644 --- a/app/com/m3/octoparts/repository/config/HttpPartConfigRepository.scala +++ b/app/com/m3/octoparts/repository/config/HttpPartConfigRepository.scala @@ -31,7 +31,9 @@ object HttpPartConfigRepository extends ConfigMapper[HttpPartConfig] with Timest "alertAbsoluteThreshold" -> SkinnyParamType.Int, "alertPercentThreshold" -> SkinnyParamType.Double, "alertInterval" -> DurationParamType, - "alertMailRecipients" -> SkinnyParamType.String + "alertMailRecipients" -> SkinnyParamType.String, + "localContentsEnabled" -> SkinnyParamType.Boolean, + "localContents" -> SkinnyParamType.String ) case object DurationParamType extends AbstractParamType({ @@ -155,6 +157,8 @@ object HttpPartConfigRepository extends ConfigMapper[HttpPartConfig] with Timest alertPercentThreshold = rs.get(n.alertPercentThreshold), alertInterval = rs.long(n.alertInterval).seconds, alertMailRecipients = rs.get(n.alertMailRecipients), + localContentsEnabled = rs.get(n.localContentsEnabled), + localContents = rs.get(n.localContents), createdAt = rs.get(n.createdAt), updatedAt = rs.get(n.updatedAt) ) diff --git a/app/controllers/AdminForms.scala b/app/controllers/AdminForms.scala index 56f93f01..5220b2b3 100644 --- a/app/controllers/AdminForms.scala +++ b/app/controllers/AdminForms.scala @@ -29,7 +29,8 @@ object AdminForms { alertInterval: Option[Long], alertAbsoluteThreshold: Option[Int], alertPercentThreshold: Option[BigDecimal], - alertMailRecipients: Option[String]) { + alertMailRecipients: Option[String], + localContentsConfig: LocalContentsConfig) { data => /** Create a brand new HttpPartConfig using the data input into the form */ @@ -56,6 +57,8 @@ object AdminForms { alertAbsoluteThreshold = data.alertAbsoluteThreshold, alertPercentThreshold = data.alertPercentThreshold.map(_.toDouble), alertMailRecipients = data.alertMailRecipients, + localContentsEnabled = data.localContentsConfig.enabled, + localContents = data.localContentsConfig.contents, createdAt = DateTime.now, updatedAt = DateTime.now ) @@ -83,6 +86,8 @@ object AdminForms { alertAbsoluteThreshold = data.alertAbsoluteThreshold, alertPercentThreshold = data.alertPercentThreshold.map(_.toDouble), alertMailRecipients = data.alertMailRecipients, + localContentsEnabled = data.localContentsConfig.enabled, + localContents = data.localContentsConfig.contents, updatedAt = DateTime.now ) @@ -107,7 +112,10 @@ object AdminForms { alertInterval = Some(part.alertInterval.toSeconds), alertAbsoluteThreshold = part.alertAbsoluteThreshold, alertPercentThreshold = part.alertPercentThreshold.map(BigDecimal(_)), - alertMailRecipients = part.alertMailRecipients + alertMailRecipients = part.alertMailRecipients, + localContentsConfig = LocalContentsConfig( + enabled = part.localContentsEnabled, + contents = part.localContents) ) private def trimPartId(original: String): String = { @@ -138,10 +146,18 @@ object AdminForms { "alertInterval" -> optional(longNumber), "alertAbsoluteThreshold" -> optional(number), "alertPercentThreshold" -> optional(bigDecimal), - "alertMailRecipients" -> optional(text) + "alertMailRecipients" -> optional(text), + "localContentsConfig" -> mapping( + "enabled" -> boolean, + "contents" -> optional(text.verifying(scala.util.parsing.json.JSON.parseRaw(_).isDefined)) + )(LocalContentsConfig.apply)(LocalContentsConfig.unapply) )(PartData.apply)(PartData.unapply) ) + case class LocalContentsConfig( + enabled: Boolean, + contents: Option[String]) + case class ParamData( outputName: String, inputNameOverride: Option[String], diff --git a/app/views/part/edit.scala.html b/app/views/part/edit.scala.html index 2e7fff93..0824346a 100644 --- a/app/views/part/edit.scala.html +++ b/app/views/part/edit.scala.html @@ -30,6 +30,14 @@ prettySelect : true }); }); + + function validateJsonString(field) { + try { + JSON.parse(field.val()); + } catch (e) { + return "@Messages("parts.localContents.invalid.contents")"; + } + } } { @@ -220,6 +228,30 @@

@title

+
+ @Messages("parts.section.localContents") + +
+ +
+
+ @helper.checkbox(form("localContentsConfig.enabled")) +
+
+
+ +
+ +
+
+ @helper.textarea(form("localContentsConfig.contents"), + 'class -> "form-control validate[condRequired[funcCall[validateJsonString]]]", + 'rows -> 12) +
+
+
+
+
@@ -230,5 +262,4 @@

@title

- } diff --git a/app/views/part/list.scala.html b/app/views/part/list.scala.html index f625c469..887493f5 100644 --- a/app/views/part/list.scala.html +++ b/app/views/part/list.scala.html @@ -24,6 +24,7 @@ @Messages("parts.partId") @Messages("parts.url") @Messages("parts.owner") + @Messages("parts.section.localContents") @Messages("action") @@ -35,6 +36,13 @@ @partView.uriToInterpolate @partView.config.owner + + @if(partView.config.localContentsEnabled) { + @Messages("parts.localContents.enabled") + } else { + @Messages("parts.localContents.disabled") + } + @Messages("parts.tryIt") @Messages("detail") diff --git a/app/views/part/show.scala.html b/app/views/part/show.scala.html index 738718cd..a9985258 100644 --- a/app/views/part/show.scala.html +++ b/app/views/part/show.scala.html @@ -154,4 +154,11 @@

@Messages("parts.section.alertMail")

@Messages("parts.alertMail.none")

} +
+

@Messages("parts.section.localContents")

+ @if(part.config.localContentsEnabled) { +
@part.config.localContents.orNull
+ } else { +

@Messages("parts.localContents.disabled")

+ } } diff --git a/conf/db/migration/default/V3__Add_local_content.sql b/conf/db/migration/default/V3__Add_local_content.sql new file mode 100644 index 00000000..aab5bd91 --- /dev/null +++ b/conf/db/migration/default/V3__Add_local_content.sql @@ -0,0 +1,2 @@ +ALTER TABLE http_part_config ADD COLUMN local_contents_enabled boolean DEFAULT false NOT NULL; +ALTER TABLE http_part_config ADD COLUMN local_contents text; \ No newline at end of file diff --git a/conf/messages.en b/conf/messages.en index 3c83c50e..746ba568 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -70,6 +70,11 @@ parts.alertMail.interval.label=Interval (seconds) parts.alertMail.selectOne=At least one of these must be selected parts.alertMail.none=Disabled parts.alertMail.enabled=Enabled +parts.section.localContents=Local content settings +parts.localContents.enabled=Enabled +parts.localContents.disabled=Disabled +parts.localContents.contents=Content +parts.localContents.invalid.contents=Input JSON. tryIt.title=Try {0} ! tryIt.disable=Disable diff --git a/conf/messages.ja b/conf/messages.ja index e3d4e32c..d173520c 100644 --- a/conf/messages.ja +++ b/conf/messages.ja @@ -70,6 +70,11 @@ parts.alertMail.interval.label=間隔(秒) parts.alertMail.selectOne=どちらか指定必須、両方指定も可能 parts.alertMail.none=送信しません parts.alertMail.enabled=有効 +parts.section.localContents=ローカルコンテンツ設定 +parts.localContents.enabled=有効 +parts.localContents.disabled=無効 +parts.localContents.contents=コンテンツ +parts.localContents.invalid.contents=JSONを入力してください。 tryIt.title={0}の送信テスト tryIt.disable=無効にする diff --git a/models/src/main/scala/com/m3/octoparts/model/AggregateResponse.scala b/models/src/main/scala/com/m3/octoparts/model/AggregateResponse.scala index 1c65b411..0b9ea70d 100644 --- a/models/src/main/scala/com/m3/octoparts/model/AggregateResponse.scala +++ b/models/src/main/scala/com/m3/octoparts/model/AggregateResponse.scala @@ -53,6 +53,7 @@ case class ResponseMeta(@(ApiModelProperty @field)(required = true)@BeanProperty * @param errors * @param warnings * @param retrievedFromCache + * @param retrievedFromLocalContents * */ case class PartResponse(@(ApiModelProperty @field)(required = true)@BeanProperty partId: String, @@ -65,7 +66,8 @@ case class PartResponse(@(ApiModelProperty @field)(required = true)@BeanProperty @(ApiModelProperty @field)(required = false, dataType = "string")@BeanProperty contents: Option[String] = None, @BeanProperty warnings: Seq[String] = Nil, @BeanProperty errors: Seq[String] = Nil, - @(ApiModelProperty @field)(required = true)@BooleanBeanProperty retrievedFromCache: Boolean = false) + @(ApiModelProperty @field)(required = true)@BooleanBeanProperty retrievedFromCache: Boolean = false, + @(ApiModelProperty @field)(required = true)@BooleanBeanProperty retrievedFromLocalContents: Boolean = false) /** * Immutable wrapper for cookies diff --git a/models/src/main/scala/com/m3/octoparts/model/config/json/HttpPartConfig.scala b/models/src/main/scala/com/m3/octoparts/model/config/json/HttpPartConfig.scala index 802442ca..897d2729 100644 --- a/models/src/main/scala/com/m3/octoparts/model/config/json/HttpPartConfig.scala +++ b/models/src/main/scala/com/m3/octoparts/model/config/json/HttpPartConfig.scala @@ -24,4 +24,6 @@ case class HttpPartConfig( @(ApiModelProperty @field)(dataType = "integer", required = false, allowableValues = "range[0, Infinity]") alertAbsoluteThreshold: Option[Int] = None, @(ApiModelProperty @field)(dataType = "float", required = false, allowableValues = "range[0, 100]") alertPercentThreshold: Option[Double] = None, @(ApiModelProperty @field)(dataType = "integer", required = true, allowableValues = "range[0, Infinity]", value = "in ms") alertInterval: FiniteDuration, - @(ApiModelProperty @field)(dataType = "string", required = false) alertMailRecipients: Option[String] = None) + @(ApiModelProperty @field)(dataType = "string", required = false) alertMailRecipients: Option[String] = None, + @(ApiModelProperty @field)(required = true) localContentsEnabled: Boolean = false, + @(ApiModelProperty @field)(dataType = "string", required = false) localContents: Option[String] = None) diff --git a/test/com/m3/octoparts/model/config/HttpPartConfigSpec.scala b/test/com/m3/octoparts/model/config/HttpPartConfigSpec.scala index 8e04a627..a2b234f6 100644 --- a/test/com/m3/octoparts/model/config/HttpPartConfigSpec.scala +++ b/test/com/m3/octoparts/model/config/HttpPartConfigSpec.scala @@ -53,7 +53,9 @@ class HttpPartConfigSpec extends FunSpec with Matchers with ConfigDataMocks { alertAbsoluteThreshold = Some(1000), alertPercentThreshold = Some(33.0), alertInterval = 10 minutes, - alertMailRecipients = Some("l-chan@m3.com")) + alertMailRecipients = Some("l-chan@m3.com"), + localContentsEnabled = true, + localContents = Some("{}")) jsonModel should be(expectedModel) } diff --git a/test/com/m3/octoparts/repository/config/HttpPartConfigRepositorySpec.scala b/test/com/m3/octoparts/repository/config/HttpPartConfigRepositorySpec.scala index f4db8777..913bf811 100644 --- a/test/com/m3/octoparts/repository/config/HttpPartConfigRepositorySpec.scala +++ b/test/com/m3/octoparts/repository/config/HttpPartConfigRepositorySpec.scala @@ -231,6 +231,8 @@ class HttpPartConfigRepositorySpec extends fixture.FunSpec with DBSuite with Mat observed.description should be(expected.description) observed.uriToInterpolate should be(expected.uriToInterpolate) observed.method should be(expected.method) + observed.localContentsEnabled should be(expected.localContentsEnabled) + observed.localContents should be(expected.localContents) observedHystrixConfig.commandKey should be(expectedHystrixConfig.commandKey) observedHystrixConfig.commandGroupKey should be(expectedHystrixConfig.commandGroupKey) @@ -259,6 +261,8 @@ class HttpPartConfigRepositorySpec extends fixture.FunSpec with DBSuite with Mat alertPercentThreshold = Some(50), alertInterval = 5.minutes, alertMailRecipients = Some("c-birchall@m3.com"), + localContentsEnabled = true, + localContents = Some("{}"), createdAt = DateTime.now, updatedAt = DateTime.now ) @@ -288,6 +292,8 @@ class HttpPartConfigRepositorySpec extends fixture.FunSpec with DBSuite with Mat alertPercentThreshold = Some(50), alertInterval = 5.minutes, alertMailRecipients = Some("v-pericart@m3.com"), + localContentsEnabled = true, + localContents = Some("{}"), createdAt = DateTime.now, updatedAt = DateTime.now ) diff --git a/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala b/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala index 6b0c3679..5eac0088 100644 --- a/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala +++ b/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala @@ -37,6 +37,8 @@ trait ConfigDataMocks { alertPercentThreshold = Some(33), alertInterval = 10.minutes, alertMailRecipients = Some("l-chan@m3.com"), + localContentsEnabled = true, + localContents = Some("{}"), updatedAt = now, createdAt = now ) diff --git a/test/controllers/AdminFormsSpec.scala b/test/controllers/AdminFormsSpec.scala index 81bcbb98..8234a7f1 100644 --- a/test/controllers/AdminFormsSpec.scala +++ b/test/controllers/AdminFormsSpec.scala @@ -2,7 +2,7 @@ package controllers import com.m3.octoparts.model.config.{ PartParam, CacheGroup } import com.m3.octoparts.support.mocks.ConfigDataMocks -import controllers.AdminForms.PartData +import controllers.AdminForms.{ LocalContentsConfig, PartData } import org.scalatest.{ FunSpec, Matchers } class AdminFormsSpec extends FunSpec with Matchers with ConfigDataMocks { @@ -25,7 +25,8 @@ class AdminFormsSpec extends FunSpec with Matchers with ConfigDataMocks { alertInterval = None, alertAbsoluteThreshold = None, alertPercentThreshold = None, - alertMailRecipients = None) + alertMailRecipients = None, + localContentsConfig = LocalContentsConfig(enabled = false, contents = None)) describe("#toNewHttpPartConfig") { it("should trim leading and trailing spaces from the partId") { From cc02865e11ef18ee82689f20df7fcf27951f519c Mon Sep 17 00:00:00 2001 From: Taro Nagasawa Date: Wed, 28 Jan 2015 10:15:24 +0900 Subject: [PATCH 10/17] make JSON validation for local contents optional --- app/controllers/AdminForms.scala | 2 +- app/views/part/edit.scala.html | 38 +++++++++++++++++++++++--------- conf/messages.en | 4 +++- conf/messages.ja | 4 +++- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/app/controllers/AdminForms.scala b/app/controllers/AdminForms.scala index 5220b2b3..2c407d82 100644 --- a/app/controllers/AdminForms.scala +++ b/app/controllers/AdminForms.scala @@ -149,7 +149,7 @@ object AdminForms { "alertMailRecipients" -> optional(text), "localContentsConfig" -> mapping( "enabled" -> boolean, - "contents" -> optional(text.verifying(scala.util.parsing.json.JSON.parseRaw(_).isDefined)) + "contents" -> optional(text) )(LocalContentsConfig.apply)(LocalContentsConfig.unapply) )(PartData.apply)(PartData.unapply) ) diff --git a/app/views/part/edit.scala.html b/app/views/part/edit.scala.html index 0824346a..f79e2af8 100644 --- a/app/views/part/edit.scala.html +++ b/app/views/part/edit.scala.html @@ -29,15 +29,31 @@ $("#editPart").validationEngine({ prettySelect : true }); - }); - function validateJsonString(field) { - try { - JSON.parse(field.val()); - } catch (e) { - return "@Messages("parts.localContents.invalid.contents")"; + var localContentsConfigContents = $("#localContentsConfig_contents"); + var localContents = $("#localContents"); + + $("#jsonValidateButton").click(function () { + var valid = isValidJsonString(localContentsConfigContents.val()); + localContents.toggleClass("has-success", valid); + localContents.toggleClass("has-error", !valid); + localContentsConfigContents.popover("show"); + }); + + localContentsConfigContents.popover({trigger: "manual", content: function() { + var valid = isValidJsonString($(this).val()); + return valid ? '@Messages("parts.localContents.contents.valid")' : '@Messages("parts.localContents.contents.invalid")'; + }}); + + function isValidJsonString(s) { + try { + JSON.parse(s); + return true; + } catch (e) { + return false; + } } - } + }); } { @@ -240,14 +256,16 @@

@title

-
+
@helper.textarea(form("localContentsConfig.contents"), - 'class -> "form-control validate[condRequired[funcCall[validateJsonString]]]", - 'rows -> 12) + 'class -> "form-control", + 'rows -> 12, + Symbol("data-placement") -> "top")
+
diff --git a/conf/messages.en b/conf/messages.en index 746ba568..738e8aaf 100644 --- a/conf/messages.en +++ b/conf/messages.en @@ -74,7 +74,9 @@ parts.section.localContents=Local content settings parts.localContents.enabled=Enabled parts.localContents.disabled=Disabled parts.localContents.contents=Content -parts.localContents.invalid.contents=Input JSON. +parts.localContents.validateJson=Validate JSON +parts.localContents.contents.valid=Valid JSON +parts.localContents.contents.invalid=Invalid JSON tryIt.title=Try {0} ! tryIt.disable=Disable diff --git a/conf/messages.ja b/conf/messages.ja index d173520c..9d8091f5 100644 --- a/conf/messages.ja +++ b/conf/messages.ja @@ -74,7 +74,9 @@ parts.section.localContents=ローカルコンテンツ設定 parts.localContents.enabled=有効 parts.localContents.disabled=無効 parts.localContents.contents=コンテンツ -parts.localContents.invalid.contents=JSONを入力してください。 +parts.localContents.validateJson=JSON確認 +parts.localContents.contents.valid=有効なJSONです +parts.localContents.contents.invalid=無効なJSONです tryIt.title={0}の送信テスト tryIt.disable=無効にする From 2302a703e45da012d19a64125c9b4f239ec9754a Mon Sep 17 00:00:00 2001 From: Taro Nagasawa Date: Wed, 28 Jan 2015 12:26:34 +0900 Subject: [PATCH 11/17] Add PartResponseLocalContentSupport spec --- .../PartResponseLocalContentSupportSpec.scala | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala diff --git a/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala b/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala new file mode 100644 index 00000000..ff32826b --- /dev/null +++ b/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala @@ -0,0 +1,66 @@ +package com.m3.octoparts.aggregator.service + +import com.m3.octoparts.aggregator.PartRequestInfo +import com.m3.octoparts.aggregator.handler.HttpHandlerFactory +import com.m3.octoparts.model.PartResponse +import com.m3.octoparts.model.config.{ HttpPartConfig, ShortPartParam } +import com.m3.octoparts.repository.ConfigsRepository +import org.mockito.Mockito._ +import org.scalatest.concurrent.{ Eventually, ScalaFutures } +import org.scalatest.mock.MockitoSugar +import org.scalatest.{ FlatSpec, Matchers } + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.{ ExecutionContext, Future } + +class PartResponseLocalContentSupportSpec extends FlatSpec + with Matchers with MockitoSugar with ScalaFutures with Eventually { + + behavior of "#processWithConfig" + + private val partResponseFromSuper = mock[PartResponse] + + private class Super extends PartRequestServiceBase { + override implicit def executionContext: ExecutionContext = global + override def repository: ConfigsRepository = ??? + override def handlerFactory: HttpHandlerFactory = ??? + override def processWithConfig(ci: HttpPartConfig, + partRequestInfo: PartRequestInfo, + params: Map[ShortPartParam, Seq[String]]): Future[PartResponse] = Future(partResponseFromSuper) + } + + private val sut = new Super with PartResponseLocalContentSupport + + it should "forward to super if local contents is disabled" in { + val httpPartConfig = mock[HttpPartConfig] + when(httpPartConfig.localContentsEnabled).thenReturn(false) + + val partRequestInfo = mock[PartRequestInfo] + val params = mock[Map[ShortPartParam, Seq[String]]] + + whenReady(sut.processWithConfig(httpPartConfig, partRequestInfo, params)) { resp => + resp should be theSameInstanceAs partResponseFromSuper + } + } + + it should "return response as local contents if local contents is enabled" in { + val httpPartConfig = mock[HttpPartConfig] + when(httpPartConfig.localContentsEnabled).thenReturn(true) + when(httpPartConfig.localContents).thenReturn(Some("localContents")) + when(httpPartConfig.partId).thenReturn("hogefuga") + + val partRequestInfo = mock[PartRequestInfo] + val params = mock[Map[ShortPartParam, Seq[String]]] + + whenReady(sut.processWithConfig(httpPartConfig, partRequestInfo, params)) { resp => + resp should not be theSameInstanceAs(partResponseFromSuper) + resp should be(PartResponse( + partId = "hogefuga", + id = "hogefuga", + statusCode = Some(200), + contents = Some("localContents"), + retrievedFromLocalContents = true + )) + } + } +} \ No newline at end of file From c51ffad416f445ecf98edf5f311d84885a8bf1dc Mon Sep 17 00:00:00 2001 From: Taro Nagasawa Date: Wed, 28 Jan 2015 12:50:44 +0900 Subject: [PATCH 12/17] Add doc comments to HttpPartConfig --- app/com/m3/octoparts/model/config/HttpPartConfig.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/com/m3/octoparts/model/config/HttpPartConfig.scala b/app/com/m3/octoparts/model/config/HttpPartConfig.scala index 259ac3fb..32021318 100644 --- a/app/com/m3/octoparts/model/config/HttpPartConfig.scala +++ b/app/com/m3/octoparts/model/config/HttpPartConfig.scala @@ -12,6 +12,9 @@ import scala.util.Try /** * Model for holding configuration data for a Http dependency that * comes with a companion-object that can populate it from the database + * + * @param localContentsEnabled whether local contents is enabled + * @param localContents the static contents which is used instead of actual contents of this part */ case class HttpPartConfig(id: Option[Long] = None, // None means that the record is new partId: String, From 332db8d7d3dfb8fdc5f90a4a406b4bd8be49da04 Mon Sep 17 00:00:00 2001 From: Taro Nagasawa Date: Thu, 29 Jan 2015 10:49:26 +0900 Subject: [PATCH 13/17] Fix PartResponseLocalContentSupportSpec --- .../PartResponseLocalContentSupportSpec.scala | 33 +++++++--------- .../support/mocks/ConfigDataMocks.scala | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala b/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala index ff32826b..a3a3e9f8 100644 --- a/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala +++ b/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala @@ -5,20 +5,19 @@ import com.m3.octoparts.aggregator.handler.HttpHandlerFactory import com.m3.octoparts.model.PartResponse import com.m3.octoparts.model.config.{ HttpPartConfig, ShortPartParam } import com.m3.octoparts.repository.ConfigsRepository -import org.mockito.Mockito._ -import org.scalatest.concurrent.{ Eventually, ScalaFutures } -import org.scalatest.mock.MockitoSugar +import com.m3.octoparts.support.mocks.ConfigDataMocks +import org.scalatest.concurrent.ScalaFutures import org.scalatest.{ FlatSpec, Matchers } import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ ExecutionContext, Future } class PartResponseLocalContentSupportSpec extends FlatSpec - with Matchers with MockitoSugar with ScalaFutures with Eventually { + with Matchers with ScalaFutures with ConfigDataMocks { behavior of "#processWithConfig" - private val partResponseFromSuper = mock[PartResponse] + private val partResponseFromSuper = mockPartResponse private class Super extends PartRequestServiceBase { override implicit def executionContext: ExecutionContext = global @@ -32,11 +31,9 @@ class PartResponseLocalContentSupportSpec extends FlatSpec private val sut = new Super with PartResponseLocalContentSupport it should "forward to super if local contents is disabled" in { - val httpPartConfig = mock[HttpPartConfig] - when(httpPartConfig.localContentsEnabled).thenReturn(false) - - val partRequestInfo = mock[PartRequestInfo] - val params = mock[Map[ShortPartParam, Seq[String]]] + val httpPartConfig = mockHttpPartConfig.copy(localContentsEnabled = false) + val partRequestInfo = mockPartRequestInfo + val params = Map.empty[ShortPartParam, Seq[String]] whenReady(sut.processWithConfig(httpPartConfig, partRequestInfo, params)) { resp => resp should be theSameInstanceAs partResponseFromSuper @@ -44,21 +41,17 @@ class PartResponseLocalContentSupportSpec extends FlatSpec } it should "return response as local contents if local contents is enabled" in { - val httpPartConfig = mock[HttpPartConfig] - when(httpPartConfig.localContentsEnabled).thenReturn(true) - when(httpPartConfig.localContents).thenReturn(Some("localContents")) - when(httpPartConfig.partId).thenReturn("hogefuga") - - val partRequestInfo = mock[PartRequestInfo] - val params = mock[Map[ShortPartParam, Seq[String]]] + val httpPartConfig = mockHttpPartConfig + val partRequestInfo = mockPartRequestInfo + val params = Map.empty[ShortPartParam, Seq[String]] whenReady(sut.processWithConfig(httpPartConfig, partRequestInfo, params)) { resp => resp should not be theSameInstanceAs(partResponseFromSuper) resp should be(PartResponse( - partId = "hogefuga", - id = "hogefuga", + partId = "something", + id = "something", statusCode = Some(200), - contents = Some("localContents"), + contents = Some("{}"), retrievedFromLocalContents = true )) } diff --git a/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala b/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala index 5eac0088..26e82050 100644 --- a/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala +++ b/test/com/m3/octoparts/support/mocks/ConfigDataMocks.scala @@ -1,10 +1,13 @@ package com.m3.octoparts.support.mocks +import com.m3.octoparts.aggregator.PartRequestInfo import com.m3.octoparts.model.HttpMethod._ import com.m3.octoparts.model.config.ParamType._ import com.m3.octoparts.model.config._ +import com.m3.octoparts.model.{ CacheControl, PartResponse, PartRequest, RequestMeta } import org.joda.time.DateTime +import scala.concurrent.duration import scala.concurrent.duration._ /** @@ -66,4 +69,40 @@ trait ConfigDataMocks { updatedAt = now ) + def mockPartRequestInfo = PartRequestInfo( + requestMeta = mockRequestMeta, + partRequest = mockPartRequest, + noCache = false + ) + + def mockRequestMeta = RequestMeta( + id = "id", + serviceId = Some("serviceId"), + userId = Some("uesrId"), + sessionId = Some("sessionId"), + requestUrl = Some("https://example.com/"), + userAgent = Some("userAgent"), + timeout = Some(FiniteDuration.apply(30, duration.SECONDS)) + ) + + def mockPartRequest = PartRequest( + partId = "partId", + id = Some("id"), + params = Nil + ) + + def mockPartResponse = PartResponse( + partId = "pardId", + id = "id", + cookies = Nil, + statusCode = Some(200), + mimeType = Some("text/plain"), + charset = Some("UTF-8"), + cacheControl = CacheControl.NotSet, + contents = Some("contents"), + warnings = Seq("warning"), + errors = Seq("errors"), + retrievedFromCache = false, + retrievedFromLocalContents = true + ) } From 7456066d9cdd5baa8b1ebc46626c22eb1377672d Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Thu, 5 Feb 2015 07:22:20 +0900 Subject: [PATCH 14/17] When returning local content, the part request ID should be kept --- .../aggregator/service/PartResponseLocalContentSupport.scala | 2 +- .../service/PartResponseLocalContentSupportSpec.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala b/app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala index 243d4d11..7f2a35aa 100644 --- a/app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala +++ b/app/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupport.scala @@ -21,7 +21,7 @@ trait PartResponseLocalContentSupport extends PartRequestServiceBase { private def createPartResponse(ci: HttpPartConfig, partRequestInfo: PartRequestInfo) = PartResponse( ci.partId, - id = ci.partId, + id = partRequestInfo.partRequestId, statusCode = Some(200), contents = ci.localContents, retrievedFromLocalContents = true diff --git a/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala b/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala index a3a3e9f8..fd99f88f 100644 --- a/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala +++ b/test/com/m3/octoparts/aggregator/service/PartResponseLocalContentSupportSpec.scala @@ -49,7 +49,7 @@ class PartResponseLocalContentSupportSpec extends FlatSpec resp should not be theSameInstanceAs(partResponseFromSuper) resp should be(PartResponse( partId = "something", - id = "something", + id = "id", statusCode = Some(200), contents = Some("{}"), retrievedFromLocalContents = true From 4ccb9f7c3adfe05574ccec46f551dd0eea5890c3 Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Thu, 12 Feb 2015 08:14:20 +0900 Subject: [PATCH 15/17] Added some specs and javadocs --- .../ws/AggregateResponseEnrichment.scala | 16 +++++++++++ .../octoparts/ws/RichPartResponseSpec.scala | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala diff --git a/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala b/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala index 4186f10e..0dfb4d36 100644 --- a/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala +++ b/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala @@ -19,8 +19,14 @@ object AggregateResponseEnrichment { implicit class RichPartResponse(val part: PartResponse) extends AnyVal { + /** + * @return The partId and the request-specific id. Used to uniquely identify parts when printing messages. + */ def fullId: String = if (part.id == part.partId) part.id else s"${part.partId} (${part.id})" + /** + * @return [[PartResponse.contents]] only if there is some non-blank contents. + */ def tryContents: Try[String] = { part.contents match { case Some(contents) if StringUtils.isNotBlank(contents) => Success(contents) @@ -28,10 +34,17 @@ object AggregateResponseEnrichment { } } + /** + * Prints all parts warnings (deprecation...) + */ def printWarnings(): Unit = for (warning <- part.warnings) { logger.warn(s"In part $fullId: $warning") } + /** + * Prints [[PartResponse.warnings]], and try to extract [[PartResponse.contents]] only if there were no [[PartResponse.errors]] . + * @return + */ def tryContentsIfNoError: Try[String] = { printWarnings() if (part.errors.isEmpty) tryContents else Failure(OctopartsException(part.errors)) @@ -55,6 +68,9 @@ object AggregateResponseEnrichment { } } + /** + * Same as [[tryJson]] but only tries it when there is no [[PartResponse.errors]] + */ def tryJsonIfNoError[A: Reads]: Try[A] = { for { contents <- tryContentsIfNoError diff --git a/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala new file mode 100644 index 00000000..192cef8e --- /dev/null +++ b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala @@ -0,0 +1,27 @@ +package com.m3.octoparts.ws + +import com.m3.octoparts.model.PartResponse +import org.scalatest.{FunSpec, Matchers} + +import scala.util.Success + +class RichPartResponseSpec extends FunSpec with Matchers { + import AggregateResponseEnrichment.RichPartResponse + describe("#tryContents") { + it("should fail on blank contents") { + val partResponse = PartResponse("partId", "id", contents = Some(" \r\n ")) + partResponse.tryContents.isFailure shouldBe true + } + } + + describe("#tryContentsIfNoError") { + it ("should stop if there are errors") { + val partResponse = PartResponse("partId", "id", contents = Some("woot!"), errors = Seq("Oops")) + partResponse.tryContentsIfNoError.isFailure shouldBe true + } + it ("should not stop for mere warnings") { + val partResponse = PartResponse("partId", "id", contents = Some("woot!"), warnings = Seq("Oops")) + partResponse.tryContentsIfNoError shouldBe Success("woot!") + } + } +} From 2ce2ba1356c36c6cd0eaa41b068b6c121d33b721 Mon Sep 17 00:00:00 2001 From: Vincent PERICART Date: Thu, 12 Feb 2015 10:06:51 +0900 Subject: [PATCH 16/17] Debug the part response in case of errors --- .../m3/octoparts/ws/AggregateResponseEnrichment.scala | 10 +++++++--- .../com/m3/octoparts/ws/RichPartResponseSpec.scala | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala b/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala index 0dfb4d36..c6f3847a 100644 --- a/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala +++ b/scala-ws-client/src/main/scala/com/m3/octoparts/ws/AggregateResponseEnrichment.scala @@ -13,7 +13,7 @@ import scala.util.{ Failure, Success, Try } object AggregateResponseEnrichment { - case class OctopartsException(errors: Seq[String]) extends RuntimeException(errors.mkString(SystemUtils.LINE_SEPARATOR)) + case class OctopartsException(partResponse: PartResponse) extends RuntimeException(partResponse.errors.mkString(SystemUtils.LINE_SEPARATOR)) private val logger = Logger("com.m3.octoparts.AggregateResponseEnrichment") @@ -47,7 +47,7 @@ object AggregateResponseEnrichment { */ def tryContentsIfNoError: Try[String] = { printWarnings() - if (part.errors.isEmpty) tryContents else Failure(OctopartsException(part.errors)) + if (part.errors.isEmpty) tryContents else Failure(OctopartsException(part)) } /** @@ -83,7 +83,7 @@ object AggregateResponseEnrichment { } /** - * Convenience methods to make it easier to work with [[com.m3.octoparts.model.AggregateResponse]] + * Convenience methods to make it easier to work with [[AggregateResponse]] */ implicit class RichAggregateResponse(val aggResp: AggregateResponse) extends AnyVal { @@ -118,6 +118,10 @@ object AggregateResponseEnrichment { case Success(a) => Some(a) case Failure(failure) => logger.warn(s"Object not retrievable from part response: $id", failure) + failure match { + case OctopartsException(pr) => logger.debug(s"Part response: $pr") + case _ => + } None } diff --git a/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala index 192cef8e..12f2a608 100644 --- a/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala +++ b/scala-ws-client/src/test/scala/com/m3/octoparts/ws/RichPartResponseSpec.scala @@ -1,7 +1,7 @@ package com.m3.octoparts.ws import com.m3.octoparts.model.PartResponse -import org.scalatest.{FunSpec, Matchers} +import org.scalatest.{ FunSpec, Matchers } import scala.util.Success @@ -15,11 +15,11 @@ class RichPartResponseSpec extends FunSpec with Matchers { } describe("#tryContentsIfNoError") { - it ("should stop if there are errors") { + it("should stop if there are errors") { val partResponse = PartResponse("partId", "id", contents = Some("woot!"), errors = Seq("Oops")) partResponse.tryContentsIfNoError.isFailure shouldBe true } - it ("should not stop for mere warnings") { + it("should not stop for mere warnings") { val partResponse = PartResponse("partId", "id", contents = Some("woot!"), warnings = Seq("Oops")) partResponse.tryContentsIfNoError shouldBe Success("woot!") } From ec5e2f20566f63447039cd665f49648899509518 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 12 Feb 2015 15:25:42 +0900 Subject: [PATCH 17/17] Release 2.3.3 --- project/Version.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Version.scala b/project/Version.scala index 72588bd7..731e6203 100644 --- a/project/Version.scala +++ b/project/Version.scala @@ -1,7 +1,7 @@ object Version { - val octopartsVersion = "2.3.2" + val octopartsVersion = "2.3.3" val theScalaVersion = "2.11.5" }