(********************************************************************)
(*                                                                  *)
(*  json.s7i      Reading and processing JSON data with a JSON DOM. *)
(*  Copyright (C) 2024  Thomas Mertes                               *)
(*                                                                  *)
(*  This file is part of the Seed7 Runtime Library.                 *)
(*                                                                  *)
(*  The Seed7 Runtime Library is free software; you can             *)
(*  redistribute it and/or modify it under the terms of the GNU     *)
(*  Lesser General Public License as published by the Free Software *)
(*  Foundation; either version 2.1 of the License, or (at your      *)
(*  option) any later version.                                      *)
(*                                                                  *)
(*  The Seed7 Runtime Library is distributed in the hope that it    *)
(*  will be useful, but WITHOUT ANY WARRANTY; without even the      *)
(*  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *)
(*  PURPOSE.  See the GNU Lesser General Public License for more    *)
(*  details.                                                        *)
(*                                                                  *)
(*  You should have received a copy of the GNU Lesser General       *)
(*  Public License along with this program; if not, write to the    *)
(*  Free Software Foundation, Inc., 51 Franklin Street,             *)
(*  Fifth Floor, Boston, MA  02110-1301, USA.                       *)
(*                                                                  *)
(********************************************************************)


include "scanjson.s7i";
include "strifile.s7i";
include "bigint.s7i";
include "float.s7i";


