Page 1 of 1

JSON Module + example

Posted: Fri Apr 11, 2014 11:01 pm
by kenmo
Hello! I have written a Module to create, modify, save, and load JSON files.

I know this has been done in PureBasic at least once before, but I wrote my own from scratch because:
(a) I wanted to learn the JSON format
(b) I wanted to learn PB's new Module system
(c) I wanted it to mimic PB's XML library closely (which is why everything is called a Node)

The available Procedures and Constants are listed in the DeclareModule section. It should compile with older versions of PureBasic too, just as a normal include file rather than a Module.

There is also a detailed example program at the bottom, which you can run directly. Otherwise there is not much documentation, but it should be easy to use if you're used to PB's XML functions.

Limitations:
(1) There is no error reporting yet. If it fails or some syntax is not valid, most functions return #Null, there is no description of why.
(2) I don't guarantee it will load every real-world JSON file, I just followed RFC 7159.
(3) It will work in both ASCII or Unicode mode, but it can only save/load ASCII files. Characters above 0xFF must be escaped as "\uXXXX" and surrogate pairs are NOT implemented yet :oops:
(4) It is not super-optimized in terms of speed or memory savings, but it should be fine for medium sized files.

I am open to suggestions and feedback!


JSON.pbi

Code: Select all

; +----------+
; | JSON.pbi |
; +----------+
; |  3/28/2014 . Creation (PureBasic 5.22)
; |  4/05/2014 . Version 1.00
; |  4/11/2014 . Version 1.10 (removed PB Lists, all is now pointer-based)


;-
;- ----- JSON Module -----

