| author | Fabian Huch <huch@in.tum.de> | 
| Fri, 01 Dec 2023 20:32:34 +0100 | |
| changeset 79101 | 4e47b34fbb8e | 
| parent 79044 | 8cc1ae43e12e | 
| child 79510 | d8330439823a | 
| permissions | -rw-r--r-- | 
| 74946 | 1 | /* Title: Pure/General/json_api.scala | 
| 2 | Author: Makarius | |
| 3 | ||
| 4 | Support for JSON:API: https://jsonapi.org/format | |
| 5 | */ | |
| 6 | ||
| 7 | package isabelle | |
| 8 | ||
| 74962 | 9 | import java.net.URL | 
| 10 | ||
| 74946 | 11 | |
| 75393 | 12 | object JSON_API {
 | 
| 74946 | 13 | val mime_type = "application/vnd.api+json" | 
| 14 | ||
| 15 |   def api_error(msg: String): Nothing = error("JSON API error: " + msg)
 | |
| 16 |   def api_errors(msgs: List[String]): Nothing = error(("JSON API errors:" :: msgs).mkString("\n  "))
 | |
| 17 | ||
| 75393 | 18 |   class Service(val url: URL) {
 | 
| 74962 | 19 | override def toString: String = url.toString | 
| 20 | ||
| 21 | def get(route: String): HTTP.Content = | |
| 79044 
8cc1ae43e12e
clarified signature: avoid deprecated URL constructors;
 wenzelm parents: 
78592diff
changeset | 22 | HTTP.Client.get(Url.resolve(url, route)) | 
| 74962 | 23 | |
| 24 | def get_root(route: String = ""): Root = | |
| 77368 | 25 | Root(get(if_proper(route, "/" + route)).json) | 
| 74962 | 26 | } | 
| 27 | ||
| 75393 | 28 |   sealed case class Root(json: JSON.T) {
 | 
| 75403 | 29 | def get_links: Option[Links] = JSON.value(json, "links").map(Links.apply) | 
| 74962 | 30 | def get_next: Option[Service] = get_links.flatMap(_.get_next) | 
| 31 | ||
| 74946 | 32 | def get_included: List[Resource] = JSON.list(json, "included", Resource.some).getOrElse(Nil) | 
| 33 | def get_data: Option[Data] = JSON.value(json, "data").map(Data(_, get_included)) | |
| 74962 | 34 | |
| 74946 | 35 | def get_errors: Option[List[JSON.T]] = JSON.list(json, "errors", Some(_)) | 
| 36 | ||
| 37 | def ok: Boolean = get_errors.isEmpty | |
| 38 | def check: Root = | |
| 39 |       get_errors match {
 | |
| 40 | case None => this | |
| 41 | case Some(List(err)) => api_error(JSON.Format(err)) | |
| 42 | case Some(errs) => api_errors(errs.map(JSON.Format.apply)) | |
| 43 | } | |
| 44 |     def check_data: Data = check.get_data getOrElse api_error("missing data")
 | |
| 74962 | 45 | def check_resource: Resource = check_data.resource | 
| 46 | def check_resources: List[Resource] = check_data.resources | |
| 74946 | 47 | |
| 75393 | 48 |     def iterator: Iterator[Root] = {
 | 
| 74963 | 49 | val init = Some(this) | 
| 50 |       new Iterator[Root] {
 | |
| 51 | private var state: Option[Root] = init | |
| 52 | def hasNext: Boolean = state.nonEmpty | |
| 53 | def next(): Root = | |
| 54 |           state match {
 | |
| 55 | case None => Iterator.empty.next() | |
| 56 | case Some(res) => | |
| 57 | state = res.get_next.map(_.get_root()) | |
| 58 | res | |
| 59 | } | |
| 60 | } | |
| 61 | } | |
| 62 | def iterator_data: Iterator[Data] = iterator.map(_.check_data) | |
| 63 | def iterator_resource: Iterator[Resource] = iterator.map(_.check_resource) | |
| 64 | def iterator_resources: Iterator[Resource] = iterator.flatMap(_.check_resources) | |
| 65 | ||
| 74946 | 66 |     override def toString: String = "Root(" + (if (ok) "ok" else "errors") + ")"
 | 
| 67 | } | |
| 68 | ||
| 75393 | 69 |   sealed case class Links(json: JSON.T) {
 | 
| 74962 | 70 | def get_next: Option[Service] = | 
| 71 |       for {
 | |
| 78592 | 72 | case JSON.Value.String(next) <- JSON.value(json, "next") | 
| 74962 | 73 | if Url.is_wellformed(next) | 
| 74 | } yield new Service(Url(next)) | |
| 74946 | 75 | |
| 76 | override def toString: String = | |
| 77 | if (get_next.isEmpty) "Links()" else "Links(next)" | |
| 78 | } | |
| 79 | ||
| 75393 | 80 |   sealed case class Data(data: JSON.T, included: List[Resource]) {
 | 
| 74946 | 81 | def is_multi: Boolean = JSON.Value.List.unapply(data, Some(_)).nonEmpty | 
| 82 | ||
| 83 | def resource: Resource = | |
| 84 |       if (is_multi) api_error("singleton resource expected")
 | |
| 85 | else Resource.some(data).get | |
| 86 | ||
| 87 | def resources: List[Resource] = | |
| 88 | JSON.Value.List.unapply(data, Resource.some) | |
| 89 |         .getOrElse(api_error("multiple resources expected"))
 | |
| 90 | ||
| 91 | override def toString: String = | |
| 92 | if (is_multi) "Data(resources)" else "Data(resource)" | |
| 93 | } | |
| 94 | ||
| 75393 | 95 |   object Resource {
 | 
| 74946 | 96 | def some(json: JSON.T): Some[Resource] = Some(Resource(json)) | 
| 97 | } | |
| 98 | ||
| 75393 | 99 |   sealed case class Resource(json: JSON.T) {
 | 
| 74946 | 100 |     val id: JSON.T = JSON.value(json, "id") getOrElse api_error("missing id")
 | 
| 101 |     val type_ : JSON.T = JSON.value(json, "type") getOrElse api_error("missing type")
 | |
| 102 | val fields: JSON.Object.T = | |
| 103 | JSON.value(json, "attributes", JSON.Object.unapply).getOrElse(JSON.Object.empty) ++ | |
| 104 | JSON.value(json, "relationships", JSON.Object.unapply).getOrElse(JSON.Object.empty) | |
| 105 | ||
| 106 | override def toString: String = JSON.Format(json) | |
| 107 | } | |
| 108 | } |