Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

lib_json [AHK_L] Json<--->Object


  • Please log in to reply
50 replies to this topic
lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010
lib_JSON

lib_JSON is a lightweight Autohotkey_L scripting library.
It allows manipulation of JSON data, import and export to AHK_L objects.

Function $() allows easy JQuery-like access to a global object $$.

There is also a little sample code below. Just pay attention that the
escapechar and the comment flag are NOT reset to the default ; and `
so you have to do it after the #include directive

Freely inspired by <!-- e --><a href="mailto:titan@autohotkey.net">titan@autohotkey.net</a><!-- e -->
Additional credits to "awesomeo" and "fincs" at autohotkey.net forum for bugfixes and features

Released under the Simplified BSD License

Updates
14.02.2011, Now accepts true|false|null values. Static initializer for stdlib

10.02.2011, Fixed string recognition, also works with single quotes now

28.01.2011, Completely rewritten with shift-reduce tecnique

22.12.2010, Bugfix, partial rewrite
Now correctly handles objects in arrays
(ex. { "a": ["b":1, "c":{ "d":"__"}]} )

20.12.2010, Bugfix
Now correctly handles number entries
(ex. {"a" : 1.23 } )
Credits to awesomeo@autohotkey.net/forum for finding it and providing a fix.


Library code
#Escapechar \
#CommentFlag //


// Static initialization for stdlib, by fincs at autohotkey.com forums      //
__json_init()
{
   global
   static _ := __json_init()
   $$ := Object()
   JSON_init()
}


// Simple access to global variable $$                                      //
$(path, val = "") {

    global $$
    tempobj := $$

    last := (instr(path, ".")
        ? substr(path, 1+instr(path, ".", false, -1))
        : path)

    Loop, Parse, path, \.
    {
        if (val != "") {
            if (last = A_loopfield){
                tempObj[A_loopfield] := val
                continue
            } else if (!tempObj[A_Loopfield])
                tempObj[A_loopfield] := Object()
        } else if (!tempObj)
            break
        tempObj := tempObj[A_loopfield]
    }

    if (! tempObj)
        JSON_error("Cannot find or set entry " . path )
    else
        return (val = "" ?  tempObj : 0 )
}

//  Save JSON string to file                                              //
JSON_save(obj, filename, spacing=35, block="    ", level=1) {

    file         := FileOpen(filename, "w")
    jsonString   := JSON_to(obj, spacing, block, level) "\n"
    bytesWritten := file.write(jsonString)
    file.close()

    if (bytesWritten <= 0)
        JSON_error("Cannot write file " . filename)
    else
        return bytesWritten
}

//  Load JSON string from file                                            //
JSON_load(filename) {
    file := FileOpen(filename, "r")
    jsonString := file.read()
    file.close()
    if (jsonString == "")
        JSON_error("No file found, or blank file.")
    return JSON_from(jsonString)
}

//  Error handling                                                        //
JSON_error(s){
    Msgbox, % "[" . A_now . "] " . s
    Exit
}

//  Escape / unescape json keys and values                                //
JSON_escape(s){
    StringReplace, s, s, \\, \\\\, All
    StringReplace, s, s, ', \\',   All
    StringReplace, s, s, ", \\",   All
    return s
}
JSON_unescape(s){
    StringReplace, s, s, \\\\, \\, All
    StringReplace, s, s, \\', ',   All
    StringReplace, s, s, \\", ",   All
    return s
}

// Turns an object to a JSON string                                      //
JSON_to(obj, spacing = 50, block = "    ", level = "1" ) {

    s := ""
    for k, v in obj
    {
        // New line                        //
        if (s != "")
            s .= ","
        s .= "\n"

        // Indent key                      //
        Loop, %level%
            s .= block

        // Escape key and value            //
        k := JSON_escape(k)
        v := JSON_escape(v)

        // Write key                       //
        s .= """" k """: "

        // If object, do recursion         //
        if (isobject(v)) {
            s .= JSON_to(v, spacing, block, level + 1 )
        } else {
 
            // LeftAlign the second column      //
            totalKeyLength := level * strlen(block) + strlen(k) + 2
            if (spacing >= totalKeyLength ) {
                valueIndent := spacing - totalKeyLength
                loop, %valueIndent%
                    s .= " "
            }
            // Quote non-number values          //
            if v is not number
                v := """" v """"

            // New line                         //
            s .= v
        }
    }

    // Return                          //
    if ( (s == "") && !isobject(obj) ) {
        s := Object()
    } else if ( (s == "") && isobject(obj) ) {
        s := "{}"
    } else {
        s := "{" s "\n"
        level -= 1
        Loop, %level%
            s .= block
        s .= "}"
    }
    return s

}

//  Initialize the shift-reduce tables                                       //
JSON_init(){

    #EscapeChar `
    global JSON_regexps, JSON_rules

    //  symbol : regexp          //
    JSON_regexps := Object( ""
        . " " , "(\s+)"
        , "{" , "({)"
        , "[" , "(\[)"
        , "]" , "(\])"
        , "}" , "(})"
        , "Q" , "'([^'\\]*(\\.[^'\\]*)*)'"
        , "S" , """([^""\\]*(\\.[^""\\]*)*)"""
        , "N" , "([+\-]?\d+([.,]\d+)?)"
        , "D" , "(true|false|null)"
        , ":" , "(:)"
        , "," , "(,)" 
    . "" )

    //  1) Match "key" in the symbol stack                                   //
    //  2) Replace with "sub" in the symbol stack                            //
    //  3) Remove len("key") from the result stack                           //
    //  4) Append the result of function "func" on the result stack          //
    JSON_rules    := Object()
    JSON_rules[0] := Object( "key", "(\s+)",                             "sub", "" , "func", "JSON_reduce_spaces"   )
    JSON_rules[1] := Object( "key", "([QS]:[QSNOAD])",                   "sub", "_", "func", "JSON_reduce_keyvalue" )
    JSON_rules[2] := Object( "key", "(\[(([QSNOAD](,[QSNOAD])*\])|\]))", "sub", "A", "func", "JSON_reduce_array"    )
    JSON_rules[3] := Object( "key", "({}|{_(,_)*})",                     "sub", "O", "func", "JSON_reduce_object"   )

    #Escapechar \

}

