Code Monkey home page Code Monkey logo

jsonapi-scala's Introduction

JSON:API v1.1 implementation in scala

CI codecov jsonapi-scala-core Scala version support

Features

  • Automatic generation of jsonapi json writers with relationship handling for case classes

Requirements

  • Tested to work on Scala 2.12.10 or 2.13.3
  • If you are using Scala 2.12 use the macro paradise plugin. Add addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) into your build.sbt somewhere.
    If you are using Scala 2.13 you should instead use the compiler flag -Ymacro-annotations.
    An example for handling cross-compiling with both can be found in project/MacrosCompiler.

Releasing

Releasing is done automatically using sbt-ci-release.

Example

import _root_.spray.json.DefaultJsonProtocol._
import com.qvantel.jsonapi._

implicit val apiRoot: ApiRoot = ApiRoot(None)

@jsonApiResource final case class Employee(id: String, name: String)
@jsonApiResource final case class Company(id: String, name: String, employees: ToMany[Employee])

val acme = Company("1", "acme", ToMany.loaded(Seq(Employee("1", "number one 1"))))

val json = rawOne(acme)
val parsed = readOne[Company](json, Set("employees"))

acme == parsed // true

or see https://github.com/Doikor/jsonapi-scala-example

Known issues

  • loop handing on reader side (relationship path has to be given manually)

JsonApiClient

There is a very generic JsonApiClient interface for implementing a simple client interface for handling the http query writing side of this

The subproject "pekko-client" has an implementation of this using pekko-http

The subproject "http4s-client" has an implementation of this using http4s

Usage

import cats.data.OptionT
import com.qvantel.jsonapi.JsonApiClient

val jac = JsonApiClient.instance // won't work if you don't have an actual implementations stuff in scope. See setup.

val one: IO[Option[BillingAccount]] = jac.one[BillingAccount]("ba1") 
val many: IO[List[BillingAccount]] = jac.many[BillingAccount](Set("ba1", "ba2"))

// can also load includes at the same time
val withIncludes = jac.one[BillingAccount]("ba1", Set("customer-account"))

// includes can also be loaded on their own with a method
val ba: OptionT[IO, BillingAccount]  = OptionT(jac.one[BillingAccount]("ba"))
val ca: OptionT[IO, CustomerAccount] = ba.semiflatMap(_.customerAccount.load)

// filtering support
val filtered = jac.filter[BillingAccount]("some nice filter string here")

Setup

pekko-http client

// needs ActorSystem and Materializer for pekko-http
// the ApiEndPoint is used to as the "root" where to launch queries
import io.lemonlabs.uri.typesafe.dsl._
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import com.qvantel.jsonapi.ApiEndpoint
import com.qvantel.jsonapi.JsonApiClient
import com.qvantel.jsonapi.client.pekko.PekkoClient._

implicit val system: ActorSystem  = ActorSystem()
implicit val materializer: Materializer     = Materializer(system)
implicit val endpoint: ApiEndpoint = ApiEndpoint.Static("http://localhost:8080/api")

val jac = JsonApiClient.instance

akka-http client (to be deprecated in favor of pekko-http)

// needs ActorSystem and Materializer for akka-http
// the ApiEndPoint is used to as the "root" where to launch queries
import io.lemonlabs.uri.typesafe.dsl._
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import com.qvantel.jsonapi.ApiEndpoint
import com.qvantel.jsonapi.JsonApiClient
import com.qvantel.jsonapi.client.akka.AkkaClient._

implicit val system: ActorSystem  = ActorSystem()
implicit val materializer: ActorMaterializer = ActorMaterializer()
implicit val endpoint: ApiEndpoint = ApiEndpoint.Static("http://localhost:8080/api")

val jac = JsonApiClient.instance

http4s client

Setup for http4s client

import io.lemonlabs.uri.typesafe.dsl._
import org.http4s.client.Client
import org.http4s.client.blaze.Http1Client
import cats.effect.IO
import com.qvantel.jsonapi.ApiEndpoint
import com.qvantel.jsonapi.JsonApiClient

