diff --git a/.gitignore b/.gitignore index 96ef862..21e7aba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ .idea/ +.ensime* diff --git a/.travis.yml b/.travis.yml index b8759ad..ab1a9a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,5 +21,5 @@ notifications: on_start: never script: "sbt clean coverage test" after_success: - - "sbt coveralls" + - "sbt coverageReport coveralls" - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f499e46..05b24a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,7 @@ We are happy to accept your contributions to make scala-jsonapi better and more Please do not push to the master branch directly. Always use pull requests and let people discuss changes in the pull request. Pull requests should only be merged after all discussions have been concluded and at least one reviewer has offered **approval**. +If you expect new version to be released please update `version.sbt` and `README.md` accordingly in the same pull request as changes were made. ## Guidelines diff --git a/README.md b/README.md index bdccede..d0170ee 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Coverage Status](https://coveralls.io/repos/zalando/scala-jsonapi/badge.svg?branch=master&service=github)](https://coveralls.io/github/zalando/scala-jsonapi?branch=master) [![codecov.io](https://codecov.io/github/zalando/scala-jsonapi/coverage.svg?branch=master)](https://codecov.io/github/zalando/scala-jsonapi?branch=master) [![Join the chat at https://gitter.im/zalando/scala-jsonapi](https://badges.gitter.im/zalando/scala-jsonapi.svg)](https://gitter.im/zalando/scala-jsonapi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.zalando/scala-jsonapi_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.zalando/scala-jsonapi_2.11) scala-jsonapi is a Scala library that aims to help you produce JSON output based on the [JSON API specification][jsonapi] easily and painlessly. The library is compatible with Scala version `2.11`. It supports read and write for the following backends: @@ -23,7 +24,7 @@ This library is very much a work in progress, so expect its API to change. To use scala-jsonapi, first add a library dependency—assuming that you have [sonatype resolvers] set up. - libraryDependencies += "org.zalando" %% "scala-jsonapi" % "0.5.2" + libraryDependencies += "org.zalando" %% "scala-jsonapi" % "0.6.2" You also have to provide the used backend (e.g. spray-json). @@ -101,6 +102,18 @@ In contrast there is a type class called `JsonapiRootObjectReader` that supports For complete usage, see [the specs example]. +## JSON API Links Support + +There is support for string and object links. + +To create a string "self" link: + +Links.self("href", None) + +To create an object "self" link: + +Links.self("href", Some(meta)) + # Publishing and Releasing Publishing and releasing is made with help of the [sbt-sonatype plugin]. diff --git a/build.sbt b/build.sbt index 7ba5781..c43ff53 100644 --- a/build.sbt +++ b/build.sbt @@ -44,9 +44,9 @@ lazy val `akka-http` = project.in(file("akka-http")). settings(commonSettings: _*). settings(libraryDependencies ++= akkaHttpDeps) -ScoverageSbtPlugin.ScoverageKeys.coverageMinimum := 80 +coverageMinimum := 80 -ScoverageSbtPlugin.ScoverageKeys.coverageFailOnMinimum := true +coverageFailOnMinimum := true publishMavenStyle := true diff --git a/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiDecoders.scala b/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiDecoders.scala index e733aa7..d53d10b 100644 --- a/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiDecoders.scala +++ b/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiDecoders.scala @@ -1,6 +1,5 @@ package org.zalando.jsonapi.circe -import cats.data.Xor import io.circe._ import org.zalando.jsonapi.json.FieldNames import org.zalando.jsonapi.model.JsonApiObject._ @@ -20,51 +19,95 @@ trait CirceJsonapiDecoders { }.toList) ) - implicit val valueDecoder = Decoder.instance[Value](_.as[Json].map(jsonToValue)) + implicit val valueDecoder = Decoder.instance[Value](_.as[Json].right.map(jsonToValue)) implicit val attributesDecoder = Decoder.instance[Attributes](hcursor ⇒ { - hcursor.as[Value].flatMap { + hcursor.as[Value].right.flatMap { case JsObjectValue(value) ⇒ - Xor.Right(value) + Right(value) case _ ⇒ - Xor.Left(DecodingFailure("only an object can be decoded to Attributes", hcursor.history)) + Left(DecodingFailure("only an object can be decoded to Attributes", hcursor.history)) } }) - implicit val attributeDecoder = Decoder.instance[Attribute](_.as[Attributes].map(_.head)) + implicit val attributeDecoder = Decoder.instance[Attribute](_.as[Attributes].right.map(_.head)) implicit val linksDecoder = Decoder.instance[Links](hcursor ⇒ { - hcursor.as[Value].flatMap { + hcursor.as[Value].right.flatMap { case JsObjectValue(attributes) ⇒ - Xor.Right(attributes.map { - case Attribute(FieldNames.`self`, StringValue(url)) ⇒ Links.Self(url) + Right(attributes.map { + case Attribute(FieldNames.`self`, StringValue(url)) ⇒ Links.Self(url, None) + case Attribute(FieldNames.`self`, JsObjectValue(linkAttributes)) => + val linkValues = attributesToLinkValues(linkAttributes) + Links.Self(linkValues._1, linkValues._2) + case Attribute(FieldNames.`about`, StringValue(url)) ⇒ - Links.About(url) + Links.About(url, None) + case Attribute(FieldNames.`about`, JsObjectValue(linkAttributes)) => + val linkValues = attributesToLinkValues(linkAttributes) + Links.About(linkValues._1, linkValues._2) + case Attribute(FieldNames.`first`, StringValue(url)) ⇒ - Links.First(url) - case Attribute(FieldNames.`last`, StringValue(url)) ⇒ Links.Last(url) - case Attribute(FieldNames.`next`, StringValue(url)) ⇒ Links.Next(url) - case Attribute(FieldNames.`prev`, StringValue(url)) ⇒ Links.Prev(url) + Links.First(url, None) + case Attribute(FieldNames.`first`, JsObjectValue(linkAttributes)) => + val linkValues = attributesToLinkValues(linkAttributes) + Links.First(linkValues._1, linkValues._2) + + case Attribute(FieldNames.`last`, StringValue(url)) ⇒ Links.Last(url, None) + case Attribute(FieldNames.`last`, JsObjectValue(linkAttributes)) => + val linkValues = attributesToLinkValues(linkAttributes) + Links.Last(linkValues._1, linkValues._2) + + case Attribute(FieldNames.`next`, StringValue(url)) ⇒ Links.Next(url, None) + case Attribute(FieldNames.`next`, JsObjectValue(linkAttributes)) => + val linkValues = attributesToLinkValues(linkAttributes) + Links.Next(linkValues._1, linkValues._2) + + case Attribute(FieldNames.`prev`, StringValue(url)) ⇒ Links.Prev(url, None) + case Attribute(FieldNames.`prev`, JsObjectValue(linkAttributes)) => + val linkValues = attributesToLinkValues(linkAttributes) + Links.Prev(linkValues._1, linkValues._2) + case Attribute(FieldNames.`related`, StringValue(url)) ⇒ - Links.Related(url) + Links.Related(url, None) + case Attribute(FieldNames.`related`, JsObjectValue(linkAttributes)) => + val linkValues = attributesToLinkValues(linkAttributes) + Links.Related(linkValues._1, linkValues._2) }) case _ ⇒ - Xor.Left(DecodingFailure("only an object can be decoded to Links", hcursor.history)) + Left(DecodingFailure("only an object can be decoded to Links", hcursor.history)) } }) - def jsonToData(json: Json): Xor[DecodingFailure, Data] = json match { + def attributesToLinkValues(linkObjectAttributes: Attributes): (String, Option[Meta]) = { + (linkObjectAttributes.find(_.name == "href"), linkObjectAttributes.find(_.name == "meta")) match { + case (Some(hrefAttribute), Some(metaAttribute)) => + val href = hrefAttribute match { + case Attribute("href", StringValue(url)) => url + } + val meta: Map[String, JsonApiObject.Value] = metaAttribute match { + case Attribute("meta", JsObjectValue(metaAttributes)) => + metaAttributes.map { + case Attribute(name, value) ⇒ name -> value + }.toMap + } + (href, Some(meta)) + } + } + + def jsonToData(json: Json): Either[DecodingFailure, Data] = json match { case json: Json if json.isArray ⇒ json.as[ResourceObjects] case json: Json if json.isObject ⇒ json.as[ResourceObject] } + implicit val dataDecoder = Decoder.instance[Data](_.as[Json].right.flatMap(jsonToData)) + implicit val relationshipDecoder = Decoder.instance[Relationship](hcursor ⇒ { for { - links ← hcursor.downField(FieldNames.`links`).as[Option[Links]] - // TODO: there's prolly a cleaner way here. there's a circular dependency Data -> ResourceObject(s) -> Relationship(s) -> Data that's giving circe problems - data ← hcursor.downField(FieldNames.`data`).as[Option[Json]].map(_.flatMap(jsonToData(_).toOption)) + links ← hcursor.downField(FieldNames.`links`).as[Option[Links]].right + data ← hcursor.downField(FieldNames.`data`).as[Option[Data]].right } yield Relationship( links = links, @@ -75,31 +118,31 @@ trait CirceJsonapiDecoders { implicit val relationshipsDecoder = Decoder.instance[Relationships](_.as[Map[String, Relationship]]) implicit val jsonApiDecoder = Decoder.instance[JsonApi](hcursor ⇒ { - hcursor.as[Value].flatMap { + hcursor.as[Value].right.flatMap { case JsObjectValue(attributes) ⇒ - Xor.Right(attributes.map { + Right(attributes.map { case Attribute(name, value) ⇒ JsonApiProperty(name, value) }) case _ ⇒ - Xor.Left(DecodingFailure("only an object can be decoded to JsonApi", hcursor.history)) + Left(DecodingFailure("only an object can be decoded to JsonApi", hcursor.history)) } }) implicit val metaDecoder = Decoder.instance[Meta](hcursor ⇒ { - hcursor.as[Value].flatMap { + hcursor.as[Value].right.flatMap { case JsObjectValue(attributes) ⇒ - Xor.Right(attributes.map { + Right(attributes.map { case Attribute(name, value) ⇒ name -> value }.toMap) case _ ⇒ - Xor.Left(DecodingFailure("only an object can be decoded to Meta", hcursor.history)) + Left(DecodingFailure("only an object can be decoded to Meta", hcursor.history)) } }) implicit val errorSourceDecoder = Decoder.instance[ErrorSource](hcursor ⇒ { for { - pointer ← hcursor.downField(FieldNames.`pointer`).as[Option[String]] - parameter ← hcursor.downField(FieldNames.`parameter`).as[Option[String]] + pointer ← hcursor.downField(FieldNames.`pointer`).as[Option[String]].right + parameter ← hcursor.downField(FieldNames.`parameter`).as[Option[String]].right } yield ErrorSource( pointer = pointer, @@ -109,14 +152,14 @@ trait CirceJsonapiDecoders { implicit val errorDecoder = Decoder.instance[Error](hcursor ⇒ { for { - id ← hcursor.downField(FieldNames.`id`).as[Option[String]] - status ← hcursor.downField(FieldNames.`status`).as[Option[String]] - code ← hcursor.downField(FieldNames.`code`).as[Option[String]] - title ← hcursor.downField(FieldNames.`title`).as[Option[String]] - detail ← hcursor.downField(FieldNames.`detail`).as[Option[String]] - links ← hcursor.downField(FieldNames.`links`).as[Option[Links]] - meta ← hcursor.downField(FieldNames.`meta`).as[Option[Meta]] - source ← hcursor.downField(FieldNames.`source`).as[Option[ErrorSource]] + id ← hcursor.downField(FieldNames.`id`).as[Option[String]].right + status ← hcursor.downField(FieldNames.`status`).as[Option[String]].right + code ← hcursor.downField(FieldNames.`code`).as[Option[String]].right + title ← hcursor.downField(FieldNames.`title`).as[Option[String]].right + detail ← hcursor.downField(FieldNames.`detail`).as[Option[String]].right + links ← hcursor.downField(FieldNames.`links`).as[Option[Links]].right + meta ← hcursor.downField(FieldNames.`meta`).as[Option[Meta]].right + source ← hcursor.downField(FieldNames.`source`).as[Option[ErrorSource]].right } yield Error( id = id, @@ -132,12 +175,12 @@ trait CirceJsonapiDecoders { implicit val resourceObjectDecoder = Decoder.instance[ResourceObject](hcursor ⇒ { for { - id ← hcursor.downField(FieldNames.`id`).as[Option[String]] - `type` ← hcursor.downField(FieldNames.`type`).as[String] - attributes ← hcursor.downField(FieldNames.`attributes`).as[Option[Attributes]] - relationships ← hcursor.downField(FieldNames.`relationships`).as[Option[Relationships]] - links ← hcursor.downField(FieldNames.`links`).as[Option[Links]] - meta ← hcursor.downField(FieldNames.`meta`).as[Option[Meta]] + id ← hcursor.downField(FieldNames.`id`).as[Option[String]].right + `type` ← hcursor.downField(FieldNames.`type`).as[String].right + attributes ← hcursor.downField(FieldNames.`attributes`).as[Option[Attributes]].right + relationships ← hcursor.downField(FieldNames.`relationships`).as[Option[Relationships]].right + links ← hcursor.downField(FieldNames.`links`).as[Option[Links]].right + meta ← hcursor.downField(FieldNames.`meta`).as[Option[Meta]].right } yield ResourceObject( id = id, @@ -150,20 +193,18 @@ trait CirceJsonapiDecoders { }) implicit val resourceObjectsDecoder = - Decoder.instance[ResourceObjects](_.as[List[ResourceObject]].map(ResourceObjects)) - - implicit val dataDecoder = Decoder.instance[Data](_.as[Json].flatMap(jsonToData)) + Decoder.instance[ResourceObjects](_.as[List[ResourceObject]].right.map(ResourceObjects)) - implicit val includedDecoder = Decoder.instance[Included](_.as[ResourceObjects].map(Included.apply)) + implicit val includedDecoder = Decoder.instance[Included](_.as[ResourceObjects].right.map(Included.apply)) implicit val rootObjectDecoder = Decoder.instance[RootObject](hcursor ⇒ { for { - data ← hcursor.downField(FieldNames.`data`).as[Option[Data]] - links ← hcursor.downField(FieldNames.`links`).as[Option[Links]] - errors ← hcursor.downField(FieldNames.`errors`).as[Option[Errors]] - meta ← hcursor.downField(FieldNames.`meta`).as[Option[Meta]] - included ← hcursor.downField(FieldNames.`included`).as[Option[Included]] - jsonapi ← hcursor.downField(FieldNames.`jsonapi`).as[Option[JsonApi]] + data ← hcursor.downField(FieldNames.`data`).as[Option[Data]].right + links ← hcursor.downField(FieldNames.`links`).as[Option[Links]].right + errors ← hcursor.downField(FieldNames.`errors`).as[Option[Errors]].right + meta ← hcursor.downField(FieldNames.`meta`).as[Option[Meta]].right + included ← hcursor.downField(FieldNames.`included`).as[Option[Included]].right + jsonapi ← hcursor.downField(FieldNames.`jsonapi`).as[Option[JsonApi]].right } yield RootObject( data = data, diff --git a/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiEncoders.scala b/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiEncoders.scala index 07c4ab9..b9ab6b2 100644 --- a/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiEncoders.scala +++ b/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiEncoders.scala @@ -47,16 +47,34 @@ trait CirceJsonapiEncoders { } implicit val linkEncoder = Encoder.instance[Link] { link => - val (name: String, value: String) = link match { - case Links.Self(url) => FieldNames.`self` -> url - case Links.About(url) => FieldNames.`about` -> url - case Links.First(url) => FieldNames.`first` -> url - case Links.Last(url) => FieldNames.`last` -> url - case Links.Next(url) => FieldNames.`next` -> url - case Links.Prev(url) => FieldNames.`prev` -> url - case Links.Related(url) => FieldNames.`related` -> url + val (name: String, href: String, metaOpt: Option[Meta]) = link match { + case Links.Self(url, None) => (FieldNames.`self`, url, None) + case Links.Self(url, Some(meta)) => (FieldNames.`self`, url, Some(meta)) + + case Links.About(url, None) => (FieldNames.`about`, url, None) + case Links.About(url, Some(meta)) => (FieldNames.`about`, url, Some(meta)) + + case Links.First(url, None) => (FieldNames.`first`, url, None) + case Links.First(url, Some(meta)) => (FieldNames.`first`, url, Some(meta)) + + case Links.Last(url, None) => (FieldNames.`last`, url, None) + case Links.Last(url, Some(meta)) => (FieldNames.`last`, url, Some(meta)) + + case Links.Next(url, None) => (FieldNames.`next`, url, None) + case Links.Next(url, Some(meta)) => (FieldNames.`next`, url, Some(meta)) + + case Links.Prev(url, None) => (FieldNames.`prev`, url, None) + case Links.Prev(url, Some(meta)) => (FieldNames.`prev`, url, Some(meta)) + + case Links.Related(url, None) => (FieldNames.`related`, url, None) + case Links.Related(url, Some(meta)) => (FieldNames.`related`, url, Some(meta)) + } + metaOpt match { + case None => Json.fromFields(Seq(name -> Json.fromString(href))) + case Some(meta) => + val linkObjectJson = Json.fromFields(Seq("href" -> Json.fromString(href), "meta" -> meta.asJson)) + Json.fromFields(Seq(name -> linkObjectJson)) } - Json.fromFields(Seq(name -> Json.fromString(value))) } implicit val linksEncoder = Encoder.instance[Links](_.map(_.asJson).reduce(_.deepMerge(_))) diff --git a/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiSupport.scala b/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiSupport.scala index 8824067..4e19282 100644 --- a/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiSupport.scala +++ b/circe/src/main/scala/org/zalando/jsonapi/circe/CirceJsonapiSupport.scala @@ -18,7 +18,7 @@ trait CirceJsonapiSupport extends CirceJsonapiEncoders with CirceJsonapiDecoders implicit val circeJsonapiUnmarshaller = Unmarshaller.delegate[String, RootObject]( `application/vnd.api+json`, `application/json` - )(decode[RootObject](_).toOption.get) + )(decode[RootObject](_).right.get) } object CirceJsonapiSupport extends CirceJsonapiSupport diff --git a/circe/src/test/scala/org/zalando/jsonapi/circe/CirceJsonapiFormatSpec.scala b/circe/src/test/scala/org/zalando/jsonapi/circe/CirceJsonapiFormatSpec.scala index 3b218c2..a276fc9 100644 --- a/circe/src/test/scala/org/zalando/jsonapi/circe/CirceJsonapiFormatSpec.scala +++ b/circe/src/test/scala/org/zalando/jsonapi/circe/CirceJsonapiFormatSpec.scala @@ -9,8 +9,8 @@ import org.zalando.jsonapi.model._ class CirceJsonapiFormatSpec extends JsonBaseSpec[Json] with MustMatchers with CirceJsonapiEncoders with CirceJsonapiDecoders { - override protected def parseJson(jsonString: String): Json = parse(jsonString).toOption.get - protected def decodeJson[T](json: Json)(implicit d: io.circe.Decoder[T]): T = json.as[T].toOption.get + override protected def parseJson(jsonString: String): Json = parse(jsonString).right.get + protected def decodeJson[T](json: Json)(implicit d: io.circe.Decoder[T]): T = json.as[T].right.get "CirceJsonapiFormat" when { "serializing Jsonapi" must { @@ -29,8 +29,14 @@ class CirceJsonapiFormatSpec extends JsonBaseSpec[Json] with MustMatchers with C "transform a list of resource identifier objects correctly" in { rootObjectWithResourceIdentifierObjects.asJson mustEqual rootObjectWithResourceIdentifierObjectsJson } - "transform all link types correctly" in { - rootObjectWithResourceObjectsWithAllLinks.asJson mustEqual rootObjectWithResourceObjectsWithAllLinksJson + "transform all string link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsStrings.asJson mustEqual rootObjectWithResourceObjectsWithAllLinksAsStringsJson + } + "transform all object link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsObjects.asJson mustEqual rootObjectWithResourceObjectsWithAllLinksAsObjectsJson + } + "transform all string and object link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjects.asJson mustEqual rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJson } "transform all meta object inside resource object correctly" in { rootObjectWithResourceObjectsWithMeta.asJson mustEqual rootObjectWithResourceObjectsWithMetaJson @@ -73,8 +79,14 @@ class CirceJsonapiFormatSpec extends JsonBaseSpec[Json] with MustMatchers with C "transform a list of resource identifier objects correctly" in { decodeJson[RootObject](rootObjectWithResourceIdentifierObjectsJson) === rootObjectWithResourceIdentifierObjects } - "transform all link types correctly" in { - decodeJson[RootObject](rootObjectWithResourceObjectsWithAllLinksJson) === rootObjectWithResourceObjectsWithAllLinks + "transform all string link types correctly" in { + decodeJson[RootObject](rootObjectWithResourceObjectsWithAllLinksAsStringsJson) === rootObjectWithResourceObjectsWithAllLinksAsStrings + } + "transform all object link types correctly" in { + decodeJson[RootObject](rootObjectWithResourceObjectsWithAllLinksAsObjectsJson) === rootObjectWithResourceObjectsWithAllLinksAsObjects + } + "transform all string and object link types correctly" in { + decodeJson[RootObject](rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJson) === rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjects } "transform all meta object inside resource object correctly" in { decodeJson[RootObject](rootObjectWithResourceObjectsWithMetaJson) === rootObjectWithResourceObjectsWithMeta diff --git a/core/src/main/scala/org/zalando/jsonapi/model/implicits/AttributeConversions.scala b/core/src/main/scala/org/zalando/jsonapi/model/implicits/AttributeConversions.scala new file mode 100644 index 0000000..40222f6 --- /dev/null +++ b/core/src/main/scala/org/zalando/jsonapi/model/implicits/AttributeConversions.scala @@ -0,0 +1,16 @@ +package org.zalando.jsonapi.model.implicits + +import scala.language.implicitConversions + +import org.zalando.jsonapi.model.Attribute +import org.zalando.jsonapi.model.implicits.JsonApiObjectValueConversions._ + +object AttributeConversions { + implicit def convertPairToOptionalAttribute(pair: (String, Option[_])): Option[Attribute] = { + pair._2.map(Attribute(pair._1, _)) + } + + implicit def convertPairToAttribute(pair: (String, _)): Attribute = { + Attribute(pair._1, pair._2) + } +} diff --git a/core/src/main/scala/org/zalando/jsonapi/model/implicits/JsonApiObjectValueConversions.scala b/core/src/main/scala/org/zalando/jsonapi/model/implicits/JsonApiObjectValueConversions.scala new file mode 100644 index 0000000..84ab1ee --- /dev/null +++ b/core/src/main/scala/org/zalando/jsonapi/model/implicits/JsonApiObjectValueConversions.scala @@ -0,0 +1,44 @@ +package org.zalando.jsonapi.model.implicits + +import org.zalando.jsonapi.model.Attribute +import org.zalando.jsonapi.model.JsonApiObject._ + +import scala.language.implicitConversions + +object JsonApiObjectValueConversions { + implicit def convertAnyToValue(any: Any): Value = { + any match { + case(string: String) ⇒ + StringValue(string) + case(int: Int) ⇒ + NumberValue(int) + case(long: Long) ⇒ + NumberValue(long) + case(double: Double) ⇒ + NumberValue(double) + case(float: Float) ⇒ + NumberValue(float) + case true ⇒ + TrueValue + case false ⇒ + FalseValue + case null ⇒ + NullValue + case value: Value ⇒ + value + case(map: Map[_,_]) ⇒ + JsObjectValue( + map.map { + case(name: String, value) ⇒ + Attribute(name, convertAnyToValue(value)) + case _ ⇒ + throw UnconvertibleTypeError("Maps must have string keys to be converted to JsonApiObject Values") + }.toList + ) + case(seq: Seq[_]) ⇒ + JsArrayValue(seq.map(convertAnyToValue)) + case _ ⇒ + throw UnconvertibleTypeError(any) + } + } +} diff --git a/core/src/main/scala/org/zalando/jsonapi/model/implicits/UnconvertibleTypeError.scala b/core/src/main/scala/org/zalando/jsonapi/model/implicits/UnconvertibleTypeError.scala new file mode 100644 index 0000000..bdca1e0 --- /dev/null +++ b/core/src/main/scala/org/zalando/jsonapi/model/implicits/UnconvertibleTypeError.scala @@ -0,0 +1,9 @@ +package org.zalando.jsonapi.model.implicits + +case class UnconvertibleTypeError(msg: String) extends Exception(msg) + +object UnconvertibleTypeError { + def apply(any: Any): UnconvertibleTypeError = { + UnconvertibleTypeError(s"Can not convert ${any.getClass.getName} to JsonApiObject Value") + } +} diff --git a/core/src/main/scala/org/zalando/jsonapi/model/package.scala b/core/src/main/scala/org/zalando/jsonapi/model/package.scala index 56d45aa..c1ed65c 100644 --- a/core/src/main/scala/org/zalando/jsonapi/model/package.scala +++ b/core/src/main/scala/org/zalando/jsonapi/model/package.scala @@ -54,44 +54,51 @@ package object model { /** * A link of the "self" type. * @param url The url to link to. + * @param meta The optional meta to link to. */ - case class Self(url: String) extends Link + case class Self(url: String, meta: Option[Meta]) extends Link /** * A link of the "related" type. * @param url The url to link to. + * @param meta The optional meta to link to. */ - case class Related(url: String) extends Link + case class Related(url: String, meta: Option[Meta]) extends Link /** * A link of the "first" type. * @param url The url to link to. + * @param meta The optional meta to link to. */ - case class First(url: String) extends Link + case class First(url: String, meta: Option[Meta]) extends Link /** * A link of the "last" type. * @param url The url to link to. + * @param meta The optional meta to link to. */ - case class Last(url: String) extends Link + case class Last(url: String, meta: Option[Meta]) extends Link /** * A link of the "next" type. * @param url The url to link to. + * @param meta The optional meta to link to. */ - case class Next(url: String) extends Link + case class Next(url: String, meta: Option[Meta]) extends Link /** * A link of the "prev" type. * @param url The url to link to. + * @param meta The optional meta to link to. */ - case class Prev(url: String) extends Link + case class Prev(url: String, meta: Option[Meta]) extends Link /** * A link of the "about" type. * @param url The url to link to. + * @param meta The optional meta to link to. */ - case class About(url: String) extends Link + case class About(url: String, meta: Option[Meta]) extends Link } /** @@ -203,6 +210,15 @@ package object model { */ case object NullValue extends Value + /** + * An attribute value that is true + */ + val TrueValue = BooleanValue(true) + + /** + * An attribute value that is false + */ + val FalseValue = BooleanValue(false) } /** diff --git a/core/src/test/scala/org/zalando/jsonapi/json/JsonBaseSpec.scala b/core/src/test/scala/org/zalando/jsonapi/json/JsonBaseSpec.scala index 0470f3f..f48b002 100644 --- a/core/src/test/scala/org/zalando/jsonapi/json/JsonBaseSpec.scala +++ b/core/src/test/scala/org/zalando/jsonapi/json/JsonBaseSpec.scala @@ -18,7 +18,11 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { protected lazy val rootObjectWithResourceIdentifierObjectsJson = parseJson(rootObjectWithResourceIdentifierObjectsJsonString) - protected lazy val rootObjectWithResourceObjectsWithAllLinksJson = parseJson(rootObjectWithResourceObjectsWithAllLinksJsonString) + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsStringsJson = parseJson(rootObjectWithResourceObjectsWithAllLinksAsStringsJsonString) + + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsObjectsJson = parseJson(rootObjectWithResourceObjectsWithAllLinksAsObjectsJsonString) + + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJson = parseJson(rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJsonString) protected lazy val rootObjectWithResourceObjectsWithMetaJson = parseJson(rootObjectWithResourceObjectsWithMetaJsonString) @@ -101,9 +105,9 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { ResourceObject( `type` = "person", attributes = Some(List(Attribute("name", StringValue("foobar")))), - links = Some(List(Links.Self("/persons/1"))) + links = Some(List(Links.Self("/persons/1", None))) )))), - links = Some(List(Links.Next("/persons/2"))) + links = Some(List(Links.Next("/persons/2", None))) ) protected lazy val rootObjectWithResourceIdentifierObjectJsonString = @@ -140,13 +144,13 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { ResourceObject(`type` = "cat", id = Some("felix")) )))) - protected lazy val rootObjectWithResourceObjectsWithAllLinksJsonString = + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsStringsJsonString = """ |{ | "data": [{ | "type": "person", | "links": { - | "self": "/persons/2", + | "self" : "/someUrl", | "related": "/persons/10", | "next": "/persons/3", | "prev": "/persons/1", @@ -158,16 +162,211 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { |} """.stripMargin - protected lazy val rootObjectWithResourceObjectsWithAllLinks = RootObject(Some(ResourceObjects(List( + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsObjectsJsonString = + """ + |{ + | "data": [{ + | "type": "person", + | "links": { + | "self" : { + | "href" : "/someUrl", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | "one", + | "two" + | ] + | } + | }, + | "related": { + | "href" : "/persons/10", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | "three", + | "four" + | ] + | } + | }, + | "next": { + | "href" : "/persons/3", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | "five", + | "six" + | ] + | } + | }, + | "prev": { + | "href" : "/persons/1", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | "seven", + | "eight" + | ] + | } + | }, + | "about": { + | "href" : "/persons/11", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | "nine", + | "ten" + | ] + | } + | }, + | "first": { + | "href" : "/persons/0", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | 11, + | 12 + | ] + | } + | }, + | "last": { + | "href" : "/persons/99", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | 13, + | 14 + | ] + | } + | } + | } + | }] + |} + """.stripMargin + + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJsonString = + """ + |{ + | "data": [{ + | "type": "person", + | "links": { + | "self" : { + | "href" : "/someUrl", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | "one", + | "two" + | ] + | } + | }, + | "related": "/persons/10", + | "next": "/persons/3", + | "prev": "/persons/1", + | "about" : { + | "href" : "/persons/11", + | "meta" : { + | "foo" : "bar", + | "array" : [ + | 11, + | 12 + | ] + | } + | }, + | "first": "/persons/0", + | "last": "/persons/99" + | } + | }] + |} + """.stripMargin + + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsStrings = RootObject(Some(ResourceObjects(List( + ResourceObject(`type` = "person", links = Some( + List( + Links.Self("/someUrl", None), + Links.Related("/persons/10", None), + Links.Next("/persons/3", None), + Links.Prev("/persons/1", None), + Links.About("/persons/11", None), + Links.First("/persons/0", None), + Links.Last("/persons/99", None) + ))))))) + + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsObjects = RootObject(Some(ResourceObjects(List( + ResourceObject(`type` = "person", links = Some( + List( + Links.Self( + url = "/someUrl", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(StringValue("one"), StringValue("two"))) + )) + ), + Links.Related( + url = "/persons/10", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(StringValue("three"), StringValue("four"))) + )) + ), + Links.Next( + url = "/persons/3", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(StringValue("five"), StringValue("six"))) + )) + ), + Links.Prev( + url = "/persons/1", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(StringValue("seven"), StringValue("eight"))) + )) + ), + Links.About( + url = "/persons/11", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(StringValue("nine"), StringValue("ten"))) + )) + ), + Links.First( + url = "/persons/0", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(NumberValue(11), NumberValue(12))) + )) + ), + Links.Last( + url = "/persons/99", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(NumberValue(13), NumberValue(14))) + )) + ) + ))))))) + + protected lazy val rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjects = RootObject(Some(ResourceObjects(List( ResourceObject(`type` = "person", links = Some( List( - Links.Self("/persons/2"), - Links.Related("/persons/10"), - Links.Next("/persons/3"), - Links.Prev("/persons/1"), - Links.About("/persons/11"), - Links.First("/persons/0"), - Links.Last("/persons/99") + Links.Self( + url = "/someUrl", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(StringValue("one"), StringValue("two"))) + )) + ), + Links.Related("/persons/10", None), + Links.Next("/persons/3", None), + Links.Prev("/persons/1", None), + Links.About( + url = "/persons/11", + meta = Some(Map( + "foo" -> StringValue("bar"), + "array" -> JsArrayValue(List(NumberValue(11), NumberValue(12))) + )) + ), + Links.First("/persons/0", None), + Links.Last("/persons/99", None) ))))))) protected lazy val rootObjectWithResourceObjectsWithMetaJsonString = @@ -246,7 +445,7 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { data = None, errors = Some(List(Error( id = Some("1"), - links = Some(List(Links.Self("self-link"))), + links = Some(List(Links.Self("self-link", None))), status = Some("status1"), code = Some("code1"), title = Some("title1"), @@ -351,7 +550,7 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { relationships = Some(Map("father" -> Relationship( data = Some(ResourceObject(`type` = "person")), - links = Some(List(Links.Self("http://link.to.self"))) + links = Some(List(Links.Self("http://link.to.self", None))) ))) ) )) @@ -384,7 +583,7 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { relationships = Some(Map("father" -> Relationship( data = Some(ResourceObject(`type` = "person")), - links = Some(List(Links.Self("http://link.to.self"))) + links = Some(List(Links.Self("http://link.to.self", None))) ))) ) @@ -405,7 +604,7 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec { protected lazy val relationshipsObject = Map( "father" -> Relationship( data = Some(ResourceObject(`type` = "person")), - links = Some(List(Links.Self("http://link.to.self"))) + links = Some(List(Links.Self("http://link.to.self", None))) ) ) diff --git a/core/src/test/scala/org/zalando/jsonapi/model/implicits/AttributeConversionsSpec.scala b/core/src/test/scala/org/zalando/jsonapi/model/implicits/AttributeConversionsSpec.scala new file mode 100644 index 0000000..9797598 --- /dev/null +++ b/core/src/test/scala/org/zalando/jsonapi/model/implicits/AttributeConversionsSpec.scala @@ -0,0 +1,56 @@ +package org.zalando.jsonapi.model.implicits + +import org.scalatest.{Matchers, WordSpec} +import org.zalando.jsonapi.model.Attribute +import org.zalando.jsonapi.model.JsonApiObject._ +import org.zalando.jsonapi.model.implicits.AttributeConversions._ + +class AttributeConversionsSpec extends WordSpec with Matchers { + "scala tuples" should { + "be converted to string attributes" in { + convertPairToAttribute("name" → "string") should be(Attribute("name", StringValue("string"))) + } + "be converted to number attributes" in { + convertPairToAttribute("name" → 42) should be(Attribute("name", NumberValue(42))) + convertPairToAttribute("name" → 42l) should be(Attribute("name", NumberValue(42))) + convertPairToAttribute("name" → 42f) should be(Attribute("name", NumberValue(42))) + convertPairToAttribute("name" → 42d) should be(Attribute("name", NumberValue(42))) + } + "be converted to boolean attributes" in { + convertPairToAttribute("name" → true) should be(Attribute("name", TrueValue)) + convertPairToAttribute("name" → false) should be(Attribute("name", FalseValue)) + } + "be converted to null attributes" in { + convertPairToAttribute(("name" → null)) should be(Attribute("name", NullValue)) + } + "be converted to js object attributes" in { + convertPairToAttribute(("name" → Map("null" → null))) should be(Attribute("name", JsObjectValue(List(Attribute("null", NullValue))))) + } + "be converted to js array attributes" in { + convertPairToAttribute(("name" → List(null))) should be(Attribute("name", JsArrayValue(List(NullValue)))) + } + + "be converted to optional string attributes" in { + convertPairToOptionalAttribute("name" → Option("string")) should be(Option(Attribute("name", StringValue("string")))) + } + "be converted to optional number attributes" in { + convertPairToOptionalAttribute("name" → Option(42)) should be(Option(Attribute("name", NumberValue(42)))) + convertPairToOptionalAttribute("name" → Option(42l)) should be(Option(Attribute("name", NumberValue(42)))) + convertPairToOptionalAttribute("name" → Option(42f)) should be(Option(Attribute("name", NumberValue(42)))) + convertPairToOptionalAttribute("name" → Option(42d)) should be(Option(Attribute("name", NumberValue(42)))) + } + "be converted to optional boolean attributes" in { + convertPairToOptionalAttribute("name" → Option(true)) should be(Option(Attribute("name", BooleanValue(true)))) + convertPairToOptionalAttribute("name" → Option(false)) should be(Option(Attribute("name", BooleanValue(false)))) + } + "be converted to optional null attributes" in { + convertPairToOptionalAttribute(("name" → Option(null))) should be(None) + } + "be converted to optional js object attributes" in { + convertPairToOptionalAttribute(("name" → Option(Map("null" → null)))) should be(Option(Attribute("name", JsObjectValue(List(Attribute("null", NullValue)))))) + } + "be converted to optional js array attributes" in { + convertPairToOptionalAttribute(("name" → Option(List(null)))) should be(Option(Attribute("name", JsArrayValue(List(NullValue))))) + } + } +} diff --git a/core/src/test/scala/org/zalando/jsonapi/model/implicits/JsonApiObjectValueConversionsSpec.scala b/core/src/test/scala/org/zalando/jsonapi/model/implicits/JsonApiObjectValueConversionsSpec.scala new file mode 100644 index 0000000..d15a37b --- /dev/null +++ b/core/src/test/scala/org/zalando/jsonapi/model/implicits/JsonApiObjectValueConversionsSpec.scala @@ -0,0 +1,65 @@ +package org.zalando.jsonapi.model.implicits + +import org.scalatest.{Matchers, WordSpec} +import org.zalando.jsonapi.model.Attribute +import org.zalando.jsonapi.model.JsonApiObject._ +import org.zalando.jsonapi.model.implicits.JsonApiObjectValueConversions._ + +class JsonApiObjectValueConversionsSpec extends WordSpec with Matchers { + "scala values" should { + "be converted to string values" in { + convertAnyToValue("string") should be(StringValue("string")) + } + "be converted to number values" in { + convertAnyToValue(42) should be(NumberValue(42)) + convertAnyToValue(42l) should be(NumberValue(42)) + convertAnyToValue(42f) should be(NumberValue(42)) + convertAnyToValue(42d) should be(NumberValue(42)) + } + "be converted to boolean values" in { + convertAnyToValue(true) should be(TrueValue) + convertAnyToValue(false) should be(FalseValue) + } + "be converted to js array values" in { + convertAnyToValue(Seq("one", 2, Map("3" → 4d), false, null)) should be(JsArrayValue(List( + StringValue("one"), + NumberValue(2), + JsObjectValue(List(Attribute("3", NumberValue(4d)))), + FalseValue, + NullValue + ))) + } + "be converted to js object values" in { + convertAnyToValue(Map( + "one" → 2, + "3" → List(4f, true, null) + )) should be(JsObjectValue(List( + Attribute("one", NumberValue(2)), + Attribute("3", JsArrayValue(List( + NumberValue(4f), + TrueValue, + NullValue + ))) + ))) + } + "be converted to null values" in { + convertAnyToValue(null) should be(NullValue) + } + "be left alone" in { + for { + value ← Seq(NullValue, TrueValue, FalseValue, StringValue("value"), NumberValue(1)) + } { + convertAnyToValue(value) should be (value) + } + } + "throw an error for unconvertible types" in { + the[UnconvertibleTypeError] thrownBy { + convertAnyToValue(Map(1 → 2)) + } should have message "Maps must have string keys to be converted to JsonApiObject Values" + + the[UnconvertibleTypeError] thrownBy { + convertAnyToValue(new java.util.Date) + } should have message "Can not convert java.util.Date to JsonApiObject Value" + } + } +} diff --git a/play/src/main/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormat.scala b/play/src/main/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormat.scala index a1ed161..4d6d7f0 100644 --- a/play/src/main/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormat.scala +++ b/play/src/main/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormat.scala @@ -218,29 +218,96 @@ trait PlayJsonJsonapiFormat { override def writes(links: Links): JsValue = { val fields = links.map { _ match { - case Links.About(u) ⇒ (FieldNames.`about`, JsString(u)) - case Links.First(u) ⇒ (FieldNames.`first`, JsString(u)) - case Links.Last(u) ⇒ (FieldNames.`last`, JsString(u)) - case Links.Next(u) ⇒ (FieldNames.`next`, JsString(u)) - case Links.Prev(u) ⇒ (FieldNames.`prev`, JsString(u)) - case Links.Related(u) ⇒ (FieldNames.`related`, JsString(u)) - case Links.Self(u) ⇒ (FieldNames.`self`, JsString(u)) + case Links.About(u, None) ⇒ (FieldNames.`about`, JsString(u)) + case Links.About(u, Some(meta)) ⇒ linkValuesToJson(FieldNames.`about`, u, meta) + + case Links.First(u, None) ⇒ (FieldNames.`first`, JsString(u)) + case Links.First(u, Some(meta)) ⇒ linkValuesToJson(FieldNames.`first`, u, meta) + + case Links.Last(u, None) ⇒ (FieldNames.`last`, JsString(u)) + case Links.Last(u, Some(meta)) ⇒ linkValuesToJson(FieldNames.`last`, u, meta) + + case Links.Next(u, None) ⇒ (FieldNames.`next`, JsString(u)) + case Links.Next(u, Some(meta)) ⇒ linkValuesToJson(FieldNames.`next`, u, meta) + + case Links.Prev(u, None) ⇒ (FieldNames.`prev`, JsString(u)) + case Links.Prev(u, Some(meta)) ⇒ linkValuesToJson(FieldNames.`prev`, u, meta) + + case Links.Related(u, None) ⇒ (FieldNames.`related`, JsString(u)) + case Links.Related(u, Some(meta)) ⇒ linkValuesToJson(FieldNames.`related`, u, meta) + + case Links.Self(u, None) ⇒ (FieldNames.`self`, JsString(u)) + case Links.Self(u, Some(meta)) ⇒ linkValuesToJson(FieldNames.`self`, u, meta) } } JsObject(fields) } + def linkValuesToJson(name: String, href: String, meta: Meta): (String, JsValue) = { + (name, JsObject( + Seq( + ("href", JsString(href)), + ("meta", Json.toJson(meta)) + ) + )) + } + + def jsonToLinkValues(linkObjectJson: Seq[(String, JsValue)]): (String, Option[Meta]) = { + (linkObjectJson.find(_._1 == "href"), linkObjectJson.find(_._1 == "meta")) match { + case(Some(hrefJson), Some(metaJson)) => + val href = hrefJson match { + case ("href", JsString(hrefStr)) => hrefStr + } + val meta: Map[String, JsonApiObject.Value] = metaJson match { + case ("meta", JsObject(metaObjectJson)) => + metaObjectJson.map { + case (name, value) => + (name, value.as[JsonApiObject.Value]) + + }.toMap + } + (href, Some(meta)) + } + } + override def reads(json: JsValue): JsResult[Links] = json match { case JsObject(o) ⇒ JsSuccess(o.map { keyValue ⇒ keyValue match { - case (FieldNames.`about`, JsString(u)) ⇒ Links.About(u) - case (FieldNames.`first`, JsString(u)) ⇒ Links.First(u) - case (FieldNames.`last`, JsString(u)) ⇒ Links.Last(u) - case (FieldNames.`next`, JsString(u)) ⇒ Links.Next(u) - case (FieldNames.`prev`, JsString(u)) ⇒ Links.Prev(u) - case (FieldNames.`related`, JsString(u)) ⇒ Links.Related(u) - case (FieldNames.`self`, JsString(u)) ⇒ Links.Self(u) + case (FieldNames.`about`, JsString(u)) ⇒ Links.About(u, None) + case (FieldNames.`about`, JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.About(linkValues._1, linkValues._2) + + case (FieldNames.`first`, JsString(u)) ⇒ Links.First(u, None) + case (FieldNames.`first`, JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.First(linkValues._1, linkValues._2) + + case (FieldNames.`last`, JsString(u)) ⇒ Links.Last(u, None) + case (FieldNames.`last`, JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Last(linkValues._1, linkValues._2) + + case (FieldNames.`next`, JsString(u)) ⇒ Links.Next(u, None) + case (FieldNames.`next`, JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Next(linkValues._1, linkValues._2) + + case (FieldNames.`prev`, JsString(u)) ⇒ Links.Prev(u, None) + case (FieldNames.`prev`, JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Prev(linkValues._1, linkValues._2) + + case (FieldNames.`related`, JsString(u)) ⇒ Links.Related(u, None) + case (FieldNames.`related`, JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Related(linkValues._1, linkValues._2) + + case (FieldNames.`self`, JsString(u)) ⇒ Links.Self(u, None) + case (FieldNames.`self`, JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Self(linkValues._1, linkValues._2) } }.toVector) case _ ⇒ JsError("error.expected.links") diff --git a/play/src/test/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormatSpec.scala b/play/src/test/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormatSpec.scala index db84f63..6dd00c8 100644 --- a/play/src/test/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormatSpec.scala +++ b/play/src/test/scala/org/zalando/jsonapi/json/playjson/PlayJsonJsonapiFormatSpec.scala @@ -27,8 +27,14 @@ class PlayJsonJsonapiFormatSpec extends JsonBaseSpec[JsValue] with MustMatchers "transform a list of resource identifier objects correctly" in { Json.toJson(rootObjectWithResourceIdentifierObjects) mustEqual rootObjectWithResourceIdentifierObjectsJson } - "transform all link types correctly" in { - Json.toJson(rootObjectWithResourceObjectsWithAllLinks) mustEqual rootObjectWithResourceObjectsWithAllLinksJson + "transform all string link types correctly" in { + Json.toJson(rootObjectWithResourceObjectsWithAllLinksAsStrings) mustEqual rootObjectWithResourceObjectsWithAllLinksAsStringsJson + } + "transform all object link types correctly" in { + Json.toJson(rootObjectWithResourceObjectsWithAllLinksAsObjects) mustEqual rootObjectWithResourceObjectsWithAllLinksAsObjectsJson + } + "transform all string and object link types correctly" in { + Json.toJson(rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjects) mustEqual rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJson } "transform all meta object inside resource object correctly" in { Json.toJson(rootObjectWithResourceObjectsWithMeta) mustEqual rootObjectWithResourceObjectsWithMetaJson @@ -71,8 +77,14 @@ class PlayJsonJsonapiFormatSpec extends JsonBaseSpec[JsValue] with MustMatchers "transform a list of resource identifier objects correctly" in { Json.fromJson[RootObject](rootObjectWithResourceIdentifierObjectsJson) mustEqual JsSuccess(rootObjectWithResourceIdentifierObjects) } - "transform all link types correctly" in { - Json.fromJson[RootObject](rootObjectWithResourceObjectsWithAllLinksJson) mustEqual JsSuccess(rootObjectWithResourceObjectsWithAllLinks) + "transform all string link types correctly" in { + Json.fromJson[RootObject](rootObjectWithResourceObjectsWithAllLinksAsStringsJson) mustEqual JsSuccess(rootObjectWithResourceObjectsWithAllLinksAsStrings) + } + "transform all object link types correctly" in { + Json.fromJson[RootObject](rootObjectWithResourceObjectsWithAllLinksAsObjectsJson) mustEqual JsSuccess(rootObjectWithResourceObjectsWithAllLinksAsObjects) + } + "transform all string and object link types correctly" in { + Json.fromJson[RootObject](rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJson) mustEqual JsSuccess(rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjects) } "transform all meta object inside resource object correctly" in { Json.fromJson[RootObject](rootObjectWithResourceObjectsWithMetaJson) mustEqual JsSuccess(rootObjectWithResourceObjectsWithMeta) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d7fbc44..c1f48e5 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,12 +2,12 @@ import sbt._ object Dependencies { // Versions - lazy val circeVersion = "0.5.0-M1" - lazy val akkaVersion = "2.4.7" + lazy val circeVersion = "0.7.0" + lazy val akkaVersion = "2.4.8" lazy val sprayJsonVersion = "1.3.2" lazy val sprayHttpxVersion = "1.3.3" - lazy val playJsonVersion = "2.3.8" - lazy val scalatestVersion = "2.2.4" + lazy val playJsonVersion = "2.3.10" + lazy val scalatestVersion = "3.0.0" // Libraries lazy val sprayJson = "io.spray" %% "spray-json" % sprayJsonVersion @@ -32,4 +32,4 @@ object Dependencies { lazy val sprayJsonDeps = Seq(sprayJson) lazy val akkaHttpDeps = Seq(akkaHttpCore, akkaHttpExperimental, akkaHttpTestkit, sprayJson) -} \ No newline at end of file +} diff --git a/project/build.properties b/project/build.properties index 59e7c05..7d789d4 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.11 \ No newline at end of file +sbt.version=0.13.12 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 84d2ba3..1481ddc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,8 @@ resolvers += Classpaths.sbtPluginReleases -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.0.4") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.5") -addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.0.0") +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.1.0") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1") diff --git a/spray-json/src/main/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormat.scala b/spray-json/src/main/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormat.scala index bf5c720..a1b801c 100644 --- a/spray-json/src/main/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormat.scala +++ b/spray-json/src/main/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormat.scala @@ -239,26 +239,105 @@ trait SprayJsonJsonapiFormat { self: DefaultJsonProtocol ⇒ override def write(links: Links): JsValue = { val fields = links map (l ⇒ l match { - case Links.Self(url) ⇒ "self" -> url.toJson - case Links.About(url) ⇒ "about" -> url.toJson - case Links.First(url) ⇒ "first" -> url.toJson - case Links.Last(url) ⇒ "last" -> url.toJson - case Links.Next(url) ⇒ "next" -> url.toJson - case Links.Prev(url) ⇒ "prev" -> url.toJson - case Links.Related(url) ⇒ "related" -> url.toJson + case Links.Self(url, None) ⇒ "self" -> url.toJson + case Links.Self(url, Some(meta)) => linkValuesToJson("self", url, meta) + + case Links.About(url, None) ⇒ "about" -> url.toJson + case Links.About(url, Some(meta)) => linkValuesToJson("about", url, meta) + + case Links.First(url, None) ⇒ "first" -> url.toJson + case Links.First(url, Some(meta)) => linkValuesToJson("first", url, meta) + + case Links.Last(url, None) ⇒ "last" -> url.toJson + case Links.Last(url, Some(meta)) => linkValuesToJson("last", url, meta) + + case Links.Next(url, None) ⇒ "next" -> url.toJson + case Links.Next(url, Some(meta)) => linkValuesToJson("next", url, meta) + + case Links.Prev(url, None) ⇒ "prev" -> url.toJson + case Links.Prev(url, Some(meta)) => linkValuesToJson("prev", url, meta) + + case Links.Related(url, None) ⇒ "related" -> url.toJson + case Links.Related(url, Some(meta)) => linkValuesToJson("related", url, meta) }) JsObject(fields: _*) } + def linkValuesToJson(name: String, href: String, meta: Meta): (String, JsValue) = { + name -> JsObject( + "href" -> JsString(href), + "meta" -> meta.toJson + ) + } + + def jsonToLinkValues(linkObjectJson: Map[String, JsValue]): (String, Option[Meta]) = { + (linkObjectJson.find(_._1 == "href"), linkObjectJson.find(_._1 == "meta")) match { + case(Some(hrefJson), Some(metaJson)) => + val href = hrefJson match { + case ("href", JsString(hrefStr)) => hrefStr + } + val meta: Map[String, JsonApiObject.Value] = metaJson match { + case ("meta", JsObject(metaObjectJson)) => + metaObjectJson.map { + case (name, value) => + (name, value.convertTo[JsonApiObject.Value]) + } + } + (href, Some(meta)) + } + } + override def read(json: JsValue): Links = { val obj = json.asJsObject - val self = (obj \? FieldNames.`self`) map (url ⇒ Links.Self(url.asString)) - val about = (obj \? FieldNames.`about`) map (url ⇒ Links.About(url.asString)) - val first = (obj \? FieldNames.`first`) map (url ⇒ Links.First(url.asString)) - val last = (obj \? FieldNames.`last`) map (url ⇒ Links.Last(url.asString)) - val next = (obj \? FieldNames.`next`) map (url ⇒ Links.Next(url.asString)) - val prev = (obj \? FieldNames.`prev`) map (url ⇒ Links.Prev(url.asString)) - val related = (obj \? FieldNames.`related`) map (url ⇒ Links.Related(url.asString)) + val self = (obj \? FieldNames.`self`) map { + case(JsString(url)) ⇒ Links.Self(url, None) + case (JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Self(linkValues._1, linkValues._2) + } + + val about = (obj \? FieldNames.`about`) map { + case(JsString(url)) ⇒ Links.About(url, None) + case (JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.About(linkValues._1, linkValues._2) + } + + val first = (obj \? FieldNames.`first`) map { + case(JsString(url)) ⇒ Links.First(url, None) + case (JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.First(linkValues._1, linkValues._2) + } + + val last = (obj \? FieldNames.`last`) map { + case(JsString(url)) ⇒ Links.Last(url, None) + case (JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Last(linkValues._1, linkValues._2) + } + + val next = (obj \? FieldNames.`next`) map { + case(JsString(url)) ⇒ Links.Next(url, None) + case (JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Next(linkValues._1, linkValues._2) + } + + val prev = (obj \? FieldNames.`prev`) map { + case(JsString(url)) ⇒ Links.Prev(url, None) + case (JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Prev(linkValues._1, linkValues._2) + } + + val related = (obj \? FieldNames.`related`) map { + case(JsString(url)) ⇒ Links.Related(url, None) + case (JsObject(linkObjectJson)) => + val linkValues = jsonToLinkValues(linkObjectJson) + Links.Related(linkValues._1, linkValues._2) + } + collectSome(self, about, first, last, next, prev, related) } } diff --git a/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/ExampleSpec.scala b/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/ExampleSpec.scala index 685cb90..075b9fc 100644 --- a/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/ExampleSpec.scala +++ b/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/ExampleSpec.scala @@ -38,7 +38,7 @@ class ExampleSpec extends WordSpec with MustMatchers with SprayJsonJsonapiProtoc id = Some(person.id.toString), attributes = Some(List( Attribute("name", StringValue(person.name)) - )), links = Some(List(Links.Self("http://test.link/person/42")))))) + )), links = Some(List(Links.Self("http://test.link/person/42", None)))))) } } @@ -69,7 +69,7 @@ class ExampleSpec extends WordSpec with MustMatchers with SprayJsonJsonapiProtoc id = Some(person.id.toString), attributes = Some(List( Attribute("name", StringValue(person.name)) - )))), links = Some(List(Links.Next("http://test.link/person/43")))) + )))), links = Some(List(Links.Next("http://test.link/person/43", None)))) } } diff --git a/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormatSpec.scala b/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormatSpec.scala index 885cebd..6c06e19 100644 --- a/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormatSpec.scala +++ b/spray-json/src/test/scala/org/zalando/jsonapi/sprayjson/SprayJsonJsonapiFormatSpec.scala @@ -26,8 +26,14 @@ class SprayJsonJsonapiFormatSpec extends JsonBaseSpec[JsValue] with MustMatchers "transform a list of resource identifier objects correctly" in { rootObjectWithResourceIdentifierObjects.toJson mustEqual rootObjectWithResourceIdentifierObjectsJson } - "transform all link types correctly" in { - rootObjectWithResourceObjectsWithAllLinks.toJson mustEqual rootObjectWithResourceObjectsWithAllLinksJson + "transform all string link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsStrings.toJson mustEqual rootObjectWithResourceObjectsWithAllLinksAsStringsJson + } + "transform all object link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsObjects.toJson mustEqual rootObjectWithResourceObjectsWithAllLinksAsObjectsJson + } + "transform all string and object link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjects.toJson mustEqual rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJson } "transform all meta object inside resource object correctly" in { rootObjectWithResourceObjectsWithMeta.toJson mustEqual rootObjectWithResourceObjectsWithMetaJson @@ -70,8 +76,14 @@ class SprayJsonJsonapiFormatSpec extends JsonBaseSpec[JsValue] with MustMatchers "transform a list of resource identifier objects correctly" in { rootObjectWithResourceIdentifierObjectsJson.convertTo[RootObject] === rootObjectWithResourceIdentifierObjects } - "transform all link types correctly" in { - rootObjectWithResourceObjectsWithAllLinksJson.convertTo[RootObject] === rootObjectWithResourceObjectsWithAllLinks + "transform all string link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsStringsJson.convertTo[RootObject] === rootObjectWithResourceObjectsWithAllLinksAsStrings + } + "transform all object link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsObjectsJson.convertTo[RootObject] === rootObjectWithResourceObjectsWithAllLinksAsObjects + } + "transform all string and object link types correctly" in { + rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjectsJson.convertTo[RootObject] === rootObjectWithResourceObjectsWithAllLinksAsStringsAndObjects } "transform all meta object inside resource object correctly" in { rootObjectWithResourceObjectsWithMetaJson.convertTo[RootObject] === rootObjectWithResourceObjectsWithMeta diff --git a/version.sbt b/version.sbt index d30395f..a3c3a82 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "0.5.2" +version in ThisBuild := "0.6.2"