defmodule ExCheck.JSON do @moduledoc false # Minimal JSON encoder for the fully-controlled output of the JSON/agent reporters. # Kept dependency-free on purpose: ex_check targets Elixir ~> 1.17 (the built-in # JSON module only arrived in 1.18) and must not pull a runtime dep like Jason. @spec encode(term) :: iodata def encode(term), do: do_encode(term) defp do_encode(nil), do: "null" defp do_encode(true), do: "true" defp do_encode(false), do: "false" defp do_encode(int) when is_integer(int), do: Integer.to_string(int) defp do_encode(float) when is_float(float), do: Float.to_string(float) defp do_encode(atom) when is_atom(atom), do: encode_string(Atom.to_string(atom)) defp do_encode(str) when is_binary(str), do: encode_string(str) defp do_encode(list) when is_list(list) do inner = list |> Enum.map(&do_encode/1) |> Enum.intersperse(",") ["[", inner, "]"] end defp do_encode(map) when is_map(map) do inner = map |> Enum.map(fn {key, value} -> [encode_key(key), ":", do_encode(value)] end) |> Enum.intersperse(",") ["{", inner, "}"] end defp encode_key(key) when is_atom(key), do: encode_string(Atom.to_string(key)) defp encode_key(key) when is_binary(key), do: encode_string(key) defp encode_string(str) do [?", escape(str, ""), ?"] end defp escape(<<>>, acc), do: acc defp escape(<>, acc), do: escape(rest, acc <> "\\\"") defp escape(<>, acc), do: escape(rest, acc <> "\\\\") defp escape(<>, acc), do: escape(rest, acc <> "\\n") defp escape(<>, acc), do: escape(rest, acc <> "\\r") defp escape(<>, acc), do: escape(rest, acc <> "\\t") defp escape(<>, acc), do: escape(rest, acc <> "\\f") defp escape(<>, acc), do: escape(rest, acc <> "\\b") defp escape(<>, acc) when char < 0x20 do escaped = "\\u" <> (char |> Integer.to_string(16) |> String.pad_leading(4, "0")) escape(rest, acc <> escaped) end defp escape(<>, acc) do escape(rest, acc <> <>) end end