// Reducing functions                                                       //
// Space                           //
JSON_reduce_spaces(c)  { 
    return ""
}
// Key-value pair                  //
JSON_reduce_keyvalue(c){
    return Object(c[3], c[1])
}
// Array                           //
JSON_reduce_array(c){
    ret := Object()

    new_idx := (c.maxindex() - 1) \/\/ 2
    for old_idx, token in c {
        if (mod(old_idx,2) == 0) {
            ret[new_idx] := token
            new_idx -= 1
        }
    }
    return ret
}
// Objects                         //
JSON_reduce_object(c){
    ret := Object()
    for old_idx, key_val in c {
        if (mod(old_idx,2) == 0) {
            for key, val in key_val {
                ret[key] := val
            }
        }
    }
    return ret
}



// Main parsing method                                                                      //
JSON_from(s){

    ret     := Object()
    pos     := 1
    symbols := ""
    len     := strLen(s)

    //   Loop over the tokens         //
    while (pos <= len) {

        // Shift a token                 //
        t := JSON_shift(s,pos,symbols,ret)

        // Reduce                       //
        symbols := JSON_reduce(t["symbols"],ret), pos := t["pos"]

    }

    // If succesfully reduced, return the object/array    //
    if (symbols == "O" || symbols == "A")
        return ret[""]
    else
        JSON_error("Invalid JSON string, cannot convert to object.")
}



//  Read a token and shift in symbol to the stack                                          //
JSON_shift(s, pos, symbols, ret){

    global JSON_regexps

    for symbol,regexp in JSON_regexps {

        // match 1 includes quotes, match 2 doesn't       //
        RegexMatch(s, "PSi)(" . regexp . ")", match_, pos)
        if (match_pos1 == pos){

            // Add current state to the symbol stack          //
            symbols .= symbol

            // Update position                                //
            pos  += match_len1

            // Insert the value in the value stack            //
            ret.insert( JSON_unescape(substr(s, match_pos2, match_len2)) )

            // Return the updated symbol stack and pos        // 
            return Object("symbols", symbols, "pos", pos)
        }
    }

    // If there is nothing to shift, error  //
    JSON_error("Error at pos:" pos "\n" substr(s,pos-4))
    exit
}



//  Reduces groups of symbols into others according to the rule table                       //
JSON_reduce(symbols, ret){

    global JSON_regexps, JSON_rules

    rule_idx := 0

    // Loop over rules, to check if it's possible to reduce tokens //
    while (rule_idx <= JSON_rules.maxIndex()) {

        children    := Object()
        rule        := JSON_rules[rule_idx]
        old_symbols := rule["key"]
        new_symbol  := rule["sub"]
        reduce_func := rule["func"]

        // Find something to reduce //
        Regexmatch(symbols, "PSi)" . old_symbols . "$", match_)

        // If you find nothing, continue to the next rule //
        if ( match_pos1 < 1 ) {
            rule_idx += 1
            continue
        }

        // If you find something, remove the symbols from the symbols stack  //
        // and reduce the tokens in the result stack                         //
        Loop, %match_len1% {
            if (ryle_idx != 0 )
                children.insert( ret[ret.maxindex()] )
            ret.remove()
            stringtrimright, symbols, symbols, 1
        }

        // Append the reduced symbol to the symbol stack  //
        symbols .= new_symbol
        rule_idx := 0

        // Reduce the tokens into a new one  //
        if ((new_token := %reduce_func%(children)) != "")
            ret[ret.maxindex()+1] := new_token

    }

    return symbols

}

