Difference between revisions of "MJSON"
m |
m (line break fix) |
||
(2 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
This script is a 100% native way of parsing JSON with an easy API | This script is a 100% native way of parsing JSON with an easy API | ||
+ | The script works in 3 phases | ||
+ | 1.) Parsing. $json.parse will return the root object, and it can be quite slow. Try to do this as little as possible and re-use the value. | ||
+ | 2.) Querying. $json and $mjson can be used to read the data and is generally very fast, but requires a parsed object. | ||
+ | 3.) Deleting. json.freeall can be used to delete a json object. Call it on the root object when you are done to clean up. | ||
==Example== | ==Example== | ||
Line 47: | Line 51: | ||
json.freeall %aEmployees | json.freeall %aEmployees | ||
} | } | ||
+ | |||
+ | $mjson provides a simpler interface for quickly querying data, but it is less powerful than $json. Here is an example of how you can use $mjson to get the name of the first employee | ||
+ | |||
+ | alias json-example-simple { | ||
+ | bread json-example.txt 0 $file(json-example.txt).size &bvar | ||
+ | |||
+ | ; Parse top level | ||
+ | var %aEmployees = $json.parse(&bvar) | ||
+ | |||
+ | echo -atg The first employee is: $mjson(%aEmployees,0,name) | ||
+ | |||
+ | json.freeall %aEmployees | ||
+ | } | ||
+ | |||
+ | As you can see, this is a much simpler way to just get information quickly. However this method does not allow you to get meta-information, such as how many employees there are. | ||
==Script== | ==Script== | ||
Line 167: | Line 186: | ||
echo -atg Unknown usage of $!json --> Prop: $prop P1: $1 P2: $2 | echo -atg Unknown usage of $!json --> Prop: $prop P1: $1 P2: $2 | ||
} | } | ||
+ | } | ||
+ | ; mjson is a very thin wrapper to automatically walk down | ||
+ | ; the object chain. | ||
+ | alias mjson { | ||
+ | var %object = $1 | ||
+ | var %propertyIndex = 2 | ||
+ | while (%propertyIndex < $0) { | ||
+ | var %object = $json(%object,$($ $+ %propertyIndex,2)) | ||
+ | inc %propertyIndex | ||
+ | } | ||
+ | ; Return value of last object | ||
+ | return $json(%object,$($ $+ %propertyIndex,2)).text | ||
} | } | ||
; Hash table layout: | ; Hash table layout: | ||
Line 403: | Line 434: | ||
return %bv | return %bv | ||
} | } | ||
− | $json.parsearray - returns array tables (data, types) or error code | + | ; $json.parsearray - returns array tables (data, types) or error code |
alias -l json.parsearray { | 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) | |
− | + | noop $json.consumewhitespace($1) | |
− | + | if ($bvar($1,1) == $json.chars().ARRAY_END) { | |
− | + | json.debug Parsed empty array successfully | |
− | + | json.trimstart $1 1 | |
− | + | return %datatable %typetable | |
− | + | } | |
− | + | :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 | ||
+ | } | ||
+ | :exit | ||
+ | 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 | ; $json.consumewhitespace(&binvar) - returns number consumed |
Latest revision as of 20:31, 22 May 2015
This script is a 100% native way of parsing JSON with an easy API The script works in 3 phases 1.) Parsing. $json.parse will return the root object, and it can be quite slow. Try to do this as little as possible and re-use the value. 2.) Querying. $json and $mjson can be used to read the data and is generally very fast, but requires a parsed object. 3.) Deleting. json.freeall can be used to delete a json object. Call it on the root object when you are done to clean up.
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 }
$mjson provides a simpler interface for quickly querying data, but it is less powerful than $json. Here is an example of how you can use $mjson to get the name of the first employee
alias json-example-simple { bread json-example.txt 0 $file(json-example.txt).size &bvar ; Parse top level var %aEmployees = $json.parse(&bvar) echo -atg The first employee is: $mjson(%aEmployees,0,name) json.freeall %aEmployees }
As you can see, this is a much simpler way to just get information quickly. However this method does not allow you to get meta-information, such as how many employees there are.
Script
;$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) ; Given a handle (use json.parse first) allows you to index an array or get a handle to a property by its name ; .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 ; Get a property value ; Index into an array ; Get the Nth property name of an object ; .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 == 3) { ; Can't index non array echo -atg $json Cannot index data type 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 && $2 !isnum) { 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 } } elseif (%type == 1 && $2 isnum) { ; Indexing object by index offset var %thisIndex = 0 var %i = 1 while ($hget(%value,%i).item) { inc %i var %item = $ifmatch var %propName = $gettok(%item,2-,$asc(.)) if (type.* iswm %item) { inc %thisIndex } if (%thisIndex == $2) { return %propName } if ($2 == 0) { return %thisIndex } return $null } } else { echo -atg Unknown usage of $!json --> Prop: $prop P1: $1 P2: $2 } } ; mjson is a very thin wrapper to automatically walk down ; the object chain. alias mjson { var %object = $1 var %propertyIndex = 2 while (%propertyIndex < $0) { var %object = $json(%object,$($ $+ %propertyIndex,2)) inc %propertyIndex } ; Return value of last object return $json(%object,$($ $+ %propertyIndex,2)).text } ; 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 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 1 %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 ; 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 bset %bv $calc($bvar(%bv,0) +1) 0 return %bv } ; Add this character to the end bset %bv $calc($bvar(%bv,0) +1) $bvar($1,1) if (%escape) { dec %escape } :trim ; Trim from the start json.trimstart $1 1 ; 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) noop $json.consumewhitespace($1) if ($bvar($1,1) == $json.chars().ARRAY_END) { json.debug Parsed empty array successfully json.trimstart $1 1 return %datatable %typetable } :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 } :exit 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- }