Skip to content

Commit

Permalink
Merge pull request #142 from alejandrohdezma/feature/multi-get
Browse files Browse the repository at this point in the history
Enable indicating the whole path when retrieving nested values in JSON
  • Loading branch information
alejandrohdezma committed Apr 7, 2020
2 parents 3857d26 + d4cd8c3 commit b68e944
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ object Repository {

client
.get[Repository](uri)
.failMap {
.collectFail {
case "description" / NotFound => GithubError(descriptionNotFound)
case "license" / NotFound => GithubError(licenseNotFound)
case "license" / ("spdx_id" / NotFound) => GithubError(licenseNotInferred)
Expand All @@ -160,8 +160,8 @@ object Repository {
startYear <- json.get[ZonedDateTime]("created_at")
contributors <- json.get[String]("contributors_url")
collaborators <- json.get[String]("collaborators_url")
organizationUrl <- json.get[Option[OrganizationUrl]]("organization")
ownerUrl <- json.get[OwnerUrl]("owner")
organizationUrl <- json.get[Option[String]]("organization", "url")
ownerUrl <- json.get[String]("owner", "url")
} yield Repository(
name,
description,
Expand All @@ -170,18 +170,8 @@ object Repository {
startYear.getYear,
contributors,
collaborators.replace("{/collaborator}", ""),
organizationUrl.map(_.value),
ownerUrl.value
organizationUrl,
ownerUrl
)

final private case class OrganizationUrl(value: String) extends AnyVal

implicit private val OrganizationUrlDecoder: Decoder[OrganizationUrl] =
_.get[String]("url").map(OrganizationUrl)

final private case class OwnerUrl(value: String) extends AnyVal

implicit private val OwnerUrlDecoder: Decoder[OwnerUrl] =
_.get[String]("url").map(OwnerUrl)

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ object client {
Source.fromInputStream(inputStream, "UTF-8").mkString
}
)
}.failMap {
}.collectFail {
case _: FileNotFoundException => URLNotFound(uri)
}.flatMap(Json.parse).as[A]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ trait Decoder[A] {
/** Decode the given [[Json.Value]] */
def decode(json: Json.Value): Try[A]

/**
* What should this decoder return if a `Null` is found on the path to getting the value.
*
* This doesn't affect when the value itself is `Null`.
*/
def onNullPath: Try[A] = NotFound.raise

}

object Decoder {
Expand Down Expand Up @@ -72,9 +79,15 @@ object Decoder {
case value => NotADateTime(value).raise
}

implicit def OptionDecoder[A: Decoder]: Decoder[Option[A]] = {
case Json.Null => Try(None)
case value => Decoder[A].decode(value).map(Some(_))
implicit def OptionDecoder[A: Decoder]: Decoder[Option[A]] = new Decoder[Option[A]] {

override def decode(json: Json.Value): Try[Option[A]] = json match {
case Json.Null => Try(None)
case value => Decoder[A].decode(value).map(Some(_))
}

override def onNullPath: Try[Option[A]] = Try(None)

}

implicit def ListDecoder[A: Decoder]: Decoder[List[A]] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

package com.alejandrohdezma.sbt.github.syntax

import scala.annotation.tailrec
import scala.util.Try

import com.alejandrohdezma.sbt.github.error.NotFound
import com.alejandrohdezma.sbt.github.json.error.{InvalidPath, NotAJSONObject}
import com.alejandrohdezma.sbt.github.json.{Decoder, Json}
import com.alejandrohdezma.sbt.github.syntax.scalatry._
Expand All @@ -37,10 +37,19 @@ object json {
*
* Returns `Failure` with the error in case this is not a `Json.Object` or the decoding fails.
*/
def get[A: Decoder](path: String): Try[A] = json match {
case json: Json.Object => json.get(path).as[A].failMap { case t => InvalidPath(path, t) }
case Json.Null => NotFound.raise
case value => NotAJSONObject(value).raise
def get[A: Decoder](head: String, tail: String*): Try[A] =
recursiveGet(json, List(head +: tail: _*), Nil)

@tailrec
private def recursiveGet[A: Decoder](
value: Json.Value,
remain: List[String],
done: List[String]
): Try[A] = (value, remain) match {
case (j: Json.Value, Nil) => j.as[A].mapFail(done.foldRight(_)(InvalidPath))
case (j: Json.Object, h :: t) => recursiveGet(j.get(h), t, done :+ h)
case (Json.Null, _) => Decoder[A].onNullPath.mapFail(done.foldRight(_)(InvalidPath))
case (v, _) => done.foldRight(NotAJSONObject(v): Throwable)(InvalidPath).raise
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,37 @@ object scalatry {
* The contained throwable is used as cause for the provided one.
*
* {{{
* Failure(NotFound).failMap {
* Failure(NotFound).collectFail {
* case NotFound => UrlNotFound
* } // Result: Failure(UrlNotFound(cause = NotFound))
*
* Failure(NotAString).failMap {
* Failure(NotAString).collectFail {
* case NotFound => UrlNotFound
* } // Result: Failure(NotAString)
*
*
* Success(12).failMap {
* Success(12).collectFail {
* case NotFound => UrlNotFound
* } // Result: Success(12)
* }}}
*/
def failMap(pf: PartialFunction[Throwable, Throwable]): Try[A] = aTry match {
def collectFail(pf: PartialFunction[Throwable, Throwable]): Try[A] = aTry match {
case Success(a) => Success(a)
case Failure(b) => Failure(pf.andThen(_.initCause(b)).lift(b).getOrElse(b))
case Failure(b) => Failure(pf.andThen(initCause(b)).lift(b).getOrElse(b))
}

/**
* The given function is applied if this is a `Failure`.
* The contained throwable is used as cause for the provided one.
*
* {{{
* Failure(NotFound).mapFail(_ => UrlNotFound) // Result: Failure(UrlNotFound(cause = NotFound))
* Success(12).mapFail(_ => UrlNotFound) // Result: Success(12)
* }}}
*/
def mapFail(pf: Throwable => Throwable): Try[A] = aTry match {
case Success(a) => Success(a)
case Failure(b) => Failure(pf.andThen(initCause(b))(b))
}

/**
Expand All @@ -57,9 +71,13 @@ object scalatry {
*/
def failAs(t: => Throwable): Try[A] = aTry match {
case Success(a) => Success(a)
case Failure(b) => Failure(t.initCause(b))
case Failure(b) => Failure(initCause(b)(t))
}

}

@SuppressWarnings(Array("all"))
private def initCause[A](cause: Throwable)(t: Throwable): Throwable =
if (cause != t && t.getCause == null) t.initCause(cause) else t

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,41 @@ class JsonSyntaxSpec extends Specification {
json.get[Int]("miau") must beSuccessfulTry(42)
}

"return requested type on a path chain" >> {
val json: Json.Value = Json.Object(
Map("m1" -> Json.Object(Map("m2" -> Json.Object(Map("m3" -> Json.Number(42d))))))
)

json.get[Int]("m1", "m2", "m3") must beSuccessfulTry(42)
}

"keep failed path on decoding failure" >> {
val json: Json.Value = Json.Object(
Map("m1" -> Json.Object(Map("m2" -> Json.Object(Map("m3" -> Json.Text("miau"))))))
)

val expected =
InvalidPath("m1", InvalidPath("m2", InvalidPath("m3", NotANumber(Json.Text("miau")))))

json.get[Int]("m1", "m2", "m3") must beFailedTry(equalTo(expected))
}

"keep failed path on not a json object" >> {
val json: Json.Value = Json.Object(Map("m1" -> Json.Object(Map("m2" -> Json.Text("miau")))))

val expected = InvalidPath("m1", InvalidPath("m2", NotAJSONObject(Json.Text("miau"))))

json.get[Int]("m1", "m2", "m3") must beFailedTry(equalTo(expected))
}

"keep failed path on not found" >> {
val json: Json.Value = Json.Object(Map("m1" -> Json.Object(Map())))

val expected = InvalidPath("m1", InvalidPath("m2", NotFound))

json.get[Int]("m1", "m2", "m3") must beFailedTry(equalTo(expected))
}

"propagate Decoder[A] failure" >> {
val json: Json.Value = Json.Object(Map("miau" -> Json.Number(42d)))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import org.specs2.mutable.Specification

class TrySyntaxSpec extends Specification {

"failMap" should {
"collectFail" should {

"change value if Failure" >> {
val failure = Failure(NotFound)

val result = failure.failMap {
val result = failure.collectFail {
case _ => URLNotFound("url")
}

Expand All @@ -40,7 +40,7 @@ class TrySyntaxSpec extends Specification {
"do nothing if Success" >> {
val success = Try(42)

val result = success.failMap {
val result = success.collectFail {
case _ => NotFound
}

Expand All @@ -49,6 +49,26 @@ class TrySyntaxSpec extends Specification {

}

"mapFail" should {

"change value if Failure" >> {
val failure = Failure(NotFound)

val result = failure.mapFail(_ => URLNotFound("url"))

result must beAFailedTry(equalTo(URLNotFound("url")))
}

"do nothing if Success" >> {
val success = Try(42)

val result = success.mapFail(_ => NotFound)

result must beSuccessfulTry(42)
}

}

"failAs" should {

"change value if Failure" >> {
Expand Down

0 comments on commit b68e944

Please sign in to comment.