-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 🚀 Provide Play-JSON body encoder #114
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
51 changes: 51 additions & 0 deletions
51
play-json/src/main/scala/pact4s/playjson/JsonConversion.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,51 @@ | ||
package pact4s.playjson | ||
|
||
import au.com.dius.pact.consumer.dsl.{DslPart, PactDslJsonArray, PactDslJsonBody, PactDslJsonRootValue} | ||
import play.api.libs.json._ | ||
|
||
private[playjson] object JsonConversion { | ||
|
||
private def addFieldToBuilder(builder: PactDslJsonBody, fieldName: String, json: JsValue): PactDslJsonBody = | ||
json match { | ||
case JsNull => builder.nullValue(fieldName) | ||
case JsTrue => builder.booleanValue(fieldName, true) | ||
case JsFalse => builder.booleanValue(fieldName, false) | ||
case JsNumber(num) => builder.numberValue(fieldName, num) | ||
case JsString(str) => builder.stringValue(fieldName, str) | ||
case JsArray(array) => addArrayToJsonBody(builder, fieldName, array.toSeq) | ||
case jsonObject: JsObject => builder.`object`(fieldName, addJsonObjToBuilder(new PactDslJsonBody(), jsonObject)) | ||
} | ||
|
||
private def addJsonObjToBuilder(builder: PactDslJsonBody, jsonObj: JsObject): PactDslJsonBody = | ||
jsonObj.value.foldLeft(builder) { case (b, (s, j)) => | ||
addFieldToBuilder(b, s, j) | ||
} | ||
|
||
private def addArrayToJsonBody(builder: PactDslJsonBody, fieldName: String, array: Seq[JsValue]): PactDslJsonBody = | ||
addArrayValuesToArray(builder.array(fieldName), array).closeArray().asBody() | ||
|
||
private def addArrayValuesToArray(builder: PactDslJsonArray, array: Seq[JsValue]): PactDslJsonArray = | ||
array | ||
.foldLeft(builder) { (arrayBody, json) => | ||
json match { | ||
case JsNull => arrayBody.nullValue() | ||
case JsTrue => arrayBody.booleanValue(true) | ||
case JsFalse => arrayBody.booleanValue(false) | ||
case JsNumber(num) => arrayBody.numberValue(num) | ||
case JsString(str) => arrayBody.stringValue(str) | ||
case JsArray(arr) => addArrayValuesToArray(arrayBody.array(), arr.toSeq).closeArray().asArray() | ||
case jsonObj: JsObject => addJsonObjToBuilder(arrayBody.`object`(), jsonObj).closeObject().asArray() | ||
} | ||
} | ||
|
||
def jsonToPactDslJsonBody(json: JsValue): DslPart = | ||
json match { | ||
case JsNull => throw new IllegalArgumentException("Content cannot be null json value if set") | ||
case JsFalse => PactDslJsonRootValue.booleanType(false) | ||
case JsTrue => PactDslJsonRootValue.booleanType(true) | ||
case JsNumber(num) => PactDslJsonRootValue.numberType(num) | ||
case JsString(str) => PactDslJsonRootValue.stringType(str) | ||
case JsArray(arr) => addArrayValuesToArray(new PactDslJsonArray(), arr.toSeq) | ||
case jsonObj: JsObject => addJsonObjToBuilder(new PactDslJsonBody(), jsonObj) | ||
} | ||
} |
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,22 @@ | ||
package pact4s.playjson | ||
|
||
import au.com.dius.pact.core.model.messaging.Message | ||
import pact4s.algebras.{MessagePactDecoder, PactBodyJsonEncoder, PactDslJsonBodyEncoder} | ||
import pact4s.playjson.JsonConversion.jsonToPactDslJsonBody | ||
import pact4s.provider.ProviderState | ||
import play.api.libs.json.{Format, Json, Reads, Writes} | ||
|
||
import scala.util.Try | ||
|
||
object implicits { | ||
implicit def pactBodyEncoder[A](implicit writes: Writes[A]): PactBodyJsonEncoder[A] = | ||
(a: A) => Json.toJson(a).toString() | ||
|
||
implicit def pactDslJsonBodyConverter[A](implicit writes: Writes[A]): PactDslJsonBodyEncoder[A] = (a: A) => | ||
jsonToPactDslJsonBody(Json.toJson(a)) | ||
|
||
implicit def messagePactDecoder[A](implicit reads: Reads[A]): MessagePactDecoder[A] = (message: Message) => | ||
Try(Json.parse(message.contentsAsString()).as[A]).toEither | ||
|
||
implicit val providerStateFormat: Format[ProviderState] = Json.format[ProviderState] | ||
} |
82 changes: 82 additions & 0 deletions
82
play-json/src/test/java11+/pact4s/playjson/JsonConversionTests.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,82 @@ | ||
package pact4s.playjson | ||
|
||
import munit.FunSuite | ||
import pact4s.playjson.JsonConversion.jsonToPactDslJsonBody | ||
import play.api.libs.json.{JsNull, JsValue, Json} | ||
|
||
class JsonConversionTests extends FunSuite { | ||
|
||
def testRoundTrip(json: JsValue): Unit = | ||
assertEquals(Json.parse(jsonToPactDslJsonBody(json).getBody.toString), json) | ||
|
||
test("array-less JSON should round-trip with PactDslJsonBody") { | ||
val json = Json.obj( | ||
"key1" -> Json.toJson("value1"), | ||
"key2" -> Json.obj( | ||
"key2.1" -> Json.toJson(true), | ||
"key2.2" -> JsNull, | ||
"key2.3" -> Json.obj() | ||
), | ||
"key3" -> Json.toJson(1), | ||
"key4" -> Json.toJson(2.34) | ||
) | ||
|
||
testRoundTrip(json) | ||
} | ||
|
||
test("should raise exception if json is a top-level array") { | ||
val json = Json.arr( | ||
Json.toJson(1), | ||
Json.toJson(2), | ||
Json.toJson(3) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should roundtrip an empty json object") { | ||
testRoundTrip(Json.obj()) | ||
} | ||
|
||
test("should work if JSON object contains a nested simple array") { | ||
val json = Json.obj( | ||
"array" -> Json.toJson(List(1, 2, 3)) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should work if JSON object contains a nested array of objects") { | ||
val json = Json.obj( | ||
"array" -> Json.toJson( | ||
List( | ||
Json.obj("f" -> Json.toJson("g")), | ||
Json.obj("f" -> Json.toJson("h")) | ||
) | ||
) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should work if JSON object contains an array of array") { | ||
val json = Json.obj( | ||
"array" -> Json.toJson( | ||
List( | ||
Json.toJson(List(1, 2, 3)), | ||
Json.toJson(List(4, 5, 6)) | ||
) | ||
) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should encode top level string") { | ||
assertEquals(jsonToPactDslJsonBody(Json.toJson("pact4s")).getBody.asString(), "pact4s") | ||
} | ||
|
||
test("should encode top level boolean") { | ||
assertEquals(jsonToPactDslJsonBody(Json.toJson(true)).getBody.asBoolean().booleanValue(), true) | ||
} | ||
|
||
test("should encode top level number") { | ||
assertEquals(jsonToPactDslJsonBody(Json.toJson(12)).getBody.asNumber().intValue(), 12) | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
play-json/src/test/java8/pact4s/playjson/JsonConversionTests.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,82 @@ | ||
package pact4s.playjson | ||
|
||
import munit.FunSuite | ||
import pact4s.playjson.JsonConversion.jsonToPactDslJsonBody | ||
import play.api.libs.json.{JsNull, JsValue, Json} | ||
|
||
class JsonConversionTests extends FunSuite { | ||
|
||
def testRoundTrip(json: JsValue): Unit = | ||
assertEquals(Json.parse(jsonToPactDslJsonBody(json).getBody.toString), json) | ||
|
||
test("array-less JSON should round-trip with PactDslJsonBody") { | ||
val json = Json.obj( | ||
"key1" -> Json.toJson("value1"), | ||
"key2" -> Json.obj( | ||
"key2.1" -> Json.toJson(true), | ||
"key2.2" -> JsNull, | ||
"key2.3" -> Json.obj() | ||
), | ||
"key3" -> Json.toJson(1), | ||
"key4" -> Json.toJson(2.34) | ||
) | ||
|
||
testRoundTrip(json) | ||
} | ||
|
||
test("should raise exception if json is a top-level array") { | ||
val json = Json.arr( | ||
Json.toJson(1), | ||
Json.toJson(2), | ||
Json.toJson(3) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should roundtrip an empty json object") { | ||
testRoundTrip(Json.obj()) | ||
} | ||
|
||
test("should work if JSON object contains a nested simple array") { | ||
val json = Json.obj( | ||
"array" -> Json.toJson(List(1, 2, 3)) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should work if JSON object contains a nested array of objects") { | ||
val json = Json.obj( | ||
"array" -> Json.toJson( | ||
List( | ||
Json.obj("f" -> Json.toJson("g")), | ||
Json.obj("f" -> Json.toJson("h")) | ||
) | ||
) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should work if JSON object contains an array of array") { | ||
val json = Json.obj( | ||
"array" -> Json.toJson( | ||
List( | ||
Json.toJson(List(1, 2, 3)), | ||
Json.toJson(List(4, 5, 6)) | ||
) | ||
) | ||
) | ||
testRoundTrip(json) | ||
} | ||
|
||
test("should encode top level string") { | ||
assertEquals(jsonToPactDslJsonBody(Json.toJson("pact4s")).getBody.asInstanceOf[String], "pact4s") | ||
} | ||
|
||
test("should encode top level boolean") { | ||
assertEquals(jsonToPactDslJsonBody(Json.toJson(true)).getBody.asInstanceOf[Boolean], true) | ||
} | ||
|
||
test("should encode top level number") { | ||
assertEquals(jsonToPactDslJsonBody(Json.toJson(12)).getBody.asInstanceOf[BigDecimal].toInt, 12) | ||
} | ||
} |
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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should probably make this
% Provided
, otherwise we lock the user into a versionThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't lock user in a version, they can still use another version in their codebase (direct dependency or using
dependencyOverrides
) as long as the version is compatible.By the way, recent versions of sbt will let users know if they are pulling 2 incompatible (according to semver) versions.
If we put it to scope
Provided
and users use a version incompatible with what we expected in pact4s, it would break as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gaeljw right -- it's just that this is what the
Provided
scope is for, it requires the user to include this library and include a compatible version of play JSON -- rather than including one by default which needs to be excluded if the user wants to use a different version (or being evicted if they include a version in addition to the default one). It's just cleaner, IMO, and also a pattern we already use in this library.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any downside of using the
Compile
scope: on the contrary it will let sbt highlight potential incompatibilities. I might be wrong or partial butProvided
scope is more for context like Tomcat or Spark where the runtime server bring the dependencies itself. If your code needs play-json it must declare it needs it IMHO.Anyway I'll adapt to the standards/pattern of this library if needed :)
Though it's already merged and circe module is setup the same way: https://github.com/jbwheatley/pact4s/blob/main/project/Dependencies.scala#L50
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone is bringing this library into their project, they will already have a version of play-json defined. Since you include it in the
Compile
scope this means:I don't see any benefit to including it in
Compile
scope, personally. In fact, your example is the only other place where we don't include the dependency asProvided
. Not a hill I'm going to die on, maybe @jbwheatley can weigh in.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PS, I don't think there really is a right answer to this, it just depends on the dependency strategy we want to take for this library. We can either include everything in
Compile
scope and let the users handle potential evictions/conflicts, or we can useProvided
scope and force users to explicitly include a compatible version of the dependency, which I prefer simply because I think it's less complicated.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I totally understand your point. I guess it's a matter of habits/taste as well. I'll be fine with any choice :)