CompilerIf (#PB_Compiler_Version >= 520)
DeclareModule JSON
CompilerEndIf
  
  
  ;- Constants
  
  ; JSON module version
  #JSON_Version = 110
  
  ; JSON node types
  #JSON_Invalid = 0
  #JSON_Number  = 1
  #JSON_String  = 2
  #JSON_Boolean = 3
  #JSON_Array   = 4
  #JSON_Object  = 5
  #JSON_Null    = 6
  
  ; SaveJSON() formats
  #JSON_Compact  = $0001
  #JSON_Indented = $0002
  
  ; Other JSON constants
  #JSON_Default  = -1
  #JSON_First    =  0
  #JSON_Last     = -1
  
  
  
  ;- Procedures
  
  ; JSON management
  Declare.i CreateJSON(MainNodeType = #JSON_Object)
  Declare.i CatchJSON(*Address, Length)
  Declare.i LoadJSON(File.s)
  Declare.i SaveJSON(*JSON, File.s, Format = #JSON_Default, IndentSize = 4)
  Declare.i FreeJSON(*JSON)
  
  ; JSON node management
  Declare.i CreateJSONNode(*Parent, Type = #JSON_Object, Key.s = "", *Previous = #JSON_Last)
  Declare.i DeleteJSONNode(*Node)
  Declare.i AddJSONString(*Parent, Text.s, Key.s = "", *Previous = #JSON_Last)
  Declare.i AddJSONNumber(*Parent, Value.f, Key.s = "", *Previous = #JSON_Last)
  Declare.i AddJSONBoolean(*Parent, Value, Key.s = "", *Previous = #JSON_Last)
  Declare.i AddJSONNull(*Parent, Key.s = "", *Previous = #JSON_Last)
  Declare.i AddJSONArray(*Parent, Key.s = "", *Previous = #JSON_Last)
  Declare.i AddJSONObject(*Parent, Key.s = "", *Previous = #JSON_Last)
  
  ; JSON node iteration
  Declare.i MainJSONNode(*JSON)
  Declare.i ChildJSONNode(*Node, Index = #JSON_First)
  Declare.i ParentJSONNode(*Node)
  Declare.i NextJSONNode(*Node)
  Declare.i PreviousJSONNode(*Node)
  Declare.i NamedJSONNode(*Node, Path.s)
  Declare.i FindJSONNodeByID(*Node, Text.s, Key.s = "id")
  
  ; JSON node access
  Declare.s GetJSONNodeText(*Node)
  Declare.f GetJSONNodeValue(*Node)
  Declare.i GetJSONNodeInteger(*Node)
  Declare.s GetJSONNodeKey(*Node)
  Declare.i GetJSONChildCount(*Node)
  Declare.i GetJSONNodeType(*Node)
  Declare.s GetJSONTypeName(Type)
  
  ; JSON node modification
  Declare.i SetJSONNodeText(*Node, Text.s)
  Declare.i SetJSONNodeValue(*Node, Value.f)
  Declare.i SetJSONNodeKey(*Node, Key.s)
  
  
CompilerIf (#PB_Compiler_Version >= 520)
EndDeclareModule
CompilerEndIf

;- ------------------------------



























;-

CompilerIf (#PB_Compiler_Version >= 520)
Module JSON
CompilerEndIf

CompilerIf (#PB_Compiler_IsMainFile)
  EnableExplicit
CompilerEndIf



; Constants - Private

; JSON type constants
#xJSON_Types = 7

; JSON node flags
#xJSON_IsRoot = $0001



; Structures - Private

Structure JSONNODE
  *Parent.JSONNODE
  Key.s
  Type.i
  Raw.s
  String.s
  Number.f
  Flags.i
  ;
  *First.JSONNODE
  *Last.JSONNODE
  Children.i
  ;
  *Next.JSONNODE
  *Prev.JSONNODE
EndStructure



; Macros - Private

CompilerIf (#PB_Compiler_Unicode)
  
  Macro Unicode()
    (#True)
  EndMacro
  Macro CharSize()
    (2)
  EndMacro
  Macro ToBytes(Chars)
    ((Chars) * 2)
  EndMacro
  Macro ToChars(Bytes)
    ((Bytes) / 2)
  EndMacro
  
CompilerElse
  
  Macro Unicode()
    (#False)
  EndMacro
  Macro CharSize()
    (1)
  EndMacro
  Macro ToBytes(Chars)
    (Chars)
  EndMacro
  Macro ToChars(Bytes)
    (Bytes)
  EndMacro
CompilerEndIf



;-

; Declares - Private

Declare.i xParseJSONString(*JN.JSONNODE, *Start.CHARACTER, *End.CHARACTER, *Unescape.INTEGER = #Null)
Declare.i xParseJSONNode(*JN.JSONNODE, *Address, Length.i)



; Procedures - Private

Procedure.s xEscapeJSONString(String.s)
  Protected Build.s = ""
  Protected *C.CHARACTER = @String
  While (*C\c)
    Select (*C\c)
      Case '"'
        Build + "\" + #DQUOTE$
      Case '\'
        Build + "\\"
      ;Case '/'
      ;  Build + "\/"
      Case #BS
        Build + "\b"
      Case #FF
        Build + "\f"
      Case #LF
        Build + "\n"
      Case #CR
        Build + "\r"
      Case #TAB
        Build + "\t"
      Default
        If ((*C\c >= $20) And (*C\c <= $FF))
          Build + Chr(*C\c)
        Else
          Build + "\u" + RSet(Hex(*C\c), 4, "0")
        EndIf
    EndSelect
    *C + CharSize()
  Wend
  ProcedureReturn (Build)
EndProcedure

Procedure.s xUnescapeJSONString(String.s)
  Protected Result.s = ""
  String = #DQUOTE$ + String + #DQUOTE$
  Protected i.INTEGER
  xParseJSONString(#Null, @String, @String + StringByteLength(String), @i)
  If (i\i)
    Result = PeekS(i\i)
    FreeMemory(i\i)
  EndIf
  ProcedureReturn (Result)
EndProcedure

Procedure.i xAddJSONNode(*JN.JSONNODE, *Parent.JSONNODE, *Previous.JSONNODE)
  If (*JN And *Parent)
    *JN\Parent = *Parent
    *Parent\Children + 1
    If (*Parent\First)
      If (*Previous = #JSON_First)
        *JN\Prev = #Null
        *JN\Next = *Parent\First
        *Parent\First\Prev = *JN
        *Parent\First = *JN
      ElseIf (*Previous = #JSON_Last)
        *JN\Prev = *Parent\Last
        *JN\Next = #Null
        *Parent\Last\Next = *JN
        *Parent\Last = *JN
      Else
        If (*Previous\Parent = *Parent)
          *JN\Prev = *Previous
          *JN\Next = *Previous\Next
          *Previous\Next = *JN
          If (*JN\Next)
            *JN\Next\Prev = *JN
          Else
            *Parent\Last = *JN
          EndIf
        Else
          ProcedureReturn (#False)
        EndIf
      EndIf
    Else
      *JN\Prev = #Null
      *JN\Next = #Null
      *Parent\First = *JN
      *Parent\Last  = *JN
    EndIf
    ProcedureReturn (#True)
  EndIf
  ProcedureReturn (#False)
EndProcedure

Procedure.i xCreateJSONNode(*Parent.JSONNODE, Type.i, *Previous.JSONNODE = #JSON_Last)
  Protected *JN.JSONNODE = #Null
  If ((Type >= 0) And (Type < #xJSON_Types))
    *JN = AllocateMemory(SizeOf(JSONNODE))
    If (*JN)
      If (*Parent)
        If (xAddJSONNode(*JN, *Parent, *Previous))
          *JN\Type = Type
        Else
          FreeMemory(*JN)
          *JN = #Null
        EndIf
      Else
        *JN\Type   = #JSON_Array
        *JN\Flags  = #xJSON_IsRoot
      EndIf
    EndIf
  EndIf
  ProcedureReturn (*JN)
EndProcedure

Procedure.i xParseJSONString(*JN.JSONNODE, *Start.CHARACTER, *End.CHARACTER, *Unescape.INTEGER = #Null)
  Protected Type.i = #JSON_Invalid
  If ((*JN Or *Unescape) And *Start And (*End > *Start))
    Type.i = #JSON_String
    Protected *C.CHARACTER = *Start
    Protected *TokenEnd.CHARACTER = #Null
    Protected i.i
    Protected *Build = #Null
    Protected *BC.CHARACTER = #Null
    If (*Unescape)
      *Unescape\i = #Null
    EndIf
    *Build = AllocateMemory((*End - *Start) + CharSize(), #PB_Memory_NoClear)
    If (*Build)
      *BC = *Build
      Protected Expecting.i  = 0
      While (*C < *End)
        Select (Expecting)
          ; 0 = quote, 1 = characters, 2 = whitespace
          Case 0
            Select (*C\c)
              Case '"'
                Expecting = 1
              Default
                Type = #JSON_Invalid
            EndSelect
          Case 1
            Select (*C\c)
              Case '"'
                *TokenEnd = *C
                Expecting = 2
              Case '\'
                If (ToChars(*End - *C) >= 1 + 1)
                  *C + CharSize()
                  Select (*C\c)
                    Case '"', '\', '/'
                      *BC\c = *C\c
                    Case 'b'
                      *BC\c = #BS
                    Case 'f'
                      *BC\c = #FF
                    Case 'n'
                      *BC\c = #LF
                    Case 'r'
                      *BC\c = #CR
                    Case 't'
                      *BC\c = #TAB
                    Case 'u'
                      If (ToChars(*End - *C) >= 4 + 1)
                        Protected UValue.u = Val("$" + PeekS(*C + CharSize(), 4))
                        For i = 1 To 4
                          *C + CharSize()
                          Select (*C\c)
                            Case '0' To '9', 'a' To 'f', 'A' To 'F'
                              ;
                            Default
                              Type = #JSON_Invalid
                              Break
                          EndSelect
                        Next i
                        If (Type <> #JSON_Invalid)
                          CompilerIf (Unicode())
                            *BC\c = UValue
                          CompilerElse
                            If (UValue <= $FF)
                              *BC\c = UValue
                            Else
                              *BC\c = '?'
                            EndIf
                          CompilerEndIf
                        EndIf
                      Else
                        Type = #JSON_Invalid
                      EndIf
                    Default
                      Type = #JSON_Invalid
                  EndSelect
                  *BC + CharSize()
                Else
                  Type = #JSON_Invalid
                EndIf
              Case $00 To $1F
                Type = #JSON_Invalid
              Default
                *BC\c = *C\c
                *BC + CharSize()
            EndSelect
          Case 2
            Select (*C\c)
              Case ' ', #TAB, #CR, #LF
                ;
              Default
                Type = #JSON_Invalid
            EndSelect
        EndSelect
        If (Type = #JSON_Invalid)
          Break
        EndIf
        *C + CharSize()
      Wend
      If (*TokenEnd = #Null)
        *TokenEnd = *End
      EndIf
      *BC\c = #NUL
      If (Type <> #JSON_Invalid)
        If (Expecting = 2)
          If (*JN)
            *JN\Type   = Type
            *JN\Raw    = PeekS(*Start + CharSize(), ToChars(*TokenEnd - *Start) - 1)
            *JN\String = PeekS(*Build)
            *JN\Number = 0.0
          EndIf
        Else
          Type = #JSON_Invalid
        EndIf
      EndIf
      If (*Unescape And (Type = #JSON_String))
        *Unescape\i = *Build
      Else
        FreeMemory(*Build)
      EndIf
    EndIf
  EndIf
  ProcedureReturn (Type)
EndProcedure

Procedure.i xParseJSONNumber(*JN.JSONNODE, *Start.CHARACTER, *End.CHARACTER)
  Protected Type.i = #JSON_Invalid
  If (*Start And (*End > *Start))
    Type.i = #JSON_Number
    Protected *C.CHARACTER = *Start
    Protected *TokenEnd.CHARACTER = #Null
    Protected IntDigits.i  =  0
    Protected FracDigits.i = -1
    Protected ExpDigits.i  = -1
    Protected Expecting.i  =  0
    While (*C < *End)
      Select (Expecting)
        ; 0 = sign, 1 = int, 2 = frac, 3 = expsign, 4 = exp, 5 = whitespace
        Case 0
          Select (*C\c)
            Case '-'
              Expecting = 1
            Case '0' To '9'
              IntDigits + 1
              Expecting = 1
            Default
              Type = #JSON_Invalid
          EndSelect
        Case 1
          Select (*C\c)
            Case '0' To '9'
              IntDigits + 1
            Case '.'
              FracDigits = 0
              Expecting  = 2
            Case 'e', 'E'
              ExpDigits = 0
              Expecting = 3
            Case ' ', #TAB, #CR, #LF
              *TokenEnd = *C
              Expecting =  5
            Default
              Type = #JSON_Invalid
          EndSelect
        Case 2
          Select (*C\c)
            Case '0' To '9'
              FracDigits + 1
            Case 'e', 'E'
              ExpDigits = 0
              Expecting = 3
            Case ' ', #TAB, #CR, #LF
              *TokenEnd = *C
              Expecting = 5
            Default
              Type = #JSON_Invalid
          EndSelect
        Case 3
          Select (*C\c)
            Case '0' To '9'
              ExpDigits + 1
              Expecting = 4
            Case '+', '-'
              Expecting = 4
            Default
              Type = #JSON_Invalid
          EndSelect
        Case 4
          Select (*C\c)
            Case '0' To '9'
              ExpDigits + 1
            Case ' ', #TAB, #CR, #LF
              *TokenEnd = *C
              Expecting = 5
            Default
              Type = #JSON_Invalid
          EndSelect
        Case 5
          Select (*C\c)
            Case ' ', #TAB, #CR, #LF
              ;
            Default
              Type = #JSON_Invalid
          EndSelect
      EndSelect
      If (Type = #JSON_Invalid)
        Break
      EndIf
      *C + CharSize()
    Wend
    If (*TokenEnd = #Null)
      *TokenEnd = *End
    EndIf
    If (Type <> #JSON_Invalid)
      If ((IntDigits > 0) And (FracDigits <> 0) And (ExpDigits <> 0))
        If (*JN)
          *JN\Type   =  Type
          *JN\Raw    =  PeekS(*Start, ToChars(*TokenEnd - *Start))
          *JN\String = *JN\Raw
          *JN\Number =  ValF(*JN\String)
        EndIf
      Else
        Type = #JSON_Invalid
      EndIf
    EndIf
  EndIf
  ProcedureReturn (Type)
EndProcedure

Procedure.i xParseJSONWord(*JN.JSONNODE, *Start.CHARACTER, *End.CHARACTER)
  Protected Type.i = #JSON_Invalid
  If (*JN And *Start And (*End > *Start))
    Type.i = #JSON_Null
    Protected *C.CHARACTER = *Start
    Protected *TokenEnd.CHARACTER = #Null
    Protected Expecting.i  =  0
    While (*C < *End)
      Select (Expecting)
        ; 0 = characters, 1 = whitespace
        Case 0
          Select (*C\c)
            Case 'a' To 'z', 'A' To 'Z', '0' To '9', '_'
              ;
            Case ' ', #TAB, #CR, #LF
              *TokenEnd = *C
              Expecting = 1
            Default
              Type = #JSON_Invalid
          EndSelect
        Case 1
          Select (*C\c)
            Case ' ', #TAB, #CR, #LF
              ;
            Default
              Type = #JSON_Invalid
          EndSelect
      EndSelect
      If (Type = #JSON_Invalid)
        Break
      EndIf
      *C + CharSize()
    Wend
    If (*TokenEnd = #Null)
      *TokenEnd = *End
    EndIf
    If (Type <> #JSON_Invalid)
      Protected Raw.s = PeekS(*Start, ToChars(*TokenEnd - *Start))
      Select (Raw)
        Case "true"
          Type       = #JSON_Boolean
          *JN\Number =  1.0
        Case "false"
          Type       = #JSON_Boolean
          *JN\Number =  0.0
        Case "null"
          Type       = #JSON_Null
          *JN\Number =  0.0
        Default
          Type = #JSON_Invalid
      EndSelect
      If (Type <> #JSON_Invalid)
        *JN\Type   = Type
        *JN\Raw    = Raw
        *JN\String = Raw
      EndIf
    EndIf
  EndIf
  ProcedureReturn (Type)
EndProcedure

Procedure.i xSkipJSONWhitespace(*Start.CHARACTER, *End.CHARACTER)
  If (*Start And (*End >= *Start))
    While (*Start < *End)
      Select (*Start\c)
        Case ' ', #TAB, #CR, #LF
          ;
        Default
          Break
      EndSelect
      *Start + CharSize()
    Wend
  Else
    *Start = #Null
  EndIf
  ProcedureReturn (*Start)
EndProcedure

Procedure.i xIsJSONWhitespace(*Start.CHARACTER, *End.CHARACTER)
  If (*Start And (*End >= *Start))
    ProcedureReturn (Bool(xSkipJSONWhitespace(*Start, *End) = *End))
  EndIf
  ProcedureReturn (#False)
EndProcedure

Procedure.i xFindJSONClose(*Start.CHARACTER, *End.CHARACTER, Symbol.c, AllowAfter.i)
  Protected *Found.CHARACTER = #Null
  
  If (*Start And (*End > *Start))
    Protected *C.CHARACTER = *Start
    Protected Sublevel.i  = 0
    Protected Expecting.i = 0
    While (*C < *End)
      Select (Expecting)
        ; 0 = data, 1 = inString, 2 = whitespace
        Case 0
          If ((*C\c = Symbol) And (Sublevel = 0))
            *Found = *C
            If (AllowAfter)
              Break
            Else
              Expecting = 2
            EndIf
          Else
            Select (*C\c)
              Case '{', '['
                Sublevel + 1
              Case '}', ']'
                Sublevel - 1
                If (Sublevel < 0)
                  Break
                EndIf
              Case '"'
                Expecting = 1
              Default
                ;
            EndSelect
          EndIf
        Case 1
          Select (*C\c)
            Case '"'
              Expecting = 0
            Case $00 To $1F
              Break
            Case '\'
              If (ToChars(*End - *C) >= 1 + 1)
                *C + CharSize()
                Select (*C\c)
                  Case '"', '\', '/', 'b', 'f', 'r', 'n', 't'
                    ;
                  Case 'u'
                    If (ToChars(*End - *C) >= 4 + 1)
                      *C + 4 * CharSize()
                    Else
                      Break
                    EndIf
                  Default
                    Break
                EndSelect
              Else
                Break
              EndIf
            Default
              ;
          EndSelect
        Case 2
          Select (*C\c)
            Case ' ', #TAB, #CR, #LF
              ;
            Default
              *Found = #Null
              Break
          EndSelect
      EndSelect
      *C + CharSize()
    Wend
  EndIf
  
  ProcedureReturn (*Found)
EndProcedure

Procedure.i xParseJSONObject(*JN.JSONNODE, *Start.CHARACTER, *End.CHARACTER)
  Protected Type.i = #JSON_Invalid
  If (*JN And *Start And (*End > *Start))
    If (*Start\c = '{')
      Protected *NodeEnd.CHARACTER = xFindJSONClose(*Start + CharSize(), *End, '}', #False)
      If (*NodeEnd)
        Type = #JSON_Object
        *Start + CharSize()
        Protected *PairEnd.CHARACTER
        Repeat
          *Start = xSkipJSONWhitespace(*Start, *NodeEnd)
          *PairEnd = xFindJSONClose(*Start, *NodeEnd, ',', #True)
          If (Not *PairEnd)
            If (Not xIsJSONWhitespace(*Start, *NodeEnd))
              *PairEnd = *NodeEnd
            EndIf
          EndIf
          If (*PairEnd)
            Type = #JSON_Invalid
            Protected *Divider.CHARACTER = xFindJSONClose(*Start, *PairEnd - CharSize(), ':', #True)
            If ((*Divider > *Start) And (*Divider < *PairEnd))
              Protected NamePtr.INTEGER
              xParseJSONString(#Null, *Start, *Divider, @NamePtr)
              If (NamePtr\i)
                Protected *Child.JSONNODE = xCreateJSONNode(*JN, #JSON_Invalid)
                If (*Child)
                  *Divider + CharSize()
                  If (xParseJSONNode(*Child, *Divider, *PairEnd - *Divider) <> #JSON_Invalid)
                    *Child\Key = PeekS(NamePtr\i)
                    Type = #JSON_Object
                  Else
                    DeleteJSONNode(*Child)
                  EndIf
                EndIf
                FreeMemory(NamePtr\i)
              EndIf
            EndIf
            *Start = *PairEnd + CharSize()
          EndIf
        Until ((Not *PairEnd) Or (*Start >= *NodeEnd) Or (Type = #JSON_Invalid))
        If (Type = #JSON_Object)
          *JN\Type   =  Type
          *JN\Raw    = ""
          *JN\String = ""
          *JN\Number =  0.0
        EndIf
      EndIf
    EndIf
  EndIf
  ProcedureReturn (Type)
EndProcedure

Procedure.i xParseJSONArray(*JN.JSONNODE, *Start.CHARACTER, *End.CHARACTER)
  Protected Type.i = #JSON_Invalid
  If (*JN And *Start And (*End > *Start))
    If (*Start\c = '[')
      Protected *NodeEnd.CHARACTER = xFindJSONClose(*Start + CharSize(), *End, ']', #False)
      If (*NodeEnd)
        Type = #JSON_Array
        *Start + CharSize()
        Protected *ItemEnd.CHARACTER
        Repeat
          *Start = xSkipJSONWhitespace(*Start, *NodeEnd)
          *ItemEnd = xFindJSONClose(*Start, *NodeEnd, ',', #True)
          If (Not *ItemEnd)
            If (Not xIsJSONWhitespace(*Start, *NodeEnd))
              *ItemEnd = *NodeEnd
            EndIf
          EndIf
          If (*ItemEnd)
            Type = #JSON_Invalid
            Protected *Child.JSONNODE = xCreateJSONNode(*JN, #JSON_Invalid)
            If (*Child)
              If (xParseJSONNode(*Child, *Start, *ItemEnd - *Start) <> #JSON_Invalid)
                Type = #JSON_Array
              Else
                DeleteJSONNode(*Child)
              EndIf
            EndIf
            *Start = *ItemEnd + CharSize()
          EndIf
        Until ((Not *ItemEnd) Or (*Start >= *NodeEnd) Or (Type = #JSON_Invalid))
        If (Type = #JSON_Array)
          *JN\Type   =  Type
          *JN\Raw    = ""
          *JN\String = ""
          *JN\Number =  0.0
        EndIf
      EndIf
    EndIf
  EndIf
  ProcedureReturn (Type)
EndProcedure

Procedure.i xParseJSONNode(*JN.JSONNODE, *Address, Length.i)
  Protected Type.i = #JSON_Invalid
  
  If (*JN And (*JN\Type = #JSON_Invalid))
    If (*Address And (Length > 0))
      Protected *End.CHARACTER = *Address + Length
      Protected *C.CHARACTER   =  xSkipJSONWhitespace(*Address, *End)
      If (*C < *End)
        Select (*C\c)
          Case '{'
            Type = xParseJSONObject(*JN, *C, *End)
          Case '['
            Type = xParseJSONArray(*JN, *C, *End)
          Case '"'
            Type = xParseJSONString(*JN, *C, *End)
          Case '0' To '9', '-'
            Type = xParseJSONNumber(*JN, *C, *End)
          Case 'a' To 'z', 'A' To 'Z', '_'
            Type = xParseJSONWord(*JN, *C, *End)
          Default
            ;
        EndSelect
      EndIf
    EndIf
  EndIf
  
  ProcedureReturn (Type)
EndProcedure

Procedure xWriteJSONIndent(FN, Indent.i, Level.i)
  If (IsFile(FN))
    If (Indent > 0)
      WriteString(FN, Space(Indent * Level), #PB_Ascii)
    ElseIf (Indent < 0)
      WriteString(FN, RSet("", -Indent * Level, #TAB$), #PB_Ascii)
    EndIf
  EndIf
EndProcedure

Procedure xWriteJSONNode(FN.i, *JN.JSONNODE, Format.i, Indent.i, Level.i = 0)
  If (IsFile(FN) And *JN)
    Protected EOL.s
    Protected SP.s
    If (Format & #JSON_Indented)
      CompilerIf (#PB_Compiler_OS = #PB_OS_Windows)
        EOL = #CRLF$
      CompilerElse
        EOL = #LF$
      CompilerEndIf
      SP  = " "
    Else
      EOL = ""
      SP  = ""
    EndIf
    ;
    xWriteJSONIndent(FN, Indent, Level)
    If (*JN\Key)
      WriteString(FN, #DQUOTE$ + xEscapeJSONString(*JN\Key) + #DQUOTE$ + ":" + SP, #PB_Ascii)
    EndIf
    Protected *Child.JSONNODE
    Select (*JN\Type)
      Case #JSON_Null
        WriteString(FN, "null", #PB_Ascii)
      Case #JSON_Boolean
        If (*JN\Number)
          WriteString(FN, "true", #PB_Ascii)
        Else
          WriteString(FN, "false", #PB_Ascii)
        EndIf
      Case #JSON_Number
        WriteString(FN, *JN\Raw, #PB_Ascii)
      Case #JSON_String
        WriteString(FN, #DQUOTE$ + *JN\Raw + #DQUOTE$, #PB_Ascii)
      Case #JSON_Object
        WriteString(FN, "{" + EOL, #PB_Ascii)
        *Child = *JN\First
        If (*Child)
          While (#True)
            xWriteJSONNode(FN, *Child, Format, Indent, Level + 1)
            *Child = *Child\Next
            If (*Child)
              WriteString(FN, "," + EOL, #PB_Ascii)
            Else
              WriteString(FN, EOL, #PB_Ascii)
              Break
            EndIf
          Wend
        EndIf
        xWriteJSONIndent(FN, Indent, Level)
        WriteString(FN, "}", #PB_Ascii)
      Case #JSON_Array
        WriteString(FN, "[" + EOL, #PB_Ascii)
        *Child = *JN\First
        If (*Child)
          While (#True)
            xWriteJSONNode(FN, *Child, Format, Indent, Level + 1)
            *Child = *Child\Next
            If (*Child)
              WriteString(FN, "," + EOL, #PB_Ascii)
            Else
              WriteString(FN, EOL, #PB_Ascii)
              Break
            EndIf
          Wend
        EndIf
        xWriteJSONIndent(FN, Indent, Level)
        WriteString(FN, "]", #PB_Ascii)
    EndSelect
  EndIf
EndProcedure

;-

Procedure.i DeleteJSONNode(*JN.JSONNODE)
  If (*JN And (Not (*JN\Flags & #xJSON_IsRoot)))
    If ((*JN\Type = #JSON_Array) Or (*JN\Type = #JSON_Object))
      Protected *Prev.JSONNODE
      Protected *Next.JSONNODE
      Protected *Child.JSONNODE = *JN\First
      While (*Child)
        *Next = *Child\Next
        DeleteJSONNode(*Child)
        *Child = *Next
      Wend
    EndIf
    ;
    If (*JN\Parent)
      *Prev = *JN\Prev
      *Next = *JN\Next
      If (*Prev)
        *Prev\Next = *Next
      EndIf
      If (*Next)
        *Next\Prev = *Prev
      EndIf
      If (*JN = *JN\Parent\First)
        *JN\Parent\First = *Next
      EndIf
      If (*JN = *JN\Parent\Last)
        *JN\Parent\Last = *Prev
      EndIf
      *JN\Parent\Children - 1
    EndIf
    FreeMemory(*JN)
  EndIf
  ProcedureReturn (#Null)
EndProcedure

Procedure.i CreateJSONNode(*Parent.JSONNODE, Type.i = #JSON_Object, Key.s = "", *Previous.JSONNODE = #JSON_Last)
  Protected *Child.JSONNODE = #Null
  If ((Type >= 0) And (Type < #xJSON_Types) And (Type <> #JSON_Invalid))
    If (*Parent)
      If ((*Parent\Type = #JSON_Array) Or ((*Parent\Type = #JSON_Object) And (Key)))
        If ((*Parent\Children = 0) Or (Not (*Parent\Flags & #xJSON_IsRoot)))
          *Child = xCreateJSONNode(*Parent, Type, *Previous)
          If (*Child)
            If (*Parent\Type = #JSON_Object)
              *Child\Key = Key
            EndIf
            Select (Type)
              Case #JSON_Boolean
                *Child\String = "false"
                *Child\Raw    = *Child\String
              Case #JSON_Null
                *Child\String = "null"
                *Child\Raw    = *Child\String
              Case #JSON_Number
                *Child\String = "0"
                *Child\Raw    = *Child\String
            EndSelect
          EndIf
        EndIf
      EndIf
    EndIf
  EndIf
  ProcedureReturn (*Child)
EndProcedure

Procedure.i AddJSONString(*Parent.JSONNODE, Text.s, Key.s = "", *Previous.JSONNODE = #JSON_Last)
  Protected *New.JSONNODE = #Null
  *New = CreateJSONNode(*Parent, #JSON_String, Key, *Previous)
  If (*New)
    SetJSONNodeText(*New, Text)
  EndIf
  ProcedureReturn (*New)
EndProcedure

Procedure.i AddJSONNumber(*Parent.JSONNODE, Value.f, Key.s = "", *Previous.JSONNODE = #JSON_Last)
  Protected *New.JSONNODE = #Null
  *New = CreateJSONNode(*Parent, #JSON_Number, Key, *Previous)
  If (*New)
    SetJSONNodeValue(*New, Value)
  EndIf
  ProcedureReturn (*New)
EndProcedure

Procedure.i AddJSONBoolean(*Parent.JSONNODE, Value.i, Key.s = "", *Previous.JSONNODE = #JSON_Last)
  Protected *New.JSONNODE = #Null
  *New = CreateJSONNode(*Parent, #JSON_Boolean, Key, *Previous)
  If (*New)
    SetJSONNodeValue(*New, Value)
  EndIf
  ProcedureReturn (*New)
EndProcedure

Procedure.i AddJSONNull(*Parent.JSONNODE, Key.s = "", *Previous.JSONNODE = #JSON_Last)
  Protected *New.JSONNODE = #Null
  *New = CreateJSONNode(*Parent, #JSON_Null, Key, *Previous)
  ProcedureReturn (*New)
EndProcedure

Procedure.i AddJSONArray(*Parent.JSONNODE, Key.s = "", *Previous.JSONNODE = #JSON_Last)
  Protected *New.JSONNODE = #Null
  *New = CreateJSONNode(*Parent, #JSON_Array, Key, *Previous)
  ProcedureReturn (*New)
EndProcedure

Procedure.i AddJSONObject(*Parent.JSONNODE, Key.s = "", *Previous.JSONNODE = #JSON_Last)
  Protected *New.JSONNODE = #Null
  *New = CreateJSONNode(*Parent, #JSON_Object, Key, *Previous)
  ProcedureReturn (*New)
EndProcedure

;-

Procedure.i FreeJSON(*J.JSONNODE)
  If (*J And (*J\Flags & #xJSON_IsRoot))
    *J\Flags & (~#xJSON_IsRoot)
    DeleteJSONNode(*J)
  EndIf
  ProcedureReturn (#Null)
EndProcedure

Procedure.i SaveJSON(*J.JSONNODE, File.s, Format.i = #JSON_Default, IndentSize.i = 4)
  Protected Result.i = #False
  If (Format = #JSON_Default)
    Format = #JSON_Indented
  EndIf
  If (*J And File)
    Protected FN.i = CreateFile(#PB_Any, File)
    If (FN)
      If (Not (Format & #JSON_Indented))
        IndentSize = 0
      EndIf
      If (*J\Flags & #xJSON_IsRoot)
        If (*J\First)
          xWriteJSONNode(FN, *J\First, Format, IndentSize, 0)
          Result = #True
        EndIf
      Else
        xWriteJSONNode(FN, *J, Format, IndentSize, 0)
        Result = #True
      EndIf
      CloseFile(FN)
      If (Not Result)
        DeleteFile(File)
      EndIf
    EndIf
  EndIf
  ProcedureReturn (Result)
EndProcedure

Procedure.i CatchJSON(*Address, Length.i)
  Protected *J.JSONNODE = #Null
  
  If (*Address And (Length > 0))
    *J = xCreateJSONNode(#Null, #JSON_Array)
    If (*J)
      Protected *Main.JSONNODE = xCreateJSONNode(*J, #JSON_Invalid)
      If (*Main)
        ;
        CompilerIf (Unicode())
          ; Convert apparent ASCII buffer to Unicode buffer
          If ((Length % 2 <> 0) Or (PeekA(*Address + 1) <> #NUL))
            Protected UnicodeCopy.s = PeekS(*Address, Length, #PB_Ascii)
            *Address = @UnicodeCopy
            Length * 2
          EndIf
        CompilerEndIf
        ;
        If (xParseJSONNode(*Main, *Address, Length) <> #JSON_Invalid)
          ; OK
        Else
          *J = FreeJSON(*J)
        EndIf
      Else
        *J = FreeJSON(*J)
      EndIf
    EndIf
  EndIf
  
  ProcedureReturn (*J)
EndProcedure

Procedure.i LoadJSON(File.s)
  Protected *J.JSONNODE = #Null
  Protected FN.i = ReadFile(#PB_Any, File)
  If (FN)
    Protected Length.i = Lof(FN)
    If (Length > 0)
      Protected Buffer.s = ReadString(FN, #PB_Ascii|#PB_File_IgnoreEOL)
      If (Buffer)
        *J = CatchJSON(@Buffer, StringByteLength(Buffer))
      EndIf
    EndIf
    CloseFile(FN)
  EndIf
  ProcedureReturn (*J)
EndProcedure

Procedure.i CreateJSON(MainNodeType.i = #JSON_Object)
  Protected *J.JSONNODE = #Null
  If ((MainNodeType >= 0) And (MainNodeType < #xJSON_Types))
    If (MainNodeType <> #JSON_Invalid)
      *J = xCreateJSONNode(#Null, #JSON_Array)
      If (*J)
        Protected *Main.JSONNODE = xCreateJSONNode(*J, MainNodeType)
        If (*Main)
          ; OK
        Else
          *J = DeleteJSONNode(*J)
        EndIf
      EndIf
    EndIf
  EndIf
  ProcedureReturn (*J)
EndProcedure

;-

Procedure.i NextJSONNode(*JN.JSONNODE)
  If (*JN And *JN\Parent)
    ProcedureReturn (*JN\Next)
  EndIf
  ProcedureReturn (#Null)
EndProcedure

Procedure.i PreviousJSONNode(*JN.JSONNODE)
  If (*JN And *JN\Parent)
    ProcedureReturn (*JN\Prev)
  EndIf
  ProcedureReturn (#Null)
EndProcedure

Procedure.i MainJSONNode(*J.JSONNODE)
  If (*J)
    If (*J\Flags & #xJSON_IsRoot)
      ProcedureReturn (*J\First)
    ElseIf (*J\Parent)
      ProcedureReturn (MainJSONNode(*J\Parent))
    EndIf
  EndIf
  ProcedureReturn (#Null)
EndProcedure

Procedure.i ChildJSONNode(*JN.JSONNODE, Index.i = #JSON_First)
  Protected *Child.JSONNODE = #Null
  If (*JN)
    Select (*JN\Type)
      Case #JSON_Array, #JSON_Object
        Protected n.i = *JN\Children
        If (Index = #JSON_Last)
          Index = n - 1
        EndIf
        If ((Index >= 0) And (Index < n))
          *Child = *JN\First
          While (Index > 0)
            *Child = *Child\Next
            Index - 1
          Wend
        EndIf
      Default
        ;
    EndSelect
  EndIf
  ProcedureReturn (*Child)
EndProcedure

Procedure.i ParentJSONNode(*JN.JSONNODE)
  If (*JN And *JN\Parent And (Not (*JN\Parent\Flags & #xJSON_IsRoot)))
    ProcedureReturn (*JN\Parent)
  EndIf
  ProcedureReturn (#Null)
EndProcedure

Procedure.i FindJSONNodeByID(*JN.JSONNODE, Text.s, Key.s = "id")
  Protected *Found.JSONNODE = #Null
  If (*JN And Text And Key)
    Protected *Child.JSONNODE = *JN\First
    While (*Child)
      Select (*Child\Type)
        Case #JSON_Array, #JSON_Object
          *Found = FindJSONNodeByID(*Child, Text, Key)
        Case #JSON_String
          If ((*Child\Key = Key) And (*Child\String = Text))
            *Found = *JN
          EndIf
      EndSelect
      If (*Found)
        Break
      EndIf
      *Child = *Child\Next
    Wend
  EndIf
  ProcedureReturn (*Found)
EndProcedure

Procedure.i NamedJSONNode(*JN.JSONNODE, Path.s)
  Protected *Found.JSONNODE = #Null
  If (*JN And Path)
    If (*JN\Flags & #xJSON_IsRoot)
      ProcedureReturn (NamedJSONNode(*JN\First, Path))
    ElseIf (*JN\Type = #JSON_Object)
      Protected Name.s
      Protected Paren.i = FindString(Path, "/")
      If (Paren)
        Name = Left(Path, Paren - 1)
      Else
        Name = Path
      EndIf
      If (Name)
        *Found = *JN\First
        While (*Found)
          If (Name = *Found\Key)
            Break
          EndIf
          *Found = *Found\Next
        Wend
        If (*Found And Paren)
          Name = Mid(Path, Paren + 1)
          *Found = NamedJSONNode(*Found, Name)
        EndIf
      EndIf
    EndIf
  EndIf
  ProcedureReturn (*Found)
EndProcedure

;-

Procedure.i SetJSONNodeValue(*JN.JSONNODE, Value.f)
  If (*JN)
    If (*JN\Type = #JSON_Number)
      *JN\Number =  Value
      *JN\String =  StrF(Value)
      *JN\Raw    = *JN\String
      ProcedureReturn (#True)
    ElseIf (*JN\Type = #JSON_Boolean)
      *JN\Number = Bool(Value)
      If (*JN\Number)
        *JN\String = "true"
      Else
        *JN\String = "false"
      EndIf
      *JN\Raw = *JN\String
      ProcedureReturn (#True)
    EndIf
  EndIf
  ProcedureReturn (#False)
EndProcedure

Procedure.i IsValidJSONNumber(String.s)
  ProcedureReturn (Bool(xParseJSONNumber(#Null, @String,
      @String + StringByteLength(String)) <> #JSON_Invalid))
EndProcedure

Procedure.i SetJSONNodeText(*JN.JSONNODE, Text.s)
  If (*JN)
    If (*JN\Type = #JSON_String)
      *JN\String =  Text
      *JN\Raw    =  xEscapeJSONString(Text)
      ProcedureReturn (#True)
    ElseIf (*JN\Type = #JSON_Number)
      If (IsValidJSONNumber(Text))
        *JN\String = Text
        *JN\Raw    = Text
        *JN\Number = ValF(Text)
        ProcedureReturn (#True)
      EndIf
    EndIf
  EndIf
  ProcedureReturn (#False)
EndProcedure

Procedure.i SetJSONNodeKey(*JN.JSONNODE, Key.s)
  If (*JN And *JN\Parent And (*JN\Parent\Type = #JSON_Object))
    If (*JN\Key)
      *JN\Key = Key
    EndIf
    ProcedureReturn (#True)
  EndIf
  ProcedureReturn (#False)
EndProcedure

;-

Procedure.i GetJSONChildCount(*JN.JSONNODE)
  If (*JN)
    Select (*JN\Type)
      Case #JSON_Array, #JSON_Object
        ProcedureReturn (*JN\Children)
    EndSelect
  EndIf
  ProcedureReturn (0)
EndProcedure

Procedure.s GetJSONNodeKey(*JN.JSONNODE)
  If (*JN)
    ProcedureReturn (*JN\Key)
  EndIf
  ProcedureReturn ("")
EndProcedure

Procedure.s GetJSONNodeText(*JN.JSONNODE)
  If (*JN)
    ProcedureReturn (*JN\String)
  EndIf
  ProcedureReturn ("")
EndProcedure

Procedure.f GetJSONNodeValue(*JN.JSONNODE)
  If (*JN)
    ProcedureReturn (*JN\Number)
  EndIf
  ProcedureReturn (0.0)
EndProcedure

Procedure.i GetJSONNodeInteger(*JN.JSONNODE)
  If (*JN)
    ;Round(*JN\Number, #PB_Round_Nearest)
    ProcedureReturn (Int(*JN\Number))
  EndIf
  ProcedureReturn (0)
EndProcedure

Procedure.i GetJSONNodeType(*JN.JSONNODE)
  Protected Type.i = #JSON_Invalid
  If (*JN)
    Type = *JN\Type
    If ((Type < 0) Or (Type >= #xJSON_Types))
      Type = #JSON_Invalid
    EndIf
  EndIf
  ProcedureReturn (Type)
EndProcedure

Procedure.s GetJSONTypeName(Type.i)
  Protected Name.s
  Select (Type)
    Case #JSON_Array
      Name = "Array"
    Case #JSON_Boolean
      Name = "Boolean"
    Case #JSON_Null
      Name = "Null"
    Case #JSON_Number
      Name = "Number"
    Case #JSON_Object
      Name = "Object"
    Case #JSON_String
      Name = "String"
    Default
      Name = "Invalid"
  EndSelect
  ProcedureReturn (Name)
EndProcedure

CompilerIf (#PB_Compiler_Version >= 520)
EndModule
CompilerEndIf





















;-
;- ----- JSON Example -----

CompilerIf (#PB_Compiler_IsMainFile)

DisableExplicit

CompilerIf (#PB_Compiler_Version >= 520)
UseModule JSON
CompilerEndIf





;
;- Create a JSON
;
; Here we create an empty JSON from scratch.
; Every valid JSON has exactly one main node (top-level item).
; By default, it will be an Object type.
; But it can be an Array, String, Number, Boolean, or even Null!
;

*JSON = CreateJSON()
If (*JSON)
  *Main = MainJSONNode(*JSON)
  
  
  ;
  ;- Add some items
  ;
  ; You can add sub-items, but only to an Object or Array.
  ; To add anything to an Object, you must specify a Key string!
  ;
  ; Also, the six AddJSON____() procedures are really just
  ; shortcuts for the generic CreateJSONNode() procedure.
  ;
  
  *Obj = AddJSONObject(*Main, "jsonExample")
  
  
  ;
  ; Objects and Arrays can hold any number of child items.
  ; Here we add some basic types to the Object we created.
  ;
  
  AddJSONString(*Obj, FormatDate("%hh:%ii:%ss", #PB_Compiler_Date), "time")
  AddJSONNumber(*Obj, #PB_Compiler_Version, "PB_Compiler_Version")
  AddJSONBoolean(*Obj, #PB_Compiler_Unicode, "PB_Compiler_Unicode")
  AddJSONNull(*Obj, "nullNode")
  
  
  ;
  ; Arrays are similar to Objects, but they don't contain Key strings.
  ; Here we create an Array and add some Numbers to it.
  ;
  
  *Arr = AddJSONArray(*Obj, "randomNumbers")
  For i = 1 To 5
    AddJSONNumber(*Arr, Random(1000))
  Next i
  
  
  ;
  ; Most procedures return #Null (0) if an argument is invalid or it fails.
  ;
  ; Examples:
  ; 1. The main JSON node does not have a parent node.
  ; 2. Array indexes are 0-based, so *Arr does not have an element #5.
  ; 3. You cannot add a second top-level node.
  ; 4. You cannot set the value/text of a wrong type of node.
  ; 5. You cannot an a sub-item to an Object without a Key.
  ;
  
  Debug "Should be 0:"
  Debug "1. " + ParentJSONNode(*Main)
  Debug "2. " + ChildJSONNode(*Arr, 5)
  Debug "3. " + AddJSONString(*JSON, "top-level")
  Debug "4. " + SetJSONNodeText(*Obj, "objectText")
  Debug "5. " + AddJSONNumber(*Obj, 999)
  Debug ""
  
  
  ;
  ;- Save a JSON
  ;
  ; Here we save the JSON to an ASCII text file.
  ; You can specify Indented (default) or Compact format.
  ; The indentation step size can be specified as
  ; spaces (positive number) or tab characters (negative number).
  ;
  
  OutFile.s = GetTemporaryDirectory() + "example.json.txt"
  If (SaveJSON(*JSON, OutFile))
    RunProgram(OutFile)
    
    
    ;
    ;- Reload it
    ;
    ; Here we free the entire JSON, and reload it from the saved file.
    ;
    
    FreeJSON(*JSON)
    *JSON = LoadJSON(OutFile)
    If (*JSON)
      *Main = MainJSONNode(*JSON)
      
      
      ;
      ;- Find items
      ;
      ; You can find sub-items by their numeric index,
      ; or by their Key if they belong to a parent Object.
      ;
      
      *Obj    = ChildJSONNode(*Main)
      *Second = ChildJSONNode(*Obj, 1)
      *Third  = NextJSONNode(*Second)
      *Arr    = NamedJSONNode(*Obj, "randomNumbers")
      
      
      ;
      ;- Delete items
      ;
      ; Deleting items is simple...
      
      DeleteJSONNode(*Third)
      DeleteJSONNode(*Arr)
      
      
      ;
      ;- Modify items
      ;
      ; You can change the Value of a Boolean or Number node.
      ; You can change the Text of a String or Number node.
      ; You can change the Key of any child of an Object.
      ; Strings are escaped automatically (and unescaped when accessed).
      ;
      
      *Time = NamedJSONNode(*JSON, "jsonExample/time")
      SetJSONNodeKey(*Time, "newTime")
      SetJSONNodeText(*Time, FormatDate("%hh:%ii:%ss", Date() + 90))
      SetJSONNodeValue(NamedJSONNode(*Obj, "PB_Compiler_Version"), 567)
      
      UnsafeText.s = "He said " + #DQUOTE$ + "Hello!" + #DQUOTE$ + #LF$ + "The End."
      *Quote = AddJSONString(*Obj, UnsafeText, "quote")
      Debug GetJSONNodeText(*Quote)
      Debug ""
      
      
      ;
      ; Save two more versions of the JSON for comparison.
      ; The first uses tabs instead of spaces for indentation.
      ; The second has no formatting whitespace at all.
      ;
      
      OutFile = GetTemporaryDirectory() + "modified.json.txt"
      If (SaveJSON(*JSON, OutFile, #JSON_Indented, -1))
        RunProgram(OutFile)
      EndIf
      
      OutFile = GetTemporaryDirectory() + "compact.json.txt"
      If (SaveJSON(*JSON, OutFile, #JSON_Compact))
        RunProgram(OutFile)
      EndIf
      
      
      FreeJSON(*JSON)
    Else
      Debug "Could not reload JSON from file!"
    EndIf
    
  Else
    FreeJSON(*JSON)
    Debug "Could not save JSON!"
  EndIf
  
Else
  Debug "JSON could not be created!"
EndIf





CompilerEndIf

;- ------------------------------

;-

Re: JSON Module + example

Posted: Fri May 16, 2014 10:06 pm
by pwd
Thanks a lot for sharing this!

Re: JSON Module + example

Posted: Sat May 17, 2014 7:24 am
by davido
@kenmo, Excellent!

Thank you for sharing. :D

Re: JSON Module + example

Posted: Wed May 21, 2014 12:24 am
by said
Thanks kenmo for sharing, very nice and nicely written :D

Re: JSON Module + example

Posted: Thu May 22, 2014 4:36 pm
by marroh
Thanks, good work.