Welcome to the Tsonnet series!
In the previous post, we refactored numerical types:
Today, I'd like to tackle the presentation layer: pretty-printing the JSON output.
The print
function is not actually printing "JSON", just raw value representations.
Yojson is a pretty common choice for parsing JSON in OCaml.
Let's install it with opam:
$ opam install yojson
And add it to our dependencies in dune-project:
diff --git a/dune-project b/dune-project
index 383bbef..775351b 100644
--- a/dune-project
+++ b/dune-project
@@ -33,7 +33,9 @@
(bisect_ppx
(and
:with-test
- (>= 2.8.3))))
+ (>= 2.8.3)))
+ (yojson
+ (>= 2.2.2)))
(tags
(jsonnet interpreter compiler)))
diff --git a/tsonnet.opam b/tsonnet.opam
index 31c792c..16783c3 100644
--- a/tsonnet.opam
+++ b/tsonnet.opam
@@ -15,6 +15,7 @@ depends: [
"menhir" {= "20240715"}
"alcotest" {with-test & >= "1.8.0"}
"bisect_ppx" {with-test & >= "2.8.3"}
+ "yojson" {>= "2.2.2"}
"odoc" {with-doc}
]
build: [
The opam file is automatically updated by dune.
Before using the library, we need to specify that the lib config will depend on yojson:
diff --git a/lib/dune b/lib/dune
index 3452131..05c4dc3 100644
--- a/lib/dune
+++ b/lib/dune
@@ -1,7 +1,8 @@
(library
(name tsonnet)
(instrumentation
- (backend bisect_ppx)))
+ (backend bisect_ppx))
+ (libraries yojson))
(menhir
(modules parser))
To get rid of the printfs, we need a way of converting a expr
to JSON.
Let's encapsulate the expression conversion in a new module, dedicated to transforming expressions into JSON representation:
diff --git a/lib/json.ml b/lib/json.ml
new file mode 100644
index 0000000..886d2bd
--- /dev/null
+++ b/lib/json.ml
@@ -0,0 +1,17 @@
+open Ast
+
+let rec expr_to_yojson : (expr -> Yojson.t) = function
+ | Number n ->
+ (match n with
+ | Int i -> `Int i
+ | Float f -> `Float f)
+ | Null -> `Null
+ | Bool b -> `Bool b
+ | String s -> `String s
+ | Array values -> `List (List.map expr_to_yojson values)
+ | Object attrs ->
+ let eval' = fun (k, v) -> (k, expr_to_yojson v)
+ in `Assoc (List.map eval' attrs)
+ | _ -> failwith "value type not representable as JSON"
+
+let expr_to_string expr = Yojson.pretty_to_string (expr_to_yojson expr)
The new Json
module will implement two functions. The function expr_to_yojson
basically maps our expr
types to Yojson.t
. The function expr_to_string
will use the previous function to convert from Yojson.t
to string
.
If eventually we decide to change the way we want to render JSON, like using a more performant library, for example, we refactor this module and there's no need to update anywhere else -- maybe the tests.
With that, we can remove our messy hand-written print function:
diff --git a/lib/tsonnet.ml b/lib/tsonnet.ml
index 832b56c..a528488 100644
--- a/lib/tsonnet.ml
+++ b/lib/tsonnet.ml
@@ -6,24 +6,6 @@ let parse (s: string) : expr =
let ast = Parser.prog Lexer.read lexbuf in
ast
-let rec print = function
- | Number n ->
- (match n with
- | Int i -> Printf.sprintf "%d" i
- | Float f -> Printf.sprintf "%f" f)
- | Null -> Printf.sprintf "null"
- | Bool b -> Printf.sprintf "%b" b
- | String s -> Printf.sprintf "\"%s\"" s
- | Array values -> Printf.sprintf "[%s]" (String.concat ", " (List.map print values))
- | Object attrs ->
- let print_key_val = function
- | (k, v) -> Printf.sprintf "\"%s\": %s" k (print v)
- in
- Printf.sprintf "{%s}" (
- String.concat ", " (List.map print_key_val attrs)
- )
- | _ -> failwith "not implemented"
-
let interpret_bin_op (op: bin_op) (n1: number) (n2: number) : expr =
match op, n1, n2 with
| Add, (Int a), (Int b) -> Number (Int (a + b))
@@ -52,7 +34,7 @@ let rec interpret (e: expr) : expr =
| (Number v1), (Number v2) -> interpret_bin_op op v1 v2
| _ -> failwith "invalid binary operation"
-let run (s: string) : expr =
+let run (s: string) =
let ast = parse s in
let evaluated_ast = interpret ast in
- evaluated_ast
+ Json.expr_to_string evaluated_ast
The Tsonnet library module looks much cleaner!
As we are making the run
function return a string
, we must update main.ml
accordingly:
diff --git a/bin/main.ml b/bin/main.ml
index 78e9bb8..723b106 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -7,8 +7,8 @@ let run_parser filename =
let input_channel = open_in filename in
let content = really_input_string input_channel (in_channel_length input_channel) in
close_in input_channel;
- let expr = Tsonnet.run content in
- print_endline (Tsonnet.print expr)
+ let result = Tsonnet.run content in
+ print_endline result
let () =
Arg.parse spec_list anonymous_fun usage_msg;
And the cram tests need to reflect the output as well:
diff --git a/bin/usage.t b/bin/usage.t
index a8fc82f..b488efe 100644
--- a/bin/usage.t
+++ b/bin/usage.t
@@ -7,7 +7,14 @@ Using the Tsonnet program:
"Hello, world!"
$ tsonnet ../samples/literals/object.jsonnet
- {"int_attr": 1, "float_attr": 4.200000, "string_attr": "Hello, world!", "null_attr": null, "array_attr": [1, false, {}], "obj_attr": {"a": true, "b": false, "c": {"d": [42]}}}
+ {
+ "int_attr": 1,
+ "float_attr": 4.2,
+ "string_attr": "Hello, world!",
+ "null_attr": null,
+ "array_attr": [ 1, false, {} ],
+ "obj_attr": { "a": true, "b": false, "c": { "d": [ 42 ] } }
+ }
$ tsonnet ../samples/binary_operations.jsonnet
- 44.700000
+ 44.7
One trick that makes our lives super easy is the fact that dune has a feature called promote
.
What does it do? If the tests run and the output is correct, but the diff is conflicting, you have the option to promote it, which means that it will apply the diff for you with a single command:
$ dune runtest --auto-promote
The command above will re-run the tests and apply the diff of the output to the respective files.
This is super handy! I won't go into details here, but I really recommend reading the Diffing and Promotion documentation.
Beware, this can make you grumpy against your current building tool -- approach with caution. It feels like magic, really! 😜
It feels much better having a pretty-printed output, isn't it?!
Thanks for reading Bit Maybe Wise! Subscribe to receive new posts about Tsonnet.
Photo by Pete Godfrey on Unsplash
Top comments (0)