123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150open!Core(** [parse_json_value] attempts to parse a json value from the sexps on the right hand
side of the flatten command. (i.e. the sexps in a Parts.t) We're guaranteed that these
sexps are not nested. *)moduleEl=structtypet=Sexplib.Path.el=|Posofint|Matchofstring*int|Recofstring[@@derivingcompare]end(* When converting a sexp to JSON, we want to convert things that look like numbers to
_valid_ JSON, and, critically, we also don't want to lose any information. (Idempotent
sexp -> json -> sexp round-tripping seems like a desirable property.) It turns out this
is rather tricky.
OCaml and JSON have different grammars for numbers. The set of valid OCaml number
literals is a strict superset of valid JSON number literals. Examples (not guaranteed
to be exhaustive) of features supported in OCaml, but not in JSON:
- Hex, octal, and binary literals, e.g., 0xc0de, 0o1357, or 0b01001010
- Leading '+', e.g., +10
- Multiple leading '0's, e.g., 007
- Underscores for readability, e.g., 8_675_309
In order to avoid losing information, even if something looks like a number in OCaml,
we shouldn't convert it as a similar looking string that would also parse to the same
number (e.g., 007 -> 7). This is bad because not everything that looks like a number is
a number. Some examples include:
- Any sort of number/account id that has a standard format, e.g. 12 digit bank account
numbers may start with a 0.
- Command line arguments may start with a '+' or '-' for specifying relative ranges
rather than absolute values
Therefore, in order to make sure we produce valid JSON, we will only convert atoms that
are already valid JSON numbers according to its spec [1], which we'll do by testing the
string against a straightforward regex, and we will serialize those strings without any
modifications...
... with one exception: since we very frequently use underscores for readability in
larger numbers, we will convert strings-that-look-like-numbers-with-underscores to JSON
numbers by stripping out the underscores.
OCaml is very generous in that it accepts any number of underscores anywhere in a
number, e.g., "_-__1_.__2_e__+_3_4" is valid floating point literal. Conceivably, we
could be less accepting than OCaml, and only support single occurrences of an
underscore _between_ numbers, so things like "1_000" and "1234_5678" would still be
converted to numbers, but not things like "_1", "2_", and "3__4". (Only allowing
underscores every only every 3 numbers, something checked by ppx_js_style, is possible
too, but it would make the regex more complicated, and isn't even correct; other
cultures/situations don't always put separators as thousands/milli separators.) There's
also a standard of using a double underscore to signify the "decimal point" in an
integer that really represents a fixed point number. Ultimately there's too much gray
area here, so we'll simply accept underscores anywhere like OCaml does.
[1] : https://www.json.org/json-en.html
*)moduleAssemble_to_json=structtypeintermediate_result=|ArrayofJsonaf.tlist|Objectof(string*Jsonaf.t)listletto_json=function|Arraya->`Arraya|Objecta->`Objecta;;letcombineaccitem=matchacc,itemwith|Arraya,Arrayb->Array(a@b)|Objecta,Objectb->Object(a@b)|_->failwith"bug: shouldn't be dealing with both lists and records at once";;end(* regex based on diagram on https://www.json.org/json-en.html, but also with support for
underscores *)letjson_number_with_underscores_re=Re2.create_exn~options:{Re2.Options.defaultwithnever_capture=true}("^"(* start of string *)^"(_*-)?"(* negative numbers start with '-' *)^"(_*0_*"(* integer part is either a 0 *)^"|_*[1-9](\\d|_)*)"(* or 1-9 followed by any digits *)^"(\\._*\\d(\\d|_)*)?"(* decimal part, '.' then at least one digit, is optional *)^"(_*[eE]_*[-+]?_*\\d(\\d|_)*)?"(* exponent part, also optional *)^"$"(* end of string *));;letparse_json_value:Sexp.t->Jsonaf.t=function|Sexp.Atoms->(matchBool.of_stringswith|true->`True|false->`False|exception_->ifRe2.matchesjson_number_with_underscores_resthen`Number(String.filters~f:(func->not(Char.equalc'_')))else`Strings)|Sexp.List[]->`Null|Sexp.List_->failwith"bug - every value after flatten should be an atom or nil";;letone_deeperl=List.mapl~f:(fun(p,v)->matchpwith|Sexplib.Path.Pos_::p->p,v|Rec_::p->p,v|_->assertfalse);;letgroup_by_pathl=List.groupl~break:(fun(p1,_)(p2,_)->not([%compare.equal:El.toption](List.hdp1)(List.hdp2)));;letrecassemble_to_json(l:(Sexplib.Path.t*Sexp.t)list)=matchgroup_by_pathlwith|[[(p,v)]]->assemble_to_json1pv|[]->assertfalse|groups->letparts=List.mapgroups~f:(funl->matchList.hd_exnlwith|Pos_::_,_->Assemble_to_json.Array[assemble_to_json(one_deeperl)]|Recn::_,_->Assemble_to_json.Object[n,assemble_to_json(one_deeperl)]|_->assertfalse)inList.reduce_balanced_exnparts~f:Assemble_to_json.combine|>Assemble_to_json.to_jsonandassemble_to_json1pv:Jsonaf.t=matchpwith|[]->parse_json_valuev|Pos_::p->`Array[assemble_to_json1pv]|Recn::p->`Object[n,assemble_to_json1pv]|Match_::_->assertfalse;;letjson_of_sexpsexp=letparts=Parts.flattensexpinassemble_to_jsonparts;;