Example
#include lib_json.ahk
#EscapeChar `
#CommentFlag ;

jsonString =
( ltrim join 
    {
        "1" : "test string",
        "2" : "more testing",
        "a" : {
            "a1" : "+3.5",
            "a2" : "-5"
        }
    }
)

j := JSON_from(jsonString)
msgbox, % ""
    . j[1] . "`n"
    . j[2] . "`n"
    . (j["a","a1"] == 3.5)
JSON_save(j, "test.json")

$$ := JSON_load("test.json")
msgbox, % "" 
    . $("1") . "`n"
    . $("2") . "`n"
    . ($("a.a1") == 3.5 )

msgbox, % JSON_to($$)


awesomeo
  • Members
  • 28 posts
  • Last active: Mar 06 2012 07:22 AM
  • Joined: 25 Feb 2009
Hi,

Was having an issue loading an exceptionally complex JSON-RPC response; I needed to make the following insertion at line:135 (of lib_json.ahk) to make it work...

//Check if it's an integer
else if (RegExMatch(result_value,"^(\d+)(,|})\r*$",iresult_value))
	jsonobj[result_key] := iresult_value1

Integers don't appear (at a brief observation) to be expected when validating the JSON message.

Example:

{
	"sessionTimeoutSeconds": 1200.0,
	"stuff": {
		"appnumber": 14
	},
}

You have done some very nice work here! Saved me a pile of time, many thanks.

lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010
Sure you are right, it was "bugged" on non-quoted numbers.
I'll bugfix after lunch if I have time with the regexp I used in the parsing part.

The curious part is that all variables are considered as strings, so if you do
JSON_from("{ 'a': '3'}")["a"] == 3

should return 1 correctly

