MJSON

From Scriptwiki
Revision as of 07:29, 23 January 2015 by Aca20031 (talk | contribs) (Script)

Jump to: navigation, search

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
}

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
  }
}
; 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-
}