PtcRunner.Lisp.Runtime.Json (PtcRunner v0.14.0)

Copy Markdown View Source

JSON parsing and generation for PTC-Lisp.

Implements (json/parse-string s), (json/parse-lines s), and (json/generate-string v) — Cheshire-shaped builtins.

The two directions signal differently, following spec rule 4 (properties of input data may signal; properties of the program raise):

  • Parse helpers take external text and return nil on failure rather than raising (DIV-23) — bad input is a recoverable condition.
  • generate-string is handed a value the program itself built, so a value with no JSON encoding is a program fault. It returns a string or raises a named type_error locating the offending position (DIV-24). It never returns nil; a silent nil is indistinguishable from an empty encoding once str concatenates it (#1165).

Generated JSON objects use string keys: keyword keys encode as their name and integer keys as their decimal text. Keyword values, sets, tuples, and non-finite floats have no JSON encoding and are refused.

Summary

Functions

Encode an Elixir value as a JSON string.

Parse line-delimited JSON into a list.

Parse a JSON string into an Elixir value.

Functions

generate_string(value)

@spec generate_string(term()) :: String.t()

Encode an Elixir value as a JSON string.

Returns the encoded string, or raises a type_error naming the position that has no JSON encoding. It never returns nil — see the module doc.

A conversion walk runs before Jason.encode/1, because Jason would otherwise coerce non-boolean atoms (e.g. the PTC-Lisp keyword :fs) into JSON strings on its own, eroding the wire-boundary type signal.

Keys become JSON strings: a string key is used as-is, an integer key becomes its decimal text, and a keyword key becomes its name verbatim — no hyphen-to-underscore normalization, so :max-turns encodes as "max-turns" and never silently renames a key. Two keys that would encode to the same JSON key are refused rather than emitted as a duplicate.

Examples

iex> PtcRunner.Lisp.Runtime.Json.generate_string(nil)
"null"

iex> PtcRunner.Lisp.Runtime.Json.generate_string([1, 2, 3])
"[1,2,3]"

iex> PtcRunner.Lisp.Runtime.Json.generate_string("hello")
"\"hello\""

iex> PtcRunner.Lisp.Runtime.Json.generate_string(%{:server => "fs"})
"{\"server\":\"fs\"}"

iex> PtcRunner.Lisp.Runtime.Json.generate_string(%{1 => "a"})
"{\"1\":\"a\"}"

iex> PtcRunner.Lisp.Runtime.Json.generate_string(%{"server" => :fs})
** (RuntimeError) json/generate-string: cannot encode a keyword as a JSON value at ["server"]. JSON has no keyword type — convert it first with (name x) or (str x).

parse_lines(s)

@spec parse_lines(term()) :: [term()] | nil

Parse line-delimited JSON into a list.

Blank or whitespace-only lines are skipped. Each remaining line is parsed with parse_string/1, so malformed lines and valid JSON literal null lines both produce nil.

Examples

iex> PtcRunner.Lisp.Runtime.Json.parse_lines("{\"a\":1}\n[2,3]\n")
[%{"a" => 1}, [2, 3]]

iex> PtcRunner.Lisp.Runtime.Json.parse_lines("null\nnot json\n")
[nil, nil]

iex> PtcRunner.Lisp.Runtime.Json.parse_lines("  \n{\"ok\":true}\n")
[%{"ok" => true}]

iex> PtcRunner.Lisp.Runtime.Json.parse_lines(42)
nil

parse_string(s)

@spec parse_string(term()) :: term() | nil

Parse a JSON string into an Elixir value.

Returns the parsed value on success; nil on any failure (invalid JSON, non-binary input, nil input). Map keys are decoded as strings (no atom keys) to avoid atom memory leaks on untrusted input.

Examples

iex> PtcRunner.Lisp.Runtime.Json.parse_string(~S|{"a": 1, "b": [2, 3]}|)
%{"a" => 1, "b" => [2, 3]}

iex> PtcRunner.Lisp.Runtime.Json.parse_string("[1, 2, 3]")
[1, 2, 3]

iex> PtcRunner.Lisp.Runtime.Json.parse_string("null")
nil

iex> PtcRunner.Lisp.Runtime.Json.parse_string("not json")
nil

iex> PtcRunner.Lisp.Runtime.Json.parse_string(nil)
nil

iex> PtcRunner.Lisp.Runtime.Json.parse_string(42)
nil