(This doesn't mean that the parser works correctly)

lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010

I'll bugfix after lunch if I have time with the regexp I used in the parsing part.

Updated and credited on the site.

_

awesomeo
  • Members
  • 28 posts
  • Last active: Mar 06 2012 07:22 AM
  • Joined: 25 Feb 2009
(?P<value>[+\-]?\d+(?:[\.,]\d*)?)\D?
Absolute beauty! Well done!!!

Unfortunately the following still catches it out:

Test := json_from("{""id"":""FA7SC3384""}")
MsgBox % Test.ID


Your solution returns '7'.
My solution returns 'FA7SC3384'.

Purhaps one of validation bits are now a bit overzealous returning (\d+) as soon as it finds a value that contains an integer?

lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010

Unfortunately the following still catches it out:

Test := json_from("{""id"":""FA7SC3384""}")
MsgBox % Test.ID


Your solution returns '7'.
My solution returns 'FA7SC3384'.


You are right, I forgot the ^...$ part of it.

^(?P<value>[+\-]?\d+(?:[\.,]\d*)?)\D?$

This should work, at least it works on that particular test case. Update in progress... tomorrow I'll have some time to run more testing. I'm also adding some kind of micro warning/error log, stored in the $$ variable. That should make error handling easier and more appropriate.
Now I'm just going to bed :)

lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010
I'm running some test with JSON files from http://www.json.org/example.html
There are big problems with objects into arrays.

Test string:
Msgbox, % JSON_to(JSON_load("test.json"))

Input 1
{
    "glossary": {
        "title": "example glossary",
        "GlossDiv": {
             "title": "S",
             "GlossList": {
                 "GlossEntry": {
                     "ID": "SGML",
                     "SortAs": "SGML",
                     "GlossTerm": "Standard Generalized Markup Language",
                     "Acronym": "SGML",
                     "Abbrev": "ISO 8879:1986",
                     "GlossDef": {
                        "para": "A meta-markup language, used to create markup languages such as DocBook.",
                        "GlossSeeAlso": ["GML", "XML"]
                     },
                     "GlossSee": "markup"
                }
            }
        }
    }
}

Output 1, OK:
{
    "glossary": {
        "GlossDiv": {
            "GlossList": {
                "GlossEntry": {
                    "Abbrev":                       "ISO 8879:1986",
                    "Acronym":                      "SGML",
                    "GlossDef": {
                        "GlossSeeAlso": {
                            "0":                    "GML",
                            "1":                    "XML"
                        },
                        "para":                     "A meta-markup language, used to create markup languages such as DocBook."
                    },
                    "GlossSee":                     "markup",
                    "GlossTerm":                    "Standard Generalized Markup Language",
                    "ID":                           "SGML",
                    "SortAs":                       "SGML"
                }
            },
            "title":                                "S"
        },
        "title":                                    "example glossary"
    }
}

Input 2:
{"menu": {
  "id": "file",
  "value": "File",
  "popup": {
    "menuitem": [
      {"value": "New", "onclick": "CreateNewDoc()"},
      {"value": "Open", "onclick": "OpenDoc()"},
      {"value": "Close", "onclick": "CloseDoc()"}
    ]
  }
}}

Output 2, BAD:
{
    "menu": {
        "id":                                       "file",
        "popup": {
            "menuitem": {
                "0": {
                    "1":                            "onclick\": \"CreateNewDoc()",
                    "value":                        "New"
                },
                "2": {
                    "3":                            "onclick\": \"OpenDoc()",
                    "value":                        "Open"
                },
                "4": {
                    "5":                            "onclick\": \"CloseDoc()",
                    "value":                        "Close"
                }
            }
        },
        "value":                                    "File"
    }
}


lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010
In some day I will fix it.
I've rewritten it from scratch in a shift+reduce fashion.

lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010
Ok, it's up. I also edited the post here.

  • Guests
  • Last active:
  • Joined: --
When using json, your settings causes problem, in include files, is it not possible change them to default ahk value?

  • Guests
  • Last active:
  • Joined: --
i forgot what the settings, excuse me. :?

lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010
If the settings give you some problem, just edit the file.
You can add

#Escapechar `
#Commentflag ;

at the bottom of the file

or you can edit the file and substitute every comment
and escapechar yourself. I will not take care of that
because then it would be a problem to mantain the
library, as I write it with those settings.

  • Guests
  • Last active:
  • Joined: --
hy!
your example give allways error in my tryings
i think it is from double quote in your examples saved file: ""more testing""
if i write manually "more testing" in .json file (only one quote: -"-),
it gives no error
in jsontext i think the value of keys are written with "xyz" (one quote) because these are strings, but when saving them, this strings quotes requoted...???
is this a bug or i forget anything? thanks.

lordkrandel
  • Members
  • 32 posts
  • Last active: Jul 17 2013 12:47 PM
  • Joined: 12 Dec 2010
I didn't quite understand the case.
This is what should happen

ahk -> json
{"a": "35"} --> {"a": "35"}
{"a": """35"""} --> {"a": "\"35\""}

If anything goes __wrong__ there, like
{"a": "35"} --> {"a": ""35""}
{"a": """35"""} --> {"a": ""35""}
then I'll fix it today as soon as I found the time.
It shouldn't take me too much.

  • Guests
  • Last active:
  • Joined: --
I try to explain my problem and my solving sophisticated:
i write your example and run it, this is what i write from your example:

#include C:\Program Files\AutoHotkey\Lib\json.ahk

#EscapeChar `
#CommentFlag ;


jsonString =
( ltrim join
    {
        "1" : "test string",
        "2" : "more testing",
        "a" : {
            "a1" : "+3.5",
            "a2" : "-5"
        }
    }
)

j := JSON_from(jsonString)
msgbox, % ""
    . j[1] . "`n"
    . j[2] . "`n"
    . (j["a","a1"] == 3.5)
JSON_save(j, "test.json")

$$ := JSON_load("test.json")
msgbox, % ""
    . $("1") . "`n"
    . $("2") . "`n"
    . ($("a.a1") == 3.5 )

msgbox, % JSON_to($$)

Then i first take this msgbox:
---------------------------
jsonExample.ahk
---------------------------
"test string"
"more testing"
0
---------------------------
Ok
---------------------------

well, but second msgbox is so:
---------------------------
jsonExample.ahk
---------------------------
[20110203233403] Error at pos:42
""test string"",
"2": ""more testing"",
"a": {
"a1": ""+3.5"",
"a2": ""-5""
}
}

---------------------------
Ok
---------------------------

if i look at test.json file, i saw this lines are written there:
{
"1": ""test string"",
"2": ""more testing"",
"a": {
"a1": ""+3.5"",
"a2": ""-5""
}
}

i changed all the double double quotes with (only one) double quotes, like:
""test string"" ;old, written automatically!
"test string" ;new manual written!

then i repeated running with this new manually changed test.json file, and i take this msgbox info:
---------------------------
jsonExample.ahk
---------------------------
{
"1": ""test string"",
"2": ""more testing"",
"a": {
"a1": ""+3.5"",
"a2": ""-5""
}
}
---------------------------
Ok
---------------------------

i think when "json_to" make its work, there works, a wrong thing, mixed into the work of a wrong?
(yes i write like Shakespeare:) :D
this is the case. Thanks for your interest!