SQL frontend over the catalog engine
0

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-sql / lib / jsonval.ml
6.3 kB 183 lines
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(* SQLite's json1 value dialect (json.html). Parsing and validation reuse the 7 {!Json} library (nox-json); this module adds what is sqlite-specific: the 8 minified text serialisation sqlite3 emits (number and string formatting 9 byte-compatible) and [$.a.b[2]] path navigation. The SQL functions in 10 {!Query} build on it. *) 11 12type t = 13 | Null 14 | Bool of bool 15 | Int of int64 16 | Real of string (** the number's text, formatted by {!real} *) 17 | Str of string 18 | Arr of t list 19 | Obj of (string * t) list 20 21exception Error of string 22 23let type_name = function 24 | Null -> "null" 25 | Bool true -> "true" 26 | Bool false -> "false" 27 | Int _ -> "integer" 28 | Real _ -> "real" 29 | Str _ -> "text" 30 | Arr _ -> "array" 31 | Obj _ -> "object" 32 33(* ── Serialisation ───────────────────────────────────────────── *) 34 35(* A real as sqlite3 renders it in JSON: C [%.15g] (shortest within 15 36 significant digits, trailing zeros stripped), then a forced decimal point -- 37 "10000000000.0", "1.0e+15", "0.1" -- so it always reads back as a real. *) 38let real_text f = 39 let s = Fmt.str "%.15g" f in 40 match String.index_opt s 'e' with 41 | Some i -> 42 let mant = String.sub s 0 i 43 and ex = String.sub s i (String.length s - i) in 44 let mant = if String.contains mant '.' then mant else mant ^ ".0" in 45 mant ^ ex 46 | None -> if String.contains s '.' then s else s ^ ".0" 47 48(* Build a real node from a SQL double, formatted as sqlite3 would. *) 49let real f = Real (real_text f) 50 51let escape_into buf s = 52 Buffer.add_char buf '"'; 53 String.iter 54 (fun c -> 55 match c with 56 | '"' -> Buffer.add_string buf "\\\"" 57 | '\\' -> Buffer.add_string buf "\\\\" 58 | '\n' -> Buffer.add_string buf "\\n" 59 | '\r' -> Buffer.add_string buf "\\r" 60 | '\t' -> Buffer.add_string buf "\\t" 61 | '\b' -> Buffer.add_string buf "\\b" 62 | '\012' -> Buffer.add_string buf "\\f" 63 | c when Char.code c < 0x20 -> 64 (* control characters as \u00XX; built directly to avoid a formatter 65 allocation per character *) 66 let hex = "0123456789abcdef" in 67 Buffer.add_string buf "\\u00"; 68 Buffer.add_char buf hex.[(Char.code c lsr 4) land 0xf]; 69 Buffer.add_char buf hex.[Char.code c land 0xf] 70 | c -> Buffer.add_char buf c) 71 s; 72 Buffer.add_char buf '"' 73 74let to_text j = 75 let buf = Buffer.create 64 in 76 let rec go = function 77 | Null -> Buffer.add_string buf "null" 78 | Bool b -> Buffer.add_string buf (if b then "true" else "false") 79 | Int i -> Buffer.add_string buf (Int64.to_string i) 80 | Real s -> Buffer.add_string buf s 81 | Str s -> escape_into buf s 82 | Arr xs -> 83 Buffer.add_char buf '['; 84 List.iteri 85 (fun i x -> 86 if i > 0 then Buffer.add_char buf ','; 87 go x) 88 xs; 89 Buffer.add_char buf ']' 90 | Obj kvs -> 91 Buffer.add_char buf '{'; 92 List.iteri 93 (fun i (k, v) -> 94 if i > 0 then Buffer.add_char buf ','; 95 escape_into buf k; 96 Buffer.add_char buf ':'; 97 go v) 98 kvs; 99 Buffer.add_char buf '}' 100 in 101 go j; 102 Buffer.contents buf 103 104let pp ppf j = Fmt.string ppf (to_text j) 105 106(* ── Parsing (delegated to nox-json) ─────────────────────────── *) 107 108(* Map a parsed nox-json value into the sqlite dialect tree. A number with a 109 fraction/exponent (nox-json [Float]) is re-formatted via {!real}; sqlite3 110 preserves the original lexeme of such a number, which this does not (the one 111 intentional divergence -- integers, strings, and structure are exact). *) 112let rec of_json (j : Json.t) : t = 113 match j with 114 | Json.Null _ -> Null 115 | Json.Bool (b, _) -> Bool b 116 | Json.Number (n, _) -> ( 117 match n with 118 | Json.Number.Int i | Json.Number.Uint i -> Int i 119 | Json.Number.Float f -> real f) 120 | Json.String (s, _) -> Str s 121 | Json.Array (xs, _) -> Arr (List.map of_json xs) 122 | Json.Object (members, _) -> 123 Obj (List.map (fun ((k, _), v) -> (k, of_json v)) members) 124 125let parse s = 126 match Json.Value.of_string s with Ok v -> Some (of_json v) | Error _ -> None 127 128let parse_exn s = 129 match parse s with Some j -> j | None -> raise (Error "malformed JSON") 130 131(* ── Path navigation ($.a.b[2]) ──────────────────────────────── *) 132 133type step = Key of string | Index of int 134 135(* Parse a path expression like [$.a.b[2]] or [$[0]] into steps. *) 136let parse_path p = 137 let n = String.length p in 138 if n = 0 || p.[0] <> '$' then None 139 else begin 140 let i = ref 1 and steps = ref [] and ok = ref true in 141 while !ok && !i < n do 142 match p.[!i] with 143 | '.' -> 144 incr i; 145 let start = !i in 146 while !i < n && match p.[!i] with '.' | '[' -> false | _ -> true do 147 incr i 148 done; 149 steps := Key (String.sub p start (!i - start)) :: !steps 150 | '[' -> 151 incr i; 152 let start = !i in 153 while !i < n && p.[!i] <> ']' do 154 incr i 155 done; 156 (match int_of_string_opt (String.sub p start (!i - start)) with 157 | Some k -> steps := Index k :: !steps 158 | None -> ok := false); 159 if !i < n then incr i (* past ']' *) 160 | _ -> ok := false 161 done; 162 if !ok then Some (List.rev !steps) else None 163 end 164 165let rec navigate j = function 166 | [] -> Some j 167 | Key k :: rest -> ( 168 match j with 169 | Obj kvs -> ( 170 match List.assoc_opt k kvs with 171 | Some v -> navigate v rest 172 | None -> None) 173 | _ -> None) 174 | Index k :: rest -> ( 175 match j with 176 | Arr xs -> ( 177 match List.nth_opt xs k with 178 | Some v -> navigate v rest 179 | None -> None) 180 | _ -> None) 181 182let lookup j path = 183 match parse_path path with None -> None | Some steps -> navigate j steps