import com.qvantel.jsonapi.client.http4s.Http4sClient._
import com.qvantel.jsonapi.client.http4s.JsonApiInstances._


implicit val endpoint: ApiEndpoint = ApiEndpoint.Static("http://localhost:8080/api")

implicit val client: Client[IO] = Http1Client[IO]().unsafeRunSync()

val jac = JsonApiClient.instance

jsonapi-scala's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jsonapi-scala's Issues

Getting error "Cannot find JsonApiReader or JsonApiFormat type class"

Just found that with Scala version 2.12.3, I'm stilling having the issue (#11).
Is there anything wrong with my code? or does it fully support scala 2.12 yet? :)
Thanks.

build.sbt

scalaVersion := "2.12.3"
libraryDependencies ++= Seq(
  "com.qvantel" %% "jsonapi-scala-core" % "5.1.2",
  "io.spray" %%  "spray-json" % "1.3.3"
)
Welcome to Scala 2.12.3 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_131).
Type in expressions for evaluation. Or try :help.

scala> :paste
// Entering paste mode (ctrl-D to finish)

import _root_.spray.json.DefaultJsonProtocol._
import com.qvantel.jsonapi._

implicit val apiRoot: ApiRoot = ApiRoot(None)

@jsonApiResource final case class Employee(id: String, name: String)
@jsonApiResource final case class Company(id: String, name: String, employees: ToMany[Employee])

val acme = Company("1", "acme", ToMany.loaded(Seq(Employee("1", "number one 1"))))

val json = rawOne(acme)
val parsed = readOne[Company](json, Set("employees"))

acme == parsed // true

// Exiting paste mode, now interpreting.

<console>:19: error: could not find implicit value for evidence parameter of type com.qvantel.jsonapi.Identifiable[Employee]
       val acme = Company("1", "acme", ToMany.loaded(Seq(Employee("1", "number one 1"))))
                                                    ^
<console>:22: error: Cannot find JsonApiReader or JsonApiFormat type class for Company
       val parsed = readOne[Company](json, Set("employees"))

How to get all of a resource?

Is there a way to get a collection of all items for a resource? one and many require knowing the ID of an item ahead of time. It seemed like pathMany might be able to do this, but I couldn't get it to work.

What I'm trying to do is something like the following:
val employees = jac.all[Employee]()
Where employees would then be a List of every employee in the API.

Non-informative error message

In JsonApiReaders.scala, around line 242 there is an exception thrown with message "wrong type". Would be awesome if it could provide the same details as the other exceptions of the same type, i.e. "got [type] expected type [expectedType]".

The macros generate some dead code.

The macros generate some dead code (evident by multiple warnings about this when running the tests for example). The macros should be cleaned up.

Add Scala 2.12 build

Currently the only known issues for getting a 2.12 build is out some dependencies (Uri from spray and scala inflector). From spray there isn't (and won't be) an official 2.12 version and thus would probably be worth it to just rewrite the Uri by ourselves as we don't really use it for anything too complicated. Scala-inflector has a fork under qvantel org that supports 2.12 but needs to be published.

Cannot get it to work

Followed the example and got

Error:(10, 20) Cannot find JsonApiWriter or JsonApiFormat type class for ata.this.Company
val json = rawOne(acme)****

Also there is no "rawOne" in spray.JsonApiSupport so I had to import by

import com.qvantel.jsonapi._

JsonOption.withFilter() fails with MatchError

import com.qvantel.jsonapi.JsonOption

for {
  status <- JsonOption("Open")
  if status == "Closed"
} yield status

fails with

scala.MatchError: JsonSome(Open) (of class com.qvantel.jsonapi.JsonSome)
	at com.qvantel.jsonapi.JsonOption$WithFilter.map(sfdgfg.sc:207)
	at #worksheet#.#worksheet#(sfdgfg.sc:4)

The expected result for this for-comprehension is JsonAbsent.

