Sunday, March 14, 2010

ObjectScript JSON decoder/encoder

Those who develop in web application using Cache ObjectScript might find the lack of a native JSON library disturbing. The ZEN Google-group offers a JSON encoder/decoder, but I found it missing several important feature - such as supporting escaping and standard quoting (JSON.org allows to quote string only with ", and not with ').

Here I offer a modification for the class which supports character escaping, double quote strings, and some bugs were fixed. This class is supplied with a unitest method as well.

JSON.cls
Class JSON Extends %String
{

Parameter EscapeChar As COSEXPRESSION = "$LB($LB(""\"",""\\""),$LB($C(13),""\n""),$LB($C(10),""\r""),$LB($C(9),""\t""),$LB("""""""",""\""""""),$LB($C(8),""\b""),$LB($C(12),""\f""))";

Parameter UnEscapeChar As COSEXPRESSION = "$LB(""\\"",""\n"",""\r"",""\t"",""\"""""",""\b"",""\f"")";

Parameter JSonSlice [ Final, Internal ] = 1;

Parameter JSonInString [ Final, Internal ] = 2;

Parameter JSonInArray [ Final, Internal ] = 3;

Parameter JSonInObject [ Final, Internal ] = 4;

ClassMethod GetEscapeChars() As %String
{
  Quit ..#EscapeChar
}

ClassMethod SetAux(what As %String, where As %Integer, delim As %String) As %DataType [ Internal ]
{
  aux=##class(%ArrayOfDataTypes).%New()
  aux.SetAt(what,"what")
  aux.SetAt(where,"where")
  aux.SetAt(delim,"delim")
 
  aux
}

/// we know that it's not escaped becase there is _not_ an
/// odd number of backslashes at the end of the string so far
ClassMethod isEscaped(str As %String, As %String) As %Boolean [ Internal ]
{
  pos=$F(str,c)
  ($L($E(str,1,pos))-$L($REPLACE($E(str,1,pos),"\","")))#2=1
}

/// Escapes the string.
ClassMethod Escape(str As %String) As %String [ Internal ]
{
  for tI=1:1:$LL(..#EscapeChar)
    Set tCharPair=$LG(..#EscapeChar,tI)
    Set str=$Replace(str,$LG(tCharPair,1),$LG(tCharPair,2))
  }
  Quit str
}

ClassMethod Unescape(str As %String) As %String [ Internal ]
{
  For tI=1:1:$Length(str){
    Set tChar=$ListFind(..#UnEscapeChar,$E(str,tI,tI+1))
    if (tChar>0){
      Set $E(str,tI,tI+1)=$LG($LG(..#EscapeChar,tChar),1)
    }
  }
  Quit str
}

/// Decode a string JSON.
ClassMethod Decode(str As %String) As %ArrayOfDataTypes
{
  #dim stack as %ListOfDataTypes
  matchType=$ZCVT(str,"L")
 
  q:(matchType="true") "1"
  q:(matchType="false") "0"
  q:(matchType="null") ""  
  q:($ISVALIDNUM(matchType)) matchType 
  q:str?1"""".E1"""" ..Unescape($e(str,2,$l(str)-1))
  //$replace($e(str,2,$l(str)-1),"\""","""")
 
  // array or object notation
  match=str?1(1"[".E1"]",1"{".E1"}")
  stack=##class(%ListOfDataTypes).%New()
 
  if match {
    if $E(str,1)="[" {
      stack.Insert(..#JSonInArray)
      arr=##class(%ListOfDataTypes).%New()
    }  
    else {
      stack.Insert(..#JSonInObject)
      obj=##class(%ArrayOfDataTypes).%New()
    }
   
    stack.Insert(..SetAux(..#JSonSlice,1,"false"))
   
    chars=$E(str,2,$L(str)-1)
   
    if chars="" {
      if stack.GetAt(1)=..#JSonInArray {
        arr
      }
      else {
        obj
      }  
    }

    strlenChars=$L(chars)+1

    escaped=0
    For c=1:1:strlenChars {
      last=stack.Count()
      top=stack.GetAt(last)
     
      s:(escaped=2) escaped=0
      s:(escaped=1) escaped=2
     
      substrC2=$E(chars,c-1,c)
      if ($E(chars,c,c)="\")&&(escaped=0) escaped=1
      
      if $e(chars,c)="" {
        a=22
      }
     
      if (c=strlenChars || ($E(chars,c)=",")) && (top.GetAt("what")=..#JSonSlice) {
        // found a comma that is not inside a string, array, etc.,
        // OR we've reached the end of the character list
        slice = $E(chars, top.GetAt("where"),c-1)
        stack.Insert(..SetAux(..#JSonSlice,c+1,"false"))
        if stack.GetAt(1)=..#JSonInArray {
          // we are in an array, so just push an element onto the stack
          arr.Insert(..Decode(slice))
        }
        elseif stack.GetAt(1)=..#JSonInObject {
          // we are in an object, so figure
          // out the property name and set an
          // element in an associative array,
          // for now
                   
          match=slice?." "1""""1.E1""""." "1":"1.E
          if match {
            //'name':value par
            key1=$p(slice,":")
            key=..Decode(key1)

            val=..Decode($P(slice,":",2,$l(slice,":")))
            obj.SetAt(val, key)
                    
          }
        }
      }
      elseif $E(chars,c)="""" && (top.GetAt("what")'=..#JSonInString) {
        // found a quote, and we are not inside a string
        stack.Insert(..SetAux(..#JSonInString,c,$E(chars,c)))
      }
      elseif $E(chars,c)=top.GetAt("delim") && (top.GetAt("what")=..#JSonInString) && (escaped=0) {
        // found a quote, we're in a string, and it's not escaped (look 3 charachters behind, to see the \" is not \\" )
        last=stack.Count()
        st=stack.RemoveAt(last)
      }
      elseif ($E(chars,c)="[") && (top.GetAt("what")'=..#JSonInString) && ($CASE(top.GetAt("what"),..#JSonInString:1,..#JSonInArray:1,..#JSonSlice:1,:0))
        // found a left-bracket, and we are in an array, object, or slice
        stack.Insert(..SetAux(..#JSonInArray,c,"false"))
      }
      elseif $E(chars,c)="]" && (top.GetAt("what")=..#JSonInArray) {
        // found a right-bracket, and we're in an array
        last=stack.Count()
        st=stack.RemoveAt(last)
      }
      ;modificacio 19/11/08: ..#JSonString -> #JSonInArray
      elseif $E(chars,c)="{" && ($CASE(top.GetAt("what"),..#JSonSlice:1,..#JSonInArray:1,..#JSonInObject:1,:0)) {
        // found a left-brace, and we are in an array, object, or slice
        stack.Insert(..SetAux(..#JSonInObject,c,"false"))
      }
      elseif $E(chars,c)="}" && (top.GetAt("what")=..#JSonInObject) {
        // found a right-brace, and we're in an object
        last=stack.Count()
        st=stack.RemoveAt(last)
      }
     
    }  
   
    if stack.GetAt(1)=..#JSonInObject {
      obj
    }
    elseif stack.GetAt(1)=..#JSonInArray {
      arr
    }
  }
  str
}

/// Encode a Cache string to a JSON string
ClassMethod Encode(data As %DataType) As %String
{

  if $IsObject(data) {  
    key=""
   
    typeData=data.%ClassName()

    if typeData="%ArrayOfDataTypes" {
      //type object
      key=""
      cad=""
      {
        pData=data.GetNext(.key)
        q:key=""
        value=..Encode(pData)
        cad=$S(cad'="":cad_",",1:"")_""""_..Escape(key)_""":"_value  
      
      "{"_cad_"}"
    }
    elseif typeData="%ListOfDataTypes" {
      //type array
     
      cad=""
      i=1:1:data.Count() {
        tmp=..Encode(data.GetAt(i))
        cad=$S(i>1:cad_",",1:"")_tmp
      }
     
      cad="["_cad_"]"
      cad
    }
  }
  elseif $ISVALIDNUM(data) {
    // type number
    data
  }
  else {
    //type string
    q:data="" "null"
    """"_..Escape(data)_""""
  }
}

ClassMethod CreateStringPair(pKey As %String, pValue As %String) As %String
{
  Quit """"_pKey_""":"""_..Escape(pValue)_""""
}

ClassMethod Parse(pStr As JSON) As %ArrayOfDataTypes
{
  Quit ##class(JSON).Decode(pStr)
}

ClassMethod Stringify(pData As %DataType) As JSON
{
  Quit ##class(JSON).Encode(pData)
}

}
 



TestJSON
Method TestJSON()
{
  #Dim tException As %Exception.AbstractException
  Try {    
    Set tEscapeChars=##class(RSA.Data.DT.Simple.JSON).GetEscapeChars()
    Set tEscapeChars=tEscapeChars_$LB($LB("[","["),$LB("]","]"),$LB("{","{"),$LB("}","}"),$LB(":",":"))
    for tI=1:1:$LL(tEscapeChars){
      For tJ=1:1:$LL(tEscapeChars){
        for tK=1:1:$LL(tEscapeChars){
         
        Set tChar1=$LG(tEscapeChars,tI)
        Set tChar2=$LG(tEscapeChars,tJ)
        Set tChar3=$LG(tEscapeChars,tK)
       
        Set tJSON="{""name"":"""_$LG(tChar1,2)_$LG(tChar2,2)_$LG(tChar3,2)_" is char""}"
        Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
        Set tName=tObj.GetAt("name")
        Do $$$AssertEquals(tName,$LG(tChar1,1)_$LG(tChar2,1)_$LG(tChar3,1)_" is char","Escaping for "_$LG(tChar1,2)_$LG(tChar2,2)_$LG(tChar3,2))
       
        Set tJSON="{""name"":""the "_$LG(tChar1,2)_$LG(tChar2,2)_$LG(tChar3,2)_" is char""}"
        Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
        Set tName=tObj.GetAt("name")
        Do $$$AssertEquals(tName,"the "_$LG(tChar1,1)_$LG(tChar2,1)_$LG(tChar3,1)_" is char","Escaping middle for "_$LG(tChar1,2)_$LG(tChar2,2)_$LG(tChar3,2))
       
        Set tJSON="{""name"":""the "_$LG(tChar1,2)_$LG(tChar2,2)_$LG(tChar3,2)_"""}"
        Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
        Set tName=tObj.GetAt("name")
        Do $$$AssertEquals(tName,"the "_$LG(tChar1,1)_$LG(tChar2,1)_$LG(tChar3,1),"Escaping end for "_$LG(tChar1,2)_$LG(tChar2,2)_$LG(tChar3,2))
       
       
        if (tK=1){
          if (tI=1){
            Set tJSON="{""name"":"""_$LG(tChar1,2)_" is char""}"
            Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
            Set tName=tObj.GetAt("name")
            Do $$$AssertEquals(tName,$LG(tChar1,1)_" is char","Escaping for "_$LG(tChar1,2))
     
            Set tJSON="{""name"":""the "_$LG(tChar1,2)_" is char""}"
            Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
            Set tName=tObj.GetAt("name")
            Do $$$AssertEquals(tName,"the "_$LG(tChar1,1)_" is char","Escaping middle for "_$LG(tChar1,2))
     
            Set tJSON="{""name"":""the "_$LG(tChar1,2)_"""}"
            Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
            Set tName=tObj.GetAt("name")
            Do $$$AssertEquals(tName,"the "_$LG(tChar1,1),"Escaping end for "_$LG(tChar1,2))
           
            for tC=97:1:122{
              Set tJSON="{""name"":"""_$LG(tChar1,2)_$C(tC)_"""}"
              Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
              Set tName=tObj.GetAt("name")
              Do $$$AssertEquals(tName,$LG(tChar1,1)_$C(tC),"Escaping for "_$LG(tChar1,2)_$C(tC))
            }
          }
       
          Set tJSON="{""name"":"""_$LG(tChar1,2)_$LG(tChar2,2)_" is char""}"
          Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
          Set tName=tObj.GetAt("name")
          Do $$$AssertEquals(tName,$LG(tChar1,1)_$LG(tChar2,1)_" is char","Escaping for "_$LG(tChar1,2)_$LG(tChar2,2))
       
          Set tJSON="{""name"":""the "_$LG(tChar1,2)_$LG(tChar2,2)_" is char""}"
          Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
          Set tName=tObj.GetAt("name")
          Do $$$AssertEquals(tName,"the "_$LG(tChar1,1)_$LG(tChar2,1)_" is char","Escaping middle for "_$LG(tChar1,2)_$LG(tChar2,2))
       
          Set tJSON="{""name"":""the "_$LG(tChar1,2)_$LG(tChar2,2)_"""}"
          Set tObj=##class(RSA.Data.DT.Simple.JSON).Parse(tJSON)
          Set tName=tObj.GetAt("name")
          Do $$$AssertEquals(tName,"the "_$LG(tChar1,1)_$LG(tChar2,1),"Escaping end for "_$LG(tChar1,2)_$LG(tChar2,2))
          }
        }
      }
    }
  catch tException {
    Do $$$AssertEquals(1,0,"Exception thrown - " _ tException.Code_ ": " _ tException.Name _ " " _ tException.Data _ " " _ tException.Location)
  }
}
 

3 comments:

  1. Yonatan - Like your Cache JSON code very much. Am working on a Cache project now but don't understand Cache classes very well. Can you provide some additional information re: how to set up and use your class within Cache ObjectScript?

    Many thanks in advance for your help!!

    Brian

    ReplyDelete
  2. Yonatan - I'm interested in extending the features of this JSON utility. Could you please reach out to me via Twitter direct message to discuss?

    Thanks,
    @mccrackend

    ReplyDelete
  3. Thanks! I'm a Caché developer of 25 years. Will start using this...

    ReplyDelete