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