Deserialization throws when optional relationship is set to null at incoming JSON.

Hi,

I found some deserialization issue. Is that expected behavior for null JSON value (data.relationships.foo === null) which should be deserialized into Option[ToOne[T]] ?

Steps to reproduce

Create Option[ToOne[_]] relation like below:

import com.qvantel.jsonapi.{ApiRoot, ToOne, jsonApiResource, rawOne}
import spray.http.Uri.Path
import spray.json.DefaultJsonProtocol

object Main extends DefaultJsonProtocol {

  def main(args: Array[String]): Unit = {
    implicit val apiRoot: com.qvantel.jsonapi.ApiRoot = ApiRoot(Some(Path("/api")))

    @jsonApiResource final case class Foo(id: String)
    @jsonApiResource final case class Bar(id: String)
    @jsonApiResource final case class Baz(id: String, foo: Option[ToOne[Foo]], bar: Option[ToOne[Bar]])

    val result = rawOne(Baz("baz", None, Some(ToOne.loaded(Bar("bar")))))

    println { result.prettyPrint }

    val incomingJson =
      """
        |{
        |  "data": {
        |    "attributes": {
        |
        |    },
        |    "relationships": {
        |      "bar": {
        |        "data": {
        |          "type": "bars",
        |          "id": "bar"
        |        },
        |        "links": {
        |          "related": "/api/bazs/baz/bar"
        |        }
        |      },
        |      "foo": null
        |    },
        |    "links": {
        |      "self": "/api/bazs/baz"
        |    },
        |    "id": "baz",
        |    "type": "bazs"
        |  },
        |  "included": [{
        |    "type": "bars",
        |    "attributes": {
        |
        |    },
        |    "id": "bar",
        |    "links": {
        |      "self": "/api/bars/bar"
        |    }
        |  }]
        |}
      """.stripMargin

    import spray.json._

    val deserializedBaz = com.qvantel.jsonapi.readOne[Baz](incomingJson.parseJson.asJsObject)

    println { deserializedBaz }
  }
}

Current behavior:

main method from above throws:

spray.json.DeserializationException: JSON object expected
	at spray.json.package$.deserializationError(package.scala:23)
	at spray.json.JsValue.asJsObject(JsValue.scala:36)
	at spray.json.JsValue.asJsObject(JsValue.scala:41)
	at com.qvantel.jsonapi.spray.Main$Baz$4$$anon$3.com$qvantel$jsonapi$spray$Main$Baz$4$$anon$3$$$anonfun$19(Main.scala:14)
	at scala.Option.flatMap(Option.scala:171)
	at com.qvantel.jsonapi.spray.Main$Baz$4$$anon$3.com$qvantel$jsonapi$spray$Main$Baz$4$$anon$3$$$anonfun$18(Main.scala:14)
	at scala.Option.flatMap(Option.scala:171)
	at com.qvantel.jsonapi.spray.Main$Baz$4$$anon$3.read(Main.scala:14)
	at com.qvantel.jsonapi.spray.Main$Baz$4$$anon$3.read(Main.scala:14)
	at com.qvantel.jsonapi.package$.readOne(package.scala:144)
	at com.qvantel.jsonapi.spray.Main$.main(Main.scala:60)
	at com.qvantel.jsonapi.spray.Main.main(Main.scala)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)

Expected behavior.

Any JSON key under "relationships" section with null value should be properly deserialized into Option[ToOne[_]].

Additional info.

Reproduced at

commit 214c8af79388ed525c53ff05b4f871e7e5c1b8ba
Author: Aki Huttunen <[email protected]>
Date:   Thu Jul 6 14:24:40 2017 +0300

    Add a way to differentiate no ids and empty list of ids on ToMany
    and PolyToMany. Fixes #8

In fact this relationships section deserializes properly:

"relationships": {
  "foo": {
    "data": null
  }
}

but { "foo": null } is a valid JSON entry and should be properly deserialized when destination type was wrapped into Option.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.