| author | wenzelm | 
| Wed, 02 Apr 2025 13:39:12 +0200 | |
| changeset 82413 | a6046b6d23b4 | 
| 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: 
79044 
diff
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: 
79044 
diff
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  | 
}  |