(**
 *  Enumeration type describing the category of a [[#jsonValue|jsonValue]].
 *  Categories are JSON_NULL, JSON_BOOLEAN, JSON_NUMBER, JSON_STRING,
 *  JSON_ARRAY and JSON_OBJECT.
 *)
const type: jsonCategory is new enum
    JSON_NULL,
    JSON_BOOLEAN,
    JSON_NUMBER,
    JSON_STRING,
    JSON_ARRAY,
    JSON_OBJECT
  end enum;

const func string: str (in jsonCategory: aCategory) is
  return literal(aCategory);

enable_output(jsonCategory);


(**
 *  Interface type to represent JSON values.
 *  JSON values can be null (''jsonNull''),  booleans (''jsonBoolean''),
 *  numbers (''jsonNumber''), strings (''jsonString''), arrays (''jsonArray'') or
 *  objects (''jsonObject'').
 *   var jsonValue: json is jsonValue.value;
 *   ...
 *   json := readJson("{\"size\": 2, \"keys\": [{\"id\": 1}, {\"id\": 2}]");
 *   if "keys" in json and category(json["keys"]) = JSON_ARRAY then
 *     for aKey range json["keys"] do
 *       write(integer(aKey["id"]) <& " ");
 *     end for;
 *     writeln;
 *   end if;
 *)
const type: jsonValue is sub object interface;


const func jsonValue: readJson (inout file: inFile, inout string: symbol) is forward;


(**
 *  Get the category of the JSON value ''aValue''.
 *  Returns one of JSON_NULL, JSON_BOOLEAN, JSON_NUMBER, JSON_STRING,
 *  JSON_ARRAY or JSON_OBJECT.
 *   category(jsonValue(NULL))                  returns  JSON_NULL
 *   category(jsonValue(TRUE))                  returns  JSON_BOOLEAN
 *   category(jsonValue(              12345 ))  returns  JSON_NUMBER
 *   category(jsonValue(9223372036854775808_))  returns  JSON_NUMBER
 *   category(jsonValue("foo"))                 returns  JSON_STRING
 *   category(readJson("false"))                returns  JSON_BOOLEAN
 *   category(readJson("9223372036854775808"))  returns  JSON_NUMBER
 *   category(readJson("0.00000762939453125"))  returns  JSON_NUMBER
 *  @return the category of the JSON value ''aValue''.
 *)
const func jsonCategory: category (in jsonValue: aValue)        is DYNAMIC;


(**
 *  Get the ''type'' of the JSON value ''aValue''.
 *  Returns one of ''void'', [[boolean]], [[integer]], [[bigint|bigInteger]], [[float]],
 *  [[string]], ''jsonValueArray'' or ''jsonValueMap''.
 *   type(jsonValue(NULL))                  returns  void
 *   type(jsonValue(TRUE))                  returns  boolean
 *   type(jsonValue(9223372036854775808_))  returns  bigInteger
 *   type(jsonValue(0.00000762939453125 ))  returns  float
 *   type(jsonValue("foo"))                 returns  string
 *   type(readJson("false"))                returns  boolean
 *   type(readJson("9223372036854775808"))  returns  bigInteger
 *   type(readJson("0.00000762939453125"))  returns  float
 *  @return the ''type'' of the JSON value ''aValue''.
 *)
const func type: type (in jsonValue: aValue)                         is DYNAMIC;


(**
 *  Get the ''void'' value from the JSON null ''aValue''.
 *  This function is defined for completeness.
 *  If applied on a JSON null it will always return ''empty''.
 *   void(jsonValue(NULL))   returns  empty
 *   void(readJson("null"))  returns  empty
 *  The function [[#type(in_jsonValue)|type]] can be used to check for JSON null:
 *   if type(aJasonValue) = void then
 *  The function [[#category(in_jsonValue)|category]] can be used for the same purpose:
 *   if category(aJasonValue) = JSON_NULL then
 *  @return the void value of a JSON null.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON null.
 *)
const func void: void (in jsonValue: aValue)                         is DYNAMIC;


(**
 *  Get the [[boolean]] value from the JSON boolean ''aValue''.
 *   boolean(jsonValue(FALSE))  returns  FALSE
 *   boolean(readJson("true"))  returns  TRUE
 *  The function [[#type(in_jsonValue)|type]] can be used to check for a [[boolean]]:
 *   if type(aJasonValue) = boolean then
 *     aBoolean := boolean(aJasonValue);
 *  The function [[#category(in_jsonValue)|category]] can be used for the same purpose:
 *   if category(aJasonValue) = JSON_BOOLEAN then
 *     aBoolean := boolean(aJasonValue);
 *  @return the boolean value of a JSON boolean.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON boolean.
 *)
const func boolean: boolean (in jsonValue: aValue)                   is DYNAMIC;


(**
 *  Get the [[integer]] value from the JSON number ''aValue''.
 *   integer(jsonValue(12345))                  returns  12345
 *   integer(readJson("12345"))                 returns  12345
 *   integer(readJson("12345678909876543210"))  raises   RANGE_ERROR
 *  The function [[#type(in_jsonValue)|type]] can be used to check for an [[integer]]:
 *   if type(aJasonValue) = integer then
 *     anInteger := integer(aJasonValue);
 *  @return the integer value of a JSON number.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON number.
 *  @exception RANGE_ERROR The JSON number cannot be represented as integer.
 *)
const func integer: integer (in jsonValue: aValue)                   is DYNAMIC;


(**
 *  Get the [[bigint|bigInteger]] value from the JSON number ''aValue''.
 *   bigInteger(jsonValue(12345_))   returns  12345_
 *   bigInteger(readJson("12345"))   returns  12345_
 *   bigInteger(readJson("123.45"))  raises   RANGE_ERROR
 *  The function [[#type(in_jsonValue)|type]] can be used to check for a [[bigint|bigInteger]]:
 *   if type(aJasonValue) = bigInteger then
 *     aBigInteger := bigInteger(aJasonValue);
 *  @return the bigInteger value of a JSON number.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON number.
 *  @exception RANGE_ERROR The JSON number cannot be represented as bigInteger.
 *)
const func bigInteger: bigInteger (in jsonValue: aValue)             is DYNAMIC;


(**
 *  Get the [[float]] value from the JSON number ''aValue''.
 *   float(jsonValue(0.0625))   returns  0.0625
 *   float(readJson("0.0625"))  returns  0.0625
 *  The function [[#type(in_jsonValue)|type]] can be used to check for a [[float]]:
 *   if type(aJasonValue) = float then
 *     aFloat := float(aJasonValue);
 *  @return the float value of a JSON number.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON number.
 *)
const func float: float (in jsonValue: aValue)                       is DYNAMIC;


(**
 *  Get the [[string]] value from the JSON string ''aValue''.
 *   string(jsonValue(""))               returns  ""
 *   string(jsonValue("abcdefg"))        returns  "abcdefg"
 *   string(jsonValue(" \t\r\n"))        returns  " \t\r\n"
 *   string(readJson("\"\""))            returns  ""
 *   string(readJson("\"abcdefg\""))     returns  "abcdefg"
 *   string(readJson("\" \\t\\r\\n\""))  returns  " \t\r\n"
 *  The function [[#type(in_jsonValue)|type]] can be used to check for a [[string]]:
 *   if type(aJasonValue) = string then
 *     aString := string(aJasonValue);
 *  The function [[#category(in_jsonValue)|category]] can be used for the same purpose:
 *   if category(aJasonValue) = JSON_STRING then
 *     aString := string(aJasonValue);
 *  @return the string value of a JSON string.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON string.
 *)
const func string: string (in jsonValue: aValue)                     is DYNAMIC;


(**
 *  Access one element from the JSON array ''aValue''.
 *   type(   readJson("[false, 0.125]")[1]) returns boolean
 *   boolean(readJson("[false, 0.125]")[1]) returns FALSE
 *   type(   readJson("[false, 0.125]")[2]) returns float
 *   float(  readJson("[false, 0.125]")[2]) returns 0.125
 *  @return the element with the specified ''index'' from ''aValue''.
 *  @exception INDEX_ERROR If ''index'' is less than 1 or
 *                         greater than [[#maxIdx(in_jsonValue)|maxIdx]](aValue).
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON array.
 *)
const func jsonValue: (in jsonValue: aValue) [ (in integer: index) ] is DYNAMIC;


(**
 *  Length of JSON array ''aValue''.
 *   length(readJson("[]"))           returns 0
 *   length(readJson("[1]"))          returns 1
 *   length(readJson("[1, \"foo\"]")) returns 2
 *  @return the length of the JSON array.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON array.
 *)
const func integer: length (in jsonValue: aValue)                    is DYNAMIC;


(**
 *  Minimum index of JSON array ''aValue''.
 *   minIdx(readJson("[]"))           returns 1
 *   minIdx(readJson("[1]"))          returns 1
 *   minIdx(readJson("[1, \"foo\"]")) returns 1
 *  @return the minimum index of the JSON array.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON array.
 *)
const func integer: minIdx (in jsonValue: aValue)                    is DYNAMIC;


(**
 *  Maximum index of JSON array ''aValue''.
 *   maxIdx(readJson("[]"))           returns 0
 *   maxIdx(readJson("[1]"))          returns 1
 *   maxIdx(readJson("[1, \"foo\"]")) returns 2
 *  @return the maximum index of the JSON array.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON array.
 *)
const func integer: maxIdx (in jsonValue: aValue)                    is DYNAMIC;


(**
 *  Access one element from the JSON object ''aValue''.
 *   type(   readJson("{\"fee\": 0.5, \"foo\": true}")["fee"])  returns  float
 *   float(  readJson("{\"fee\": 0.5, \"foo\": true}")["fee"])  returns  0.5
 *   type(   readJson("{\"fee\": 0.5, \"foo\": true}")["foo"])  returns  boolean
 *   boolean(readJson("{\"fee\": 0.5, \"foo\": true}")["foo"])  returns  TRUE
 *  @return the element with the specified ''name'' from ''aValue''.
 *  @exception INDEX_ERROR If ''aValue'' does not have an element
 *             with the key ''name''.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON object.
 *)
const func jsonValue: (in jsonValue: aValue) [ (in string: name) ]   is DYNAMIC;


(**
 *  Determine if ''aKey'' is an element name of the JSON object ''aValue''.
 *   "fee" in readJson("{\"fee\": 0.5, \"foo\": true}")  returns  TRUE
 *   "fi"  in readJson("{\"fee\": 0.5, \"foo\": true}")  returns  FALSE
 *  @return TRUE if ''aKey'' is an element name of the JSON object ''aValue'',
 *          FALSE otherwise.
 *)
const func boolean: (in string: aKey) in (in jsonValue: aValue)      is DYNAMIC;


(**
 *  Determine if ''aKey'' is not an element name of the JSON object ''aValue''.
 *   "fee" not in readJson("{\"fee\": 0.5, \"foo\": true}")  returns  FALSE
 *   "fi"  not in readJson("{\"fee\": 0.5, \"foo\": true}")  returns  TRUE
 *  @return FALSE if ''aKey'' is an element name of the JSON object ''aValue'',
 *          TRUE otherwise.
 *)
const func boolean: (in string: aKey) not in (in jsonValue: aValue)  is DYNAMIC;


(**
 *  Obtain the element names of the JSON object ''aValue''.
 *   keys(readJson("{}"))                             returns  0 times ""
 *   keys(readJson("{\"fee\": 0.5}"))                 returns  [] ("fee")
 *   keys(readJson("{\"fee\": 0.5, \"foo\": true}"))  returns  [] ("fee", "foo")
 *  @return an array with the element names of the JSON object.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON object.
 *)
const func array string: keys (in jsonValue: aValue)                 is DYNAMIC;


(**
 *  Obtain the elements of the JSON array ''aValue''.
 *   values(readJson("[]"))           returns  0 times jsonValue.value
 *   values(readJson("[\"fee\"]"))    returns  [] (jsonValue("fee"))
 *   values(readJson("[0.5, true]"))  returns  [] (jsonValue(0.5), jsonValue(TRUE))
 *  @return an array with the elements of the JSON array.
 *  @exception ILLEGAL_ACTION The value ''aValue'' is not a JSON array.
 *)
const func array jsonValue: values (in jsonValue: aValue)             is DYNAMIC;


(**
 *  Convert the given JSON value ''aValue'' to a [[string]].
 *   str(readJson("[1, \"a\", {\"ok\" : true}]"))  returns  "[1,\"a\",{\"ok\":true}]"
 *  @return the JSON value as [[string]].
 *)
const func string: str (in jsonValue: aValue)                        is DYNAMIC;


(**
 *  For-loop where ''forVar'' loops over the elements of a JSON array.
 *   var jsonValue: json is jsonValue.value;
 *   var jsonValue: anElement is jsonValue.value;
 *   ...
 *   if "keys" in json and category(json["keys"]) = JSON_ARRAY then
 *     for anElement range json["keys"] do
 *       ...
 *)
const proc: for (inout jsonValue: forVar) range (in jsonValue: aValue) do
              (in proc: statements)
            end for is func
  begin
    for forVar range values(aValue) do
      statements;
    end for;
  end func;


const type: jsonBase is new struct
  end struct;


type_implements_interface(jsonBase, jsonValue);


#
# jsonNull
#


##
#  jsonValue implementation type representing null.
#
const type: jsonNull is sub jsonBase struct
  end struct;


type_implements_interface(jsonNull, jsonValue);


const jsonValue: (attr jsonValue) . value is jsonNull.value;


const type: jsonValueMap is hash [string] jsonValue;
const type: jsonValueArray is array jsonValue;


(**
 *  Create a null [[#jsonValue|jsonValue]].
 *   jsonValue(NULL)
 *  @return a ''jsonNull'' as [[#jsonValue|jsonValue]].
 *)
const func jsonValue: jsonValue (NULL) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var jsonNull: aNull is jsonNull.value;
  begin
    aValue := toInterface(aNull);
  end func;


const func jsonValue: readJsonNull (inout file: inFile, inout string: symbol) is func
  result
    var jsonValue: aValue is jsonValue.value;
  begin
    aValue := jsonValue(NULL);
    symbol := getJsonSymbol(inFile);
  end func;


const func jsonCategory: category (in jsonNull: aNull) is return JSON_NULL;
const func type: type (in jsonNull: aNull)             is return void;
const func void: void (in jsonNull: aNull)             is return empty;
const func string: str (in jsonNull: aNull)            is return "null";


#
# jsonBoolean
#


##
#  jsonValue implementation type representing a boolean.
#
const type: jsonBoolean is sub jsonBase struct
    var boolean: okay is FALSE;
  end struct;


type_implements_interface(jsonBoolean, jsonValue);


(**
 *  Create a [[#jsonValue|jsonValue]] with the given [[boolean]] ''okay''.
 *   jsonValue(TRUE)
 *  @return a ''jsonBoolean'' with the given ''okay'' as [[#jsonValue|jsonValue]].
 *)
const func jsonValue: jsonValue (in boolean: okay) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var jsonBoolean: aBoolean is jsonBoolean.value;
  begin
    aBoolean.okay := okay;
    aValue := toInterface(aBoolean);
  end func;


const func jsonValue: readJsonBoolean (inout file: inFile, inout string: symbol) is func
  result
    var jsonValue: aValue is jsonValue.value;
  begin
    if symbol = "false" then
      aValue := jsonValue(FALSE);
    elsif symbol = "true" then
      aValue := jsonValue(TRUE);
    else
      raise RANGE_ERROR;
    end if;
    symbol := getJsonSymbol(inFile);
  end func;


const func jsonCategory: category (in jsonBoolean: aBoolean) is return JSON_BOOLEAN;
const func type: type (in jsonBoolean: aBoolean)             is return boolean;
const func boolean: boolean (in jsonBoolean: aBoolean)       is return aBoolean.okay;
const func string: str (in jsonBoolean: aBoolean)            is return lower(str(aBoolean.okay));


#
# jsonNumber
#


##
#  jsonValue implementation type representing a number.
#
const type: jsonNumber is sub jsonBase struct
    var string: number is "";
  end struct;


type_implements_interface(jsonNumber, jsonValue);


(**
 *  Create a ''jsonNumber'' with the given ''number''.
 *)
const func jsonValue: jsonNumber (in string: number) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var jsonNumber: aNumber is jsonNumber.value;
  begin
    aNumber.number := number;
    aValue := toInterface(aNumber);
  end func;


(**
 *  Create a [[#jsonValue|jsonValue]] with the given [[integer]] ''number''.
 *   jsonValue(123)
 *  @return a ''jsonNumber'' with the given [[integer]] ''number'' as [[#jsonValue|jsonValue]].
 *)
const func jsonValue: jsonValue (in integer: number) is return jsonNumber(str(number));


(**
 *  Create a [[#jsonValue|jsonValue]] with the given [[bigint|bigInteger]] ''number''.
 *   jsonValue(123_)
 *  @return a ''jsonNumber'' with the given [[bigint|bigInteger]] ''number'' as [[#jsonValue|jsonValue]].
 *)
const func jsonValue: jsonValue (in bigInteger: number) is return jsonNumber(str(number));


(**
 *  Create a [[#jsonValue|jsonValue]] with the given [[float]] ''number''.
 *   jsonValue(0.125)
 *  @return a ''jsonNumber'' with the given [[float]] ''number'' as [[#jsonValue|jsonValue]].
 *)
const func jsonValue: jsonValue (in float: number) is return jsonNumber(str(number));


const func jsonValue: readJsonNumber (inout file: inFile, inout string: symbol) is func
  result
    var jsonValue: aValue is jsonValue.value;
  begin
    aValue := jsonNumber(symbol);
    symbol := getJsonSymbol(inFile);
  end func;


const func jsonCategory: category (in jsonNumber: aNumber) is return JSON_NUMBER;


const func type: type (in jsonNumber: aNumber) is func
  result
    var type: aType is void;
  begin
    if succeeds(ignore(integer(aNumber.number))) then
      aType := integer;
    elsif succeeds(ignore(bigInteger(aNumber.number))) then
      aType := bigInteger;
    elsif succeeds(ignore(float(aNumber.number))) then
      aType := float;
    else
      raise RANGE_ERROR;
    end if;
  end func;


const func integer: integer (in jsonNumber: aNumber)       is return integer(aNumber.number);
const func bigInteger: bigInteger (in jsonNumber: aNumber) is return bigInteger(aNumber.number);
const func float: float (in jsonNumber: aNumber)           is return float(aNumber.number);
const func string: str (in jsonNumber: aNumber)            is return str(aNumber.number);


#
# jsonString
#


##
#  jsonValue implementation type representing a string.
#
const type: jsonString is sub jsonBase struct
    var string: stri is "";
  end struct;


type_implements_interface(jsonString, jsonValue);


(**
 *  Create a [[#jsonValue|jsonValue]] with the given [[string]] ''stri''.
 *   jsonValue("test")
 *  @return a ''jsonString'' with the given [[string]] ''stri'' as [[#jsonValue|jsonValue]].
 *)
const func jsonValue: jsonValue (in string: stri) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var jsonString: aString is jsonString.value;
  begin
    aString.stri := stri;
    aValue := toInterface(aString);
  end func;


const func jsonValue: readJsonString (inout file: inFile, inout string: symbol) is func
  result
    var jsonValue: aValue is jsonValue.value;
  begin
    aValue := jsonValue(symbol[2 .. pred(length(symbol))]);
    symbol := getJsonSymbol(inFile);
  end func;


const func jsonCategory: category (in jsonString: aString) is return JSON_STRING;
const func type: type (in jsonString: aString)             is return string;
const func string: string (in jsonString: aString)         is return aString.stri;
const func string: str (in jsonString: aString)            is return literal(aString.stri);


#
# jsonArray
#


##
#  jsonValue implementation type representing an array.
#
const type: jsonArray is sub jsonBase struct
    var jsonValueArray: elements is jsonValueArray.value;
  end struct;


type_implements_interface(jsonArray, jsonValue);


(**
 *  Create a [[#jsonValue|jsonValue]] with the given ''elements''.
 *   jsonValue(0 times jsonValue.value)
 *  @return a ''jsonArray'' with the given ''elements'' as [[#jsonValue|jsonValue]].
 *)
const func jsonValue: jsonValue (in jsonValueArray: elements) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var jsonArray: anArray is jsonArray.value;
  begin
    anArray.elements := elements;
    aValue := toInterface(anArray);
  end func;


const func jsonValue: readJsonArray (inout file: inFile, inout string: symbol) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var jsonArray: anArray is jsonArray.value;
  begin
    symbol := getJsonSymbol(inFile);
    if symbol <> "]" then
      anArray.elements &:= readJson(inFile, symbol);
      while symbol = "," do
        symbol := getJsonSymbol(inFile);
        anArray.elements &:= readJson(inFile, symbol);
      end while;
    end if;
    if symbol = "]" then
      symbol := getJsonSymbol(inFile);
    else
      raise RANGE_ERROR;
    end if;
    aValue := toInterface(anArray);
  end func;


const func jsonCategory: category (in jsonArray: anArray)             is return JSON_ARRAY;
const func type: type (in jsonArray: anArray)                         is return jsonValueArray;
const func jsonValue: (in jsonArray: anArray) [ (in integer: index) ] is return anArray.elements[index];
const func integer: length (in jsonArray: anArray)                    is return length(anArray.elements);
const func integer: minIdx (in jsonArray: anArray)                    is return 1;
const func integer: maxIdx (in jsonArray: anArray)                    is return maxIdx(anArray.elements);
const func array jsonValue: values (in jsonArray: anArray)            is return anArray.elements;


const func string: str (in jsonArray: anArray) is func
  result
    var string: stri is "[";
  local
    var jsonValue: element is jsonValue.value;
    var boolean: firstElement is TRUE;
  begin
    for element range anArray.elements do
      if firstElement then
        firstElement := FALSE;
      else
        stri &:= ",";
      end if;
      stri &:= str(element);
    end for;
    stri &:= "]";
  end func;


#
# jsonObject
#


##
#  jsonValue implementation type representing an object.
#
const type: jsonObject is sub jsonBase struct
    var array string: elementNames is 0 times "";
    var jsonValueMap: elements is jsonValueMap.value;
  end struct;


type_implements_interface(jsonObject, jsonValue);


const func jsonValue: readJsonObject (inout file: inFile, inout string: symbol) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var string: elementName is "";
    var jsonObject: anObject is jsonObject.value;
  begin
    symbol := getJsonSymbol(inFile);
    if symbol <> "}" then
      if startsWith(symbol, "\"") then
        elementName := symbol[2 .. pred(length(symbol))];
        symbol := getJsonSymbol(inFile);
        if symbol = ":" then
          symbol := getJsonSymbol(inFile);
          anObject.elementNames &:= elementName;
          anObject.elements @:= [elementName] readJson(inFile, symbol);
          while symbol = "," do
            symbol := getJsonSymbol(inFile);
            if startsWith(symbol, "\"") then
              elementName := symbol[2 .. pred(length(symbol))];
              symbol := getJsonSymbol(inFile);
              if symbol = ":" then
                symbol := getJsonSymbol(inFile);
                anObject.elementNames &:= elementName;
                anObject.elements @:= [elementName] readJson(inFile, symbol);
              else
                raise RANGE_ERROR;
              end if;
            else
              raise RANGE_ERROR;
            end if;
          end while;
        else
          raise RANGE_ERROR;
        end if;
      else
        raise RANGE_ERROR;
      end if;
    end if;
    if symbol = "}" then
      symbol := getJsonSymbol(inFile);
    else
      raise RANGE_ERROR;
    end if;
    aValue := toInterface(anObject);
  end func;


const func jsonCategory: category (in jsonObject: anObject) is return JSON_OBJECT;
const func type: type (in jsonObject: anObject) is return jsonValueMap;
const func jsonValue: (in jsonObject: anObject) [ (in string: name) ] is return anObject.elements[name];
const func boolean: (in string: aKey) in (in jsonObject: anObject) is return aKey in anObject.elements;
const func boolean: (in string: aKey) not in (in jsonObject: anObject) is return aKey not in anObject.elements;
const func array string: keys (in jsonObject: anObject) is return anObject.elementNames;


const func string: str (in jsonObject: anObject) is func
  result
    var string: stri is "{";
  local
    var string: elementName is "";
    var boolean: firstElement is TRUE;
  begin
    for elementName range anObject.elementNames do
      if firstElement then
        firstElement := FALSE;
      else
        stri &:= ",";
      end if;
      stri &:= literal(elementName);
      stri &:= ":";
      stri &:= str(anObject.elements[elementName]);
    end for;
    stri &:= "}";
  end func;


const func jsonValue: readJson (inout file: inFile, inout string: symbol) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var string: elementName is "";
    var jsonObject: anObject is jsonObject.value;
  begin
    # write(symbol);
    if symbol = "{" then
      aValue := readJsonObject(inFile, symbol);
    elsif symbol = "[" then
      aValue := readJsonArray(inFile, symbol);
    elsif startsWith(symbol, "\"") then
      aValue := readJsonString(inFile, symbol);
    elsif symbol <> "" and symbol[1] in digit_char | {'-'} then
      aValue := readJsonNumber(inFile, symbol);
    elsif symbol = "null" then
      aValue := readJsonNull(inFile, symbol);
    elsif symbol = "true" or symbol = "false" then
      aValue := readJsonBoolean(inFile, symbol);
    else
      raise RANGE_ERROR;
    end if;
    # TRACE_OBJ(aValue); writeln;
  end func;


(**
 *  Read a [[#jsonValue|jsonValue]] from the given ''inFile''.
 *   var file: aFile is STD_NULL;
 *   var jsonValue: json is jsonValue.value;
 *   ...
 *   aFile := openUtf8("test.json", "r");
 *   if aFile <> STD_NULL then
 *     json := readJson(aFile);
 *  @return the [[#jsonValue|jsonValue]] read from ''inFile''.
 *  @exception RANGE_ERROR The ''inFile'' does not contain valid JSON.
 *)
const func jsonValue: readJson (inout file: inFile) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var string: symbol is "";
  begin
    symbol := getJsonSymbol(inFile);
    aValue := readJson(inFile, symbol);
  end func;


(**
 *  Read a [[#jsonValue|jsonValue]] from the given ''jsonStri''.
 *   var jsonValue: json is jsonValue.value;
 *   ...
 *   json := readJson("[1, \"a\", {\"ok\" : true}]");
 *  @return the [[#jsonValue|jsonValue]] read from ''jsonStri''.
 *  @exception RANGE_ERROR The string ''jsonStri'' does not contain valid JSON.
 *)
const func jsonValue: readJson (in string: jsonStri) is func
  result
    var jsonValue: aValue is jsonValue.value;
  local
    var file: jsonFile is STD_NULL;
  begin
    jsonFile := openStriFile(jsonStri);
    aValue := readJson(jsonFile);
  end func;