support for JSON:API;
authorwenzelm
Wed, 15 Dec 2021 23:18:41 +0100
changeset 74946 0dd14d8b16da
parent 74945 4dc90b43ba94
child 74947 7ada0c20379b
child 74948 15ce207f69c8
support for JSON:API;
etc/build.props
src/Pure/General/json_api.scala
src/Pure/Tools/flarum.scala
--- a/etc/build.props	Wed Dec 15 19:41:30 2021 +0100
+++ b/etc/build.props	Wed Dec 15 23:18:41 2021 +0100
@@ -67,6 +67,7 @@
   src/Pure/General/graphics_file.scala \
   src/Pure/General/http.scala \
   src/Pure/General/json.scala \
+  src/Pure/General/json_api.scala \
   src/Pure/General/linear_set.scala \
   src/Pure/General/logger.scala \
   src/Pure/General/long_name.scala \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Pure/General/json_api.scala	Wed Dec 15 23:18:41 2021 +0100
@@ -0,0 +1,76 @@
+/*  Title:      Pure/General/json_api.scala
+    Author:     Makarius
+
+Support for JSON:API: https://jsonapi.org/format
+*/
+
+package isabelle
+
+
+object JSON_API
+{
+  val mime_type = "application/vnd.api+json"
+
+  def api_error(msg: String): Nothing = error("JSON API error: " + msg)
+  def api_errors(msgs: List[String]): Nothing = error(("JSON API errors:" :: msgs).mkString("\n  "))
+
+  sealed case class Root(json: JSON.T)
+  {
+    def get_links: Option[Links] = JSON.value(json, "links").map(Links)
+    def get_included: List[Resource] = JSON.list(json, "included", Resource.some).getOrElse(Nil)
+    def get_data: Option[Data] = JSON.value(json, "data").map(Data(_, get_included))
+    def get_errors: Option[List[JSON.T]] = JSON.list(json, "errors", Some(_))
+
+    def ok: Boolean = get_errors.isEmpty
+    def check: Root =
+      get_errors match {
+        case None => this
+        case Some(List(err)) => api_error(JSON.Format(err))
+        case Some(errs) => api_errors(errs.map(JSON.Format.apply))
+      }
+    def check_data: Data = check.get_data getOrElse api_error("missing data")
+
+    override def toString: String = "Root(" + (if (ok) "ok" else "errors") + ")"
+  }
+
+  sealed case class Links(json: JSON.T)
+  {
+    def get_next: Option[Root] =
+      for (next <- JSON.value(json, "next") if next != null) yield Root(next)
+
+    override def toString: String =
+      if (get_next.isEmpty) "Links()" else "Links(next)"
+  }
+
+  sealed case class Data(data: JSON.T, included: List[Resource])
+  {
+    def is_multi: Boolean = JSON.Value.List.unapply(data, Some(_)).nonEmpty
+
+    def resource: Resource =
+      if (is_multi) api_error("singleton resource expected")
+      else Resource.some(data).get
+
+    def resources: List[Resource] =
+      JSON.Value.List.unapply(data, Resource.some)
+        .getOrElse(api_error("multiple resources expected"))
+
+    override def toString: String =
+      if (is_multi) "Data(resources)" else "Data(resource)"
+  }
+
+  object Resource
+  {
+    def some(json: JSON.T): Some[Resource] = Some(Resource(json))
+  }
+
+  sealed case class Resource(json: JSON.T)
+  {
+    val id: JSON.T = JSON.value(json, "id") getOrElse api_error("missing id")
+    val type_ : JSON.T = JSON.value(json, "type") getOrElse api_error("missing type")
+    val fields: JSON.Object.T =
+      JSON.value(json, "attributes", JSON.Object.unapply).getOrElse(JSON.Object.empty) ++
+      JSON.value(json, "relationships", JSON.Object.unapply).getOrElse(JSON.Object.empty)
+
+    override def toString: String = JSON.Format(json)
+  }
+}
--- a/src/Pure/Tools/flarum.scala	Wed Dec 15 19:41:30 2021 +0100
+++ b/src/Pure/Tools/flarum.scala	Wed Dec 15 23:18:41 2021 +0100
@@ -22,8 +22,11 @@
     override def toString: String = url.toString
 
     def get(route: String): HTTP.Content =
-      HTTP.Client.get(new URL(url, route))
+      HTTP.Client.get(if (route.isEmpty) url else new URL(url, route))
 
-    val api: JSON.T = get("api").json
+    def get_api(route: String = ""): JSON_API.Root =
+      JSON_API.Root(get("api" + (if (route.isEmpty) "" else "/" + route)).json)
+
+    val api: JSON_API.Root = get_api()
   }
 }