123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- (* json-util.sml
- *
- * COPYRIGHT (c) 2017 The Fellowship of SML/NJ (http://www.smlnj.org)
- * All rights reserved.
- *
- * Utility functions for processing the JSON in-memory representation.
- *)
- structure JSONUtil : sig
- (* exceptions for conversion functions *)
- exception NotBool of JSON.value
- exception NotInt of JSON.value
- exception NotNumber of JSON.value
- exception NotString of JSON.value
- (* exception that is raised when trying to process a non-object value as an object *)
- exception NotObject of JSON.value
- (* exception that is raised when the given field is not found in an object *)
- exception FieldNotFound of JSON.value * string
- (* exception that is raised when trying to process a non-array value as an array *)
- exception NotArray of JSON.value
- (* exception that is raise when access to an array value is out of bounds *)
- exception ArrayBounds of JSON.value * int
- (* map the above exceptions to a message string; we use General.exnMessage for other
- * exceptions.
- *)
- val exnMessage : exn -> string
- (* conversion functions for atomic values. These raise the corresponding
- * "NotXXX" exceptions when their argument has the wrong shape. Also note
- * that asNumber will accept both integers and floats and asInt may raise
- * Overflow if the number is too large.
- *)
- val asBool : JSON.value -> bool
- val asInt : JSON.value -> Int.int
- val asIntInf : JSON.value -> IntInf.int
- val asNumber : JSON.value -> Real.real
- val asString : JSON.value -> string
- (* find a field in an object; this function raises the NotObject exception when
- * the supplied value is not an object.
- *)
- val findField : JSON.value -> string -> JSON.value option
- (* lookup a field in an object; this function raises the NotObject exception when
- * the supplied value is not an object and raises FieldNotFound if the value is
- * an object, but does not have the specified field.
- *)
- val lookupField : JSON.value -> string -> JSON.value
- (* convert a JSON array to an SML vector *)
- val asArray : JSON.value -> JSON.value vector
- (* map a conversion function over a JSON array to produce a list; this function
- * raises the NotArray exception if the second argument is not an array.
- *)
- val arrayMap : (JSON.value -> 'a) -> JSON.value -> 'a list
- (* path specification for indexing into JSON values *)
- datatype edge
- = SUB of int (* index into array component *)
- | SEL of string (* select field of object *)
- type path = edge list
- (* `get (jv, path)` returns the component of `jv` named by `path`. It raises
- * the NotObject, NotArray, and FieldNotFound exceptions if there is an inconsistency
- * between the path and the structure of `jv`.
- *)
- val get : JSON.value * path -> JSON.value
- (* `replace (jv, path, v)` replaces the component of `jv` named by `path`
- * with the value `v`.
- *)
- val replace : JSON.value * path * JSON.value -> JSON.value
- (* `insert (jv, path, lab, v)` inserts `lab : v` into the object named by `path`
- * in `jv`
- *)
- val insert : JSON.value * path * string * JSON.value -> JSON.value
- (* `append (jv, path, vs)` appends the list of values `vs` onto the array named by `path`
- * in `jv`
- *)
- val append : JSON.value * path * JSON.value list -> JSON.value
- end = struct
- structure J = JSON
- exception NotBool of J.value
- exception NotInt of J.value
- exception NotNumber of J.value
- exception NotString of J.value
- exception NotObject of J.value
- exception FieldNotFound of J.value * string
- exception NotArray of J.value
- exception ArrayBounds of J.value * int
- (* conversion functions for atomic values *)
- fun asBool (J.BOOL b) = b
- | asBool v = raise NotBool v
- fun asInt (J.INT n) = Int.fromLarge n
- | asInt v = raise NotInt v
- fun asIntInf (J.INT n) = n
- | asIntInf v = raise NotInt v
- fun asNumber (J.INT n) = Real.fromLargeInt n
- | asNumber (J.FLOAT f) = f
- | asNumber v = raise NotNumber v
- fun asString (J.STRING s) = s
- | asString v = raise NotString v
- fun findField (J.OBJECT fields) = let
- fun find lab = (case List.find (fn (l, v) => (l = lab)) fields
- of NONE => NONE
- | SOME(_, v) => SOME v
- (* end case *))
- in
- find
- end
- | findField v = raise NotObject v
- fun lookupField (v as J.OBJECT fields) = let
- fun find lab = (case List.find (fn (l, v) => (l = lab)) fields
- of NONE => raise FieldNotFound(v, concat["no definition for field \"", lab, "\""])
- | SOME(_, v) => v
- (* end case *))
- in
- find
- end
- | lookupField v = raise NotObject v
- fun asArray (J.ARRAY vs) = Vector.fromList vs
- | asArray v = raise NotArray v
- fun arrayMap f (J.ARRAY vs) = List.map f vs
- | arrayMap f v = raise NotArray v
- (* map the above exceptions to a message string; we use General.exnMessage for other
- * exceptions.
- *)
- fun exnMessage exn = let
- fun v2s (J.ARRAY _) = "array"
- | v2s (J.BOOL false) = "'false'"
- | v2s (J.BOOL true) = "'true'"
- | v2s (J.FLOAT _) = "number"
- | v2s (J.INT _) = "number"
- | v2s J.NULL = "'null'"
- | v2s (J.OBJECT _) = "object"
- | v2s (J.STRING _) = "string"
- in
- case exn
- of NotBool v => String.concat[
- "expected boolean, but found ", v2s v
- ]
- | NotInt(J.FLOAT _) => "expected integer, but found floating-point number"
- | NotInt v => String.concat[
- "expected integer, but found ", v2s v
- ]
- | NotNumber v => String.concat[
- "expected number, but found ", v2s v
- ]
- | NotString v => String.concat[
- "expected string, but found ", v2s v
- ]
- | NotObject v => String.concat[
- "expected object, but found ", v2s v
- ]
- | FieldNotFound(v, fld) => String.concat[
- "no definition for field \"", fld, "\" in object"
- ]
- | NotArray v => String.concat[
- "expected array, but found ", v2s v
- ]
- | _ => General.exnMessage exn
- (* end case *)
- end
- (* path specification for indexing into JSON values *)
- datatype edge
- = SEL of string (* select field of object *)
- | SUB of int (* index into array component *)
- type path = edge list
- fun get (v, []) = v
- | get (v as J.OBJECT fields, SEL lab :: rest) =
- (case List.find (fn (l, v) => (l = lab)) fields
- of NONE => raise raise FieldNotFound(v, concat["no definition for field \"", lab, "\""])
- | SOME(_, v) => get (v, rest)
- (* end case *))
- | get (v, SEL _ :: _) = raise NotObject v
- | get (J.ARRAY vs, SUB i :: rest) = get (List.nth(vs, i), rest)
- | get (v, SUB _ :: _) = raise (NotArray v)
- (* top-down zipper to support functional editing *)
- datatype zipper
- = ZNIL
- | ZOBJ of {
- prefix : (string * J.value) list,
- label : string,
- child : zipper,
- suffix : (string * J.value) list
- }
- | ZARR of {
- prefix : J.value list,
- child : zipper,
- suffix : J.value list
- }
- (* follow a path into a JSON value while constructing a zipper *)
- fun unzip (v, []) = (ZNIL, v)
- | unzip (v as J.OBJECT fields, SEL lab :: rest) = let
- fun find (_, []) = raise FieldNotFound(v, concat["no definition for field \"", lab, "\""])
- | find (pre, (l, v)::flds) = if (l = lab)
- then let
- val (zipper, v) = unzip (v, rest)
- in
- (ZOBJ{prefix=pre, label=lab, suffix=flds, child=zipper}, v)
- end
- else find ((l, v)::pre, flds)
- in
- find ([], fields)
- end
- | unzip (v, SEL _ :: _) = raise NotObject v
- | unzip (v as J.ARRAY vs, SUB i :: rest) = let
- fun sub (_, [], _) = raise ArrayBounds(v, i)
- | sub (prefix, v::vs, 0) = let
- val (zipper, v) = unzip (v, rest)
- in
- (ZARR{prefix = prefix, child = zipper, suffix = vs}, v)
- end
- | sub (prefix, v::vs, i) = sub(v::prefix, vs, i-1)
- in
- sub ([], vs, i)
- end
- | unzip (v, SUB _ :: _) = raise NotArray v
- (* zip up a zipper *)
- fun zip (zipper, v) = let
- fun zip' ZNIL = v
- | zip' (ZOBJ{prefix, label, suffix, child}) =
- J.OBJECT(List.revAppend(prefix, (label, zip' child)::suffix))
- | zip' (ZARR{prefix, child, suffix}) =
- J.ARRAY(List.revAppend(prefix, zip' child :: suffix))
- in
- zip' zipper
- end
- fun replace (jv, path, v) = zip (#1 (unzip (jv, path)), v)
- fun insert (jv, path, label, v) = (case unzip (jv, path)
- of (zipper, J.OBJECT fields) => zip (zipper, J.OBJECT((label, v)::fields))
- | (_, v) => raise NotObject v
- (* end case *))
- fun append (jv, path, vs) = (case unzip (jv, path)
- of (zipper, J.ARRAY jvs) => zip (zipper, J.ARRAY(jvs @ vs))
- | (_, v) => raise NotArray v
- (* end case *))
- end
|