MJSON
This script is a 100% native way of parsing JSON with an easy API
Example
To use the below example, Save the following JSON in "json-example.txt" in $mircdir
[
{
"name":"Todd",
"IsManager":false,
"Spouse":{"Name":"Marie"},
"Sallary":100000
},
{
"name":"Gena",
"IsManager":true,
"Spouse":null,
"Sallary":157103e-3
}
]
And then run /json-example with the script loaded.
alias json-example {
bread json-example.txt 0 $file(json-example.txt).size &bvar
; Parse top level
var %aEmployees = $json.parse(&bvar)
echo -atg The top level is a(n) $json.type(%aEmployees)
var %cEmployees = $json(%aEmployees).length
echo -atg Number of employees: %cEmployees
var %i = 0
while (%i < %cEmployees) {
var %oEmployee = $json(%aEmployees,%i)
inc %i
echo -atg Name: $json(%oEmployee,name).text
echo -atg IsManager: $json(%oEmployee,ismanager).text
var %spouse = $json(%oEmployee,spouse)
if (%spouse) { echo -atg Spouse: $json(%spouse,name).text }
echo -atg Sallary: $json(%oEmployee,Sallary).text
}
; Free memory
json.freeall %aEmployees
}
;$json.parse(&bvar | json string) - Parses a json and returns you a "handle". Use this in the other functions
; If the handle is "ERROR N" then N is an error code
; $json(handle,N/PROPERTY)
; Give a handle (use json.parse first) allows you to index an array get a handle to a property
; .text - Returns a data property or array index as text, e.g. $json($json.parse({"foo":"bar"}),bar).text
; .bvar - Returns a data property or array index as a binvar
; $json.freeall(handle)
; Frees memory used to parse the given handle
;$json.parse(&bvar | json string) - Parses a json and returns you a "handle". Use this in the other functions
; If the handle is "ERROR N" then N is an error code
alias json.parse {
var %bvar = $false
if (&* iswm $1) {
%bvar = $1
}
else {
%bvar = $json.createbvar
bset -t %bvar 1 $1-
}
return $json.parseany(%bvar)
}
; $json.type(handle) - Returns ARRAY, OBJECT, or Data
alias json.type {
tokenize 32 $1-
if ($1 == 1) { return OBJECT }
elseif ($1 == 2) { return ARRAY }
elseif ($1 == 3) { return DATA }
else return UNKNOWN TYPE $1
}
; $json(handle [,N/PROPERTY])
; Give a handle (use json.parse first) allows you to index an array get a handle to a property
; .text - Returns a data property or array index as text, e.g. $json($json.parse({"foo":"bar"}),bar).text
; .bvar - Returns a data property or array index as a binvar
; .length - If the property is an array, or the handle is an array that isn't indexed, returns its length
alias json {
var %type = $gettok($1,1,32)
var %value = $gettok($1,2-,32)
if ($2 isnum && %type != 2) {
; Can't index non array
echo -atg $json Cannot use indexer on non-array value
return
}
; Indexing into an array
if ($2 isnum && %type == 2) {
var %datatable = $gettok(%value,1,32)
var %typetable = $gettok(%value,2,32)
var %thistype = $hget(%typetable,$2)
var %thisdata = $hget(%datatable,$2)
if (!%thistype) { return $null }
if (%thistype == 3 && $prop == bvar) {
var %bv = $json.createbvar
noop $hget(%datatable,$2,%bv)
return %bv
}
elseif (%thistype == 3 && $prop == text) {
return %thisdata
}
elseif (%thistype == 3) {
var %bv = $json.createbvar
noop $hget(%datatable,$2,%bv)
return %thistype %bv
}
elseif (%thistype != 3) {
return %thistype %thisdata
}
}
elseif (%type == 2 && $prop == length) {
; If we're not indexing, but we have an array with the .length property, return its length
; Return length of the data table for the array
return $hget($gettok(%value,1,32),0).item
}
; Getting a property of an object
elseif (%type == 1) {
var %proptype = $json.getproperty(%value,$2).type
var %propvalue = $json.getproperty(%value,$2).value
var %propbv = $json.getproperty(%value,$2).bvar
if (!%proptype) { return $null }
if (%proptype == 3 && $prop == text) {
return %propvalue
}
elseif (%proptype == 3 && $prop == bvar) {
return %propbv
}
elseif (%proptype == 3) {
return %proptype %propbv
}
elseif (%proptype == 2 && $prop == length) {
; Return length of the data table for the array
return $hget($gettok(%propvalue,1,32),0).item
}
else {
return %proptype %propvalue
}
}
else {
echo -atg Unknown usage of $!json --> Prop: $prop P1: $1 P2: $2
}
}
; Hash table layout:
; Name: json.<ticks><rand1,4294967295>
; Key: Name of json property
; Value <N> <value>
; N: 1 = pointer to another object, 3 = raw value
; 2 = <pointer to data array> <pointer to assoc array indicating types of data array>
; $json.proxyrecurse(idname, param1, param2)
alias -l json.proxyrecurse2 {
; json.debug Recurse on: return $($+($,$1,$chr(40),$2,$chr(44),$3,$chr(41)),1)
return $($+($,$1,$chr(40),$2,$chr(44),$3,$chr(41)),2)
}
; /json.freeall <type> <value>
alias -l json.freeall {
if (!$isid) {
noop $json.proxyrecurse2(json.freeall,$1,$2-)
return
}
json.debug ?4freeall $1-
; For binary data, just return
if ($1 == 3) {
return
}
; For arrays, call on each element then hfree tables
elseif ($1 == 2) {
var %datatable = $gettok($2,1,32)
var %typetable = $gettok($2,2,32)
json.debug FreeAll: Freeing array, datatable: %datatable - typetable: %typetable
var %i = 0
while ($hget(%typetable,%i)) {
var %elementType = $ifmatch
if (%elementType == 3) {
inc %i
continue
}
noop $json.proxyrecurse2(json.freeall,%elementType,$hget(%datatable,%i))
inc %i
}
.hfree %datatable
.hfree %typetable
}
; For objects, call on every property then hfree table
elseif ($1 == 1) {
var %i = 1
while ($hget($2,%i).item) {
inc %i
var %item = $ifmatch
if (type.* iswm %item) {
var %membername = $gettok(%item,2-,$asc(.))
noop $json.proxyrecurse2(json.freeall,$hget($2,%item), $hget($2,value. $+ %membername))
}
}
.hfree $2
}
}
; Parses an array, object, string or int, depending on the next character, and returns <type> <value>
; or returns "ERROR <error>
alias -l json.parseany {
; Now, read the value of the property
if ($bvar($1,1) == $json.chars().STRING_START) {
json.debug Parseany detected a string
var %strVal = $json.parsestring($1)
if (%strVal isnum) {
json.debug Error parsing string: %error
return ERROR %strVal
}
json.debug ParseAny: Read string value: $bvar(%strval,1-).text
return 3 %strval
}
elseif ($bvar($1,1) == $json.chars().OBJECT_START) {
json.debug ParseAny: detected an object
var %obj = $json.parseobject($1)
if (%obj isnum) { json.debug ParseAny: Error reading object subvalue ( $+ %obj $+ ) | return ERROR %obj }
json.debug ParseAny: Read object subvalue as %obj
return 1 %obj
}
elseif ($bvar($1,1) == $json.chars().ARRAY_START) {
json.debug Parseany detected an array
var %array = $json.parsearray($1)
if (%array isnum) {
json.debug ParseAny: error reading array subvalue: %array
return ERROR %array
}
json.debug ParseAny: Read array subvalue as %array
return 2 %array
}
elseif ($bvar($1,1,4).text == null) {
json.debug ParseAny found NULL value
json.trimstart $1 4
return 3 $null
}
elseif ($bvar($1,1,5).text == false || $bvar($1,1,4).text == true) {
var %value = $iif($bvar($1,1,5).text == false,$false,$true)
; if true, trim 'true' off the top, otherwise trim 'false'
if (%value) { json.trimstart $1 4 }
else { json.trimstart $1 5 }
json.debug ParseAny found bool value: %value
return 3 %value
}
; Must be an int
else {
var %int = $json.parseint($1)
if (%int isnum) {
json.debug Error reading int subvalue: %int
return ERROR %int
}
if ($bvar(%int,0) == 0) {
return ERROR $json.errors().UNEXPECTED_CHAR
}
json.debug Read int subvalue into var %int as $bvar(%int,1-).text
return 3 %int
}
}
; $json.parseobject(&binvar) - returns object name or error code
alias -l json.parseobject {
if ($bvar($1,1) == $json.chars().OBJECT_START) {
; Trim off {
json.trimstart $1 1
}
var %table = $json.createtable
var %error = 0
:property
noop $json.consumewhitespace($1)
if ($bvar($1,1) != $json.chars().STRING_START) { %error = $json.errors().EXPECTED_START | goto return }
; Property name
var %strPropName = $json.parsestring($1)
if (%strPropName isnum) { json.debug Error reading property name for object $1 }
else {
%strPropName = $bvar(%strPropName,1-).text
json.debug ParseObject: Read property %strPropName
noop $json.consumewhitespace($1)
if ($bvar($1,1) != $json.chars().MAPPING) { %error = $json.errors().EXPECTED_MAPPING | goto return }
; trim the mapping character
json.trimstart $1 1
noop $json.consumewhitespace($1)
var %any = $json.parseany($1)
var %type = $gettok(%any,1,32)
var %value = $gettok(%any,2-,32)
if (%type == ERROR) {
%error = %value
goto return
}
if (%type == 3) {
; rvalue
hadd %table type. $+ %strPropName 3
if (%value == $true || %value == $false) {
json.debug PasrseObject; Found boolean value for %strPropName : %value
hadd %table value. $+ %strPropName %value
}
elseif (!$bvar(%value,0)) {
hadd %table value. $+ %strPropName $null
}
else {
hadd -b %table value. $+ %strPropName %value
}
}
else if (%type == 2) {
; Arrays
json.debug Read array %strPropName as %value for object %table
hadd %table type. $+ %strPropName 2
; Add array hashtable
hadd %table value. $+ %strPropName %value
}
else if (%type == 1) {
hadd %table type. $+ %strPropName 1
; Add object naem to table
hadd %table value. $+ %strPropName %value
}
}
; Check to see if there are more properties
noop $json.consumewhitespace($1)
if ($bvar($1,1) == $json.chars().OBJECT_END) { json.debug Successful object parsing. | json.trimstart $1 1 | goto return }
elseif ($bvar($1,1) == $json.chars().DELIMITER) { json.trimstart $1 1 | goto property }
else { return $json.errors().UNEXPECTED_CHAR }
:return
if (%error) {
json.debug ParseObject Exiting with error %error
json.freeall %table
return %error
}
return %table
}
; $json.parsestring(&binvar) - returns string (binvar) or error code
alias -l json.parsestring {
var %bv = $json.createbvar
if ($bvar($1,1) != $json.chars().STRING_START) { return $json.errors().EXPECTED_START }
json.trimstart $1 1
var %escape = 0
:loop
if ($bvar($1,1) == $json.chars().STRING_ESCAPE) {
if (%escape) {
; If the escape character is escaped, append it
dec %escape
goto append
}
else {
inc %escape
; Skip appending
goto trim
}
}
if (%escape) {
if ($bvar($1,1) == $asc(r)) { bset $1 1 $cr | dec %escape }
if ($bvar($1,1) == $asc(n)) { bset $1 1 $lf | dec %escape }
if ($bvar($1,1) == $asc(t)) { bset $1 1 9 | dec %escape }
if ($bvar($1,1) == $asc(b)) { bset $1 1 8 | dec %escape }
}
:append
; Add this character to the end
bset %bv $calc($bvar(%bv,0) +1) $bvar($1,1)
:trim
; Trim from the start
json.trimstart $1 1
; If we're at the end of a string and not escaped, bail out
if ($bvar($1,1) == $json.chars().STRING_END && !%escape) {
json.trimstart $1 1
return %bv
}
; Make sure we haven't consumed all the input
if ($bvar($1,0)) {
goto loop
}
return %bv
}
; $json.parseint(&binvar) - returns an int or error code
alias -l json.parseint {
var %bv = &json.createbvar
; Lax on the number format since its a string for us anyway
while ($bvar($1,1).text isnum || $bvar($1,1) == $asc(.) || $bvar($1,1) == $asc(-) || $bvar($1,1) == $asc(+) || $bvar($1,1) == $asc(e)) {
bset %bv $calc($bvar(%bv,0) +1) $bvar($1,1)
json.trimstart $1 1
}
return %bv
}
$json.parsearray - returns array tables (data, types) or error code
alias -l json.parsearray {
if ($bvar($1,1) != $json.chars().ARRAY_START) {
return $json.errors().EXPECTED_START
}
json.trimstart $1 1
; Create a linked list for the data
var %datatable = $json.createtable(1)
; Create a linked list for the types (associative with respect to the data list)
var %typetable = $json.createtable(1)
:member
noop $json.consumewhitespace($1)
; Parse members
var %any = $json.parseany($1)
var %type = $gettok(%any,1,32)
var %value = $gettok(%any,2-,32)
if (%type == ERROR) {
json.debug Error parsing array member: %value
json.freeall 2 %datatable %typetable
return %value
}
else {
; 0 indexed
var %newindex = $calc($hget(%datatable,0).item)
; Binary data
json.debug ParseArray: Add type %type array index %newindex as %value
if (%type == 3) {
hadd -b %datatable %newindex %value
}
else {
hadd %datatable %newindex %value
}
hadd %typetable %newindex %type
}
noop $json.consumewhitespace($1)
if ($bvar($1,1) == $json.chars().DELIMITER) {
json.trimstart $1 1
goto member
}
elseif ($bvar($1,1) == $json.chars().ARRAY_END) {
json.debug Parsed array successfully
json.trimstart $1 1
return %datatable %typetable
}
json.debug ParseArray failed with an unknown error. Read characters: $bvar($1,1)
json.freeall 2 %datatable %typetable
return $json.errors().UNEXPECTED_CHAR
}
; $json.consumewhitespace(&binvar) - returns number consumed
alias -l json.consumewhitespace {
while ($bvar($1,0) && ($bvar($1,1) == 32 || $bvar($1,1) == 9 || $bvar($1,1) == 160 || $bvar($1,1) == 10 || $bvar($1,1) == 13)) {
json.trimstart $1 1
}
}
; /json.trimstart &binvar length - trims length characters from the start of $binvar
alias -l json.trimstart {
; json.debug Trimstart: $1 $2 -- Size of bvar: $bvar($1,0)
if ($bvar($1,0) == 1) { bunset $1 }
else { bcopy -c $1 1 $1 $calc($2 +1) -1 }
}
; $json.getpropery(table, property).type - Returns number representing type of property
; $json.getproperty(table, property).value - Returns value of property
; $json.getproperty(table, property).bvar - Returns value of property as bvar
; $json.getproperty(table, property).arraytypetable - If property is array, returns its type table
; $json.getproperty(table, property).arraydatatable - If property is table,r eturns its data table
alias -l json.getproperty {
var %type = $hget($1,type. $+ $2)
var %value = $hget($1,value. $+ $2)
if ($prop == type) { return %type }
if ($prop == bvar) { noop $hget($1, value. $+ $2, &bvReturn) | return &bvReturn }
if ($prop == arraytypetable) { return $gettok(%value,2,32) }
if ($prop == arraydatatable) { return $gettok(%value,1,32) }
return %value
}
; $json.createtable - Creates a hashtable and returns its name
; $json.createtable(N) - gives it size N
alias -l json.createtable {
var %ticks = $ticks
var %rand = $rand(1,4294967295)
var %table = $+(json.,%ticks,.,%rand)
hmake %table $iif($1,$1,10)
return %table
}
; Creates a random binvar name to avoid collisions
alias -l json.createbvar {
var %ticks = $ticks
var %rand = $rand(1,4294967295)
var %bv = $+(&jsonbv.,%ticks,.,%rand)
return %bv
}
alias -l json.chars {
; {
if ($prop == OBJECT_START) { return 123 }
; }
if ($prop == OBJECT_END) { return 125 }
; "
if ($prop == STRING_START || $prop == STRING_END) { return 34 }
if ($prop == STRING_ESCAPE) { return $asc(\) }
if ($prop == ARRAY_START) { return $asc([) }
if ($prop == ARRAY_END) { return $asc(]) }
; ,
if ($prop == DELIMITER) { return 44 }
if ($prop == MAPPING) { return $asc(:) }
}
alias -l json.errors {
if ($prop == EXPECTED_START) { return 1 }
if ($prop == EXPECTED_END) { return 2 }
if ($prop == EXPECTED_DELIMITER) { return 3 }
if ($prop == EXPECTED_MAPPING) { return 4 }
if ($prop == UNEXPECTED_CHAR) { return 5 }
return 255
}
alias -l json.debug {
; echo -atg Debug: $1-
}