diff --git a/Makefile b/Makefile index b859630..5763d95 100644 --- a/Makefile +++ b/Makefile @@ -96,7 +96,7 @@ INSTALL_LIBRARY = @INSTALL_LIBRARY@ PACKAGE_NAME = rl_json PACKAGE_VERSION = 0.5 CC = gcc -CFLAGS_DEFAULT = -g +CFLAGS_DEFAULT = -O2 -fomit-frame-pointer CFLAGS_WARNING = -Wall -Wno-implicit-int EXEEXT = LDFLAGS_DEFAULT = -Wl,--export-dynamic @@ -164,7 +164,7 @@ CLEANFILES = scripts-stamp scripts/tclIndex CPPFLAGS = LIBS = -L/home/cyan/local/lib -lyajl AR = ar -CFLAGS = -O3 -pipe -m64 ${CFLAGS_DEFAULT} ${CFLAGS_WARNING} ${SHLIB_CFLAGS} +CFLAGS = -O3 -pipe ${CFLAGS_DEFAULT} ${CFLAGS_WARNING} ${SHLIB_CFLAGS} COMPILE = $(CC) $(DEFS) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) .SUFFIXES: .c .$(OBJEXT) diff --git a/generic/main.c b/generic/main.c index b00506a..7aad031 100644 --- a/generic/main.c +++ b/generic/main.c @@ -144,6 +144,26 @@ static int JSON_GetJvalFromObj(Tcl_Interp* interp, Tcl_Obj* obj, int* type, Tcl_ return TCL_OK; } +//}}} +static int JSON_SetIntRep(Tcl_Interp* interp, Tcl_Obj* target, int type, Tcl_Obj* replacement) //{{{ +{ + if (Tcl_IsShared(target)) + THROW_ERROR("Called JSON_SetIntRep on a shared object: ", Tcl_GetString(target)); + + target->internalRep.ptrAndLongRep.value = type; + + if (target->internalRep.ptrAndLongRep.ptr != NULL) + Tcl_DecrRefCount((Tcl_Obj*)target->internalRep.ptrAndLongRep.ptr); + + target->internalRep.ptrAndLongRep.ptr = replacement; + if (target->internalRep.ptrAndLongRep.ptr != NULL) + Tcl_IncrRefCount((Tcl_Obj*)target->internalRep.ptrAndLongRep.ptr); + + Tcl_InvalidateStringRep(target); + + return TCL_OK; +} + //}}} static Tcl_Obj* JSON_NewJvalObj(int type, const void* p, int l) //{{{ { @@ -151,7 +171,7 @@ static Tcl_Obj* JSON_NewJvalObj(int type, const void* p, int l) //{{{ Tcl_Obj* val = NULL; res->typePtr = &json_type; - res->internalRep.ptrAndLongRep.value = type; + res->internalRep.ptrAndLongRep.ptr = NULL; switch (type) { case JSON_OBJECT: val = Tcl_NewDictObj(); break; @@ -170,18 +190,39 @@ static Tcl_Obj* JSON_NewJvalObj(int type, const void* p, int l) //{{{ val = Tcl_NewStringObj(p, l); break; - default: Tcl_Panic("JSON_NewJvalObj, unhandled type: %d", type); + default: + Tcl_Panic("JSON_NewJvalObj, unhandled type: %d", type); } - res->internalRep.ptrAndLongRep.ptr = val; - if (res->internalRep.ptrAndLongRep.ptr != NULL) - Tcl_IncrRefCount((Tcl_Obj*)res->internalRep.ptrAndLongRep.ptr); - Tcl_InvalidateStringRep(res); + + if (JSON_SetIntRep(NULL, res, type, val) != TCL_OK) + Tcl_Panic("Couldn't set JSON intrep"); return res; } //}}} +static int force_json_number(Tcl_Interp* interp, Tcl_Obj* obj, Tcl_Obj** forced) //{{{ +{ + Tcl_Obj* cmd; + int res; + + cmd = Tcl_NewListObj(0, NULL); + TEST_OK(Tcl_ListObjAppendElement(interp, cmd, Tcl_NewStringObj("::tcl::mathop::+", -1))); + TEST_OK(Tcl_ListObjAppendElement(interp, cmd, Tcl_NewStringObj("0", 1))); + TEST_OK(Tcl_ListObjAppendElement(interp, cmd, obj)); + + Tcl_IncrRefCount(cmd); + res = Tcl_EvalObjEx(interp, cmd, TCL_EVAL_DIRECT); + Tcl_DecrRefCount(cmd); + + if (res == TCL_OK) + *forced = Tcl_GetObjResult(interp); + + return res; +} + +//}}} static void append_json_string(const struct serialize_context* scx, Tcl_Obj* obj) //{{{ { int len; @@ -212,7 +253,7 @@ static int serialize_json_val(Tcl_Interp* interp, struct serialize_context* scx, int v_type = 0; Tcl_Obj* iv = NULL; - TEST_OK(Tcl_DictObjFirst(NULL, val, &search, &k, &v, &done)); + TEST_OK(Tcl_DictObjFirst(interp, val, &search, &k, &v, &done)); Tcl_DStringAppend(ds, "{", 1); for (; !done; Tcl_DictObjNext(&search, &k, &v, &done)) { @@ -267,7 +308,7 @@ static int serialize_json_val(Tcl_Interp* interp, struct serialize_context* scx, } Tcl_DStringAppend(ds, ":", 1); - JSON_GetJvalFromObj(NULL, v, &v_type, &iv); + JSON_GetJvalFromObj(interp, v, &v_type, &iv); if (serialize_json_val(interp, scx, v_type, iv) != TCL_OK) { res = TCL_ERROR; break; @@ -285,7 +326,7 @@ static int serialize_json_val(Tcl_Interp* interp, struct serialize_context* scx, Tcl_Obj* iv = NULL; int v_type = 0; - TEST_OK(Tcl_ListObjGetElements(NULL, val, &oc, &ov)); + TEST_OK(Tcl_ListObjGetElements(interp, val, &oc, &ov)); Tcl_DStringAppend(ds, "[", 1); for (i=0; iserialize_mode = SERIALIZE_NORMAL; reset_mode = 1; } else if (subst_type == JSON_DYN_TEMPLATE) { + if (subst_val != NULL) Tcl_DecrRefCount(subst_val); res = JSON_GetJvalFromObj(interp, subst_val, &subst_type, &subst_val); + Tcl_IncrRefCount(subst_val); + } else if (subst_type == JSON_NUMBER) { + Tcl_Obj* forced; + + if (force_json_number(interp, subst_val, &forced) != TCL_OK) { + Tcl_ResetResult(interp); + Tcl_SetObjResult(interp, Tcl_ObjPrintf("Error substituting value from \"%s\" into template, not a number: \"%s\"", Tcl_GetString(val), Tcl_GetString(subst_val))); + return TCL_ERROR; + } + + if (subst_val != NULL) + Tcl_DecrRefCount(subst_val); + + Tcl_IncrRefCount(subst_val = forced); + Tcl_ResetResult(interp); } if (res == TCL_OK) res = serialize_json_val(interp, scx, subst_type, subst_val); + if (subst_val != NULL) + Tcl_DecrRefCount(subst_val); + if (reset_mode) scx->serialize_mode = SERIALIZE_TEMPLATE; } @@ -715,6 +778,186 @@ static int get_modifier(Tcl_Interp* interp, Tcl_Obj* modobj, enum modifiers* mod return TCL_OK; } +//}}} +static int set_path(Tcl_Interp* interp, Tcl_Obj* srcvar, Tcl_Obj *const pathv[], int pathc, Tcl_Obj* replacement) //{{{ +{ + int type, i, newtype; + Tcl_Obj* val; + Tcl_Obj* step; + Tcl_Obj* src; + Tcl_Obj* target; + Tcl_Obj* newval; + + TEST_OK(JSON_GetJvalFromObj(interp, replacement, &newtype, &newval)); + + src = Tcl_ObjGetVar2(interp, srcvar, NULL, TCL_LEAVE_ERR_MSG); + if (src == NULL) + return TCL_ERROR; + + if (Tcl_IsShared(src)) { + src = Tcl_DuplicateObj(src); + if (Tcl_ObjSetVar2(interp, srcvar, NULL, src, TCL_LEAVE_ERR_MSG) == NULL) + return TCL_ERROR; + } + + /* + fprintf(stderr, "set_path, srcvar: \"%s\", src: \"%s\"\n", + Tcl_GetString(srcvar), Tcl_GetString(src)); + */ + target = src; + + TEST_OK(JSON_GetJvalFromObj(interp, target, &type, &val)); + if (Tcl_IsShared(val)) { + Tcl_DecrRefCount(val); + val = Tcl_DuplicateObj(val); + Tcl_IncrRefCount((Tcl_Obj*)(target->internalRep.ptrAndLongRep.ptr = val)); + } + + // Walk the path as far as it exists in src + //fprintf(stderr, "set, initial type %s\n", type_names[type]); + for (i=0; i= 4) { + if (index_str[3] != '-' || index_str[3] != '+') { + ok = 0; + } else { + // errno is magically thread-safe on POSIX + // systems (it's thread-local) + errno = 0; + index += strtol(index_str+3, &end, 10); + if (errno != 0 || *end != 0) + ok = 0; + } + } + } + + if (!ok) + THROW_ERROR("Expected an integer index or end(+/-integer)?, got ", Tcl_GetString(step)); + + //fprintf(stderr, "Resolved index of %ld from \"%s\"\n", index, index_str); + } else { + //fprintf(stderr, "Explicit index: %ld\n", index); + } + + if (index < 0) { + // Prepend element to the array + target = JSON_NewJvalObj(JSON_NULL, NULL, 0); + TEST_OK(Tcl_ListObjReplace(interp, val, -1, 0, 0, &target)); + + i++; + goto followed_path; + } else if (index >= ac) { + int new_i; + for (new_i=0; new_iinternalRep.ptrAndLongRep.ptr = val)); + } + //fprintf(stderr, "Walked on to new type %s\n", type_names[type]); + } + +followed_path: + // target points at the (first) object to replace. It and its internalRep + // are unshared + + // If any path elements remain then they need to be created as object + // keys + for (i=0; iinternalRep.ptrAndLongRep.ptr); + val = Tcl_NewDictObj(); + TEST_OK(JSON_SetIntRep(interp, target, JSON_OBJECT, val)); + } + + target = JSON_NewJvalObj(JSON_OBJECT, NULL, 0); + TEST_OK(Tcl_DictObjPut(interp, val, pathv[i], target)); + TEST_OK(JSON_GetJvalFromObj(interp, target, &type, &val)); + // This was just created - it can't be shared + } + + TEST_OK(JSON_SetIntRep(interp, target, newtype, newval)); + + return TCL_OK; +} + //}}} static int resolve_path(Tcl_Interp* interp, Tcl_Obj* src, Tcl_Obj *const pathv[], int pathc, Tcl_Obj** target, int exists) //{{{ { @@ -835,7 +1078,7 @@ static int resolve_path(Tcl_Interp* interp, Tcl_Obj* src, Tcl_Obj *const pathv[] break; //}}} default: - Tcl_Panic("Unhandled modifier type: %d", modifier); + THROW_ERROR("Unhandled modifier type: ", Tcl_GetString(Tcl_NewIntObj(modifier))); } //fprintf(stderr, "Handled modifier, skipping descent check\n"); break; @@ -844,7 +1087,7 @@ static int resolve_path(Tcl_Interp* interp, Tcl_Obj* src, Tcl_Obj *const pathv[] } switch (type) { case JSON_UNDEF: //{{{ - Tcl_Panic("Found JSON_UNDEF type jval following path"); + THROW_ERROR("Found JSON_UNDEF type jval following path"); //}}} case JSON_OBJECT: //{{{ TEST_OK(Tcl_DictObjGet(interp, val, step, target)); @@ -856,7 +1099,7 @@ static int resolve_path(Tcl_Interp* interp, Tcl_Obj* src, Tcl_Obj *const pathv[] ": \"", Tcl_GetString(step), "\" not found"); } - TEST_OK(JSON_GetJvalFromObj(interp, src, &type, &val)); + //TEST_OK(JSON_GetJvalFromObj(interp, src, &type, &val)); //fprintf(stderr, "Descended into object, new type: %s, val: (%s)\n", type_names[type], Tcl_GetString(val)); break; //}}} @@ -932,7 +1175,7 @@ static int resolve_path(Tcl_Interp* interp, Tcl_Obj* src, Tcl_Obj *const pathv[] Tcl_GetString(Tcl_NewIntObj(pathc)), ": \"", Tcl_GetString(step), "\""); default: - Tcl_Panic("Unhandled type: %d", type); + THROW_ERROR("Unhandled type: ", Tcl_GetString(Tcl_NewIntObj(type))); } TEST_OK(JSON_GetJvalFromObj(interp, *target, &type, &val)); @@ -1028,27 +1271,6 @@ static int convert_to_tcl(Tcl_Interp* interp, Tcl_Obj* obj, Tcl_Obj** out) //{{{ return res; } -//}}} -static int force_json_number(Tcl_Interp* interp, Tcl_Obj* obj, Tcl_Obj** forced) //{{{ -{ - Tcl_Obj* cmd; - int res; - - cmd = Tcl_NewListObj(0, NULL); - TEST_OK(Tcl_ListObjAppendElement(interp, cmd, Tcl_NewStringObj("::tcl::mathop::+", -1))); - TEST_OK(Tcl_ListObjAppendElement(interp, cmd, Tcl_NewStringObj("0", 1))); - TEST_OK(Tcl_ListObjAppendElement(interp, cmd, obj)); - - Tcl_IncrRefCount(cmd); - res = Tcl_EvalObjEx(interp, cmd, TCL_EVAL_DIRECT); - Tcl_DecrRefCount(cmd); - - if (res == TCL_OK) - *forced = Tcl_GetObjResult(interp); - - return res; -} - //}}} static int _new_object(Tcl_Interp* interp, int objc, Tcl_Obj *const objv[], Tcl_Obj** res) //{{{ { @@ -1088,7 +1310,7 @@ static int new_json_value_from_list(Tcl_Interp* interp, int objc, Tcl_Obj *const "true", "false", "null", - "bool", + "boolean", (char*)NULL }; enum { @@ -1213,7 +1435,7 @@ static int new_json_value_from_list(Tcl_Interp* interp, int objc, Tcl_Obj *const break; //}}} default: - Tcl_Panic("Invalid new_type: %d", new_type); + THROW_ERROR("Invalid new_type: ", Tcl_GetString(Tcl_NewIntObj(new_type))); } return TCL_OK; @@ -1638,9 +1860,8 @@ static int jsonObjCmd(ClientData cdata, Tcl_Interp* interp, int objc, Tcl_Obj *c break; //}}} case M_SET: //{{{ - if (objc < 6) CHECK_ARGS(5, "set varname path type val"); - // TODO - THROW_ERROR("Not implemented yet"); + if (objc < 4) CHECK_ARGS(5, "set varname ?path ...? json_val"); + TEST_OK(set_path(interp, objv[2], objv+3, objc-4, objv[objc-1])); break; //}}} case M_FMT: diff --git a/tests/all.tcl b/tests/all.tcl index 3f54f85..97e1159 100644 --- a/tests/all.tcl +++ b/tests/all.tcl @@ -12,6 +12,13 @@ if {[lsearch [namespace children] ::tcltest] == -1} { namespace import ::tcltest::* } +if {[llength [info commands memory]] == 1} { + memory init on + #memory onexit memdebug + #memory validate on + #memory trace on +} + set ::tcltest::testSingleFile false set ::tcltest::testsDirectory [file dir [info script]] diff --git a/tests/set.test b/tests/set.test index 8580576..a7e1354 100644 --- a/tests/set.test +++ b/tests/set.test @@ -4,37 +4,136 @@ if {"::tcltest" ni [namespace children]} { } package require rl_json +package require parse_args +namespace path {::parse_args} -# Compare two json docs, ignoring non-semantic elements (optional whitespace, -# object key ordering, etc) -proc compare {j1 j2} { #<<< - if {[json get $j1 ?type] ne [json get $j2 ?type]} { - return 0 +# Compare two JSON values, ignoring non-semantic elements (optional +# whitespace, object key ordering, etc) +proc _compare_json {opts j1 j2 {path {}}} { #<<< + set mismatch { + msg { + upvar 1 path path + if {[llength $path] != 0} { + append msg ", at path $path" + } + throw {RL TEST JSON_MISMATCH} $msg + } } - switch -- [json get $j1 ?type] { + try { + json get $j1 ?type + } on error errmsg { + apply $mismatch "Cannot parse left JSON value:\n$errmsg" + } on ok j1_type {} + + try { + json get $j2 ?type + } on error errmsg { + apply $mismatch "Cannot parse right JSON value:\n$errmsg" + } on ok j2_type {} + + set j1_val [json get $j1] + set j2_val [json get $j2] + + if {$j2_type eq "string" && [regexp {^\?([A-Z]):(.*)$} $j2_val - cmp_type cmp_args]} { + # Custom matching <<< + set escape 0 + switch -- $cmp_type { + D { # Compare dates <<< + if {$j1_type ne "string"} { + apply $mismatch "left value isn't a date: $j1, expecting a string, got $j1_type" + } + + set now [clock seconds] + try { + set scanned [clock scan $j1_val] + } on error {errmsg options} { + apply $mismatch "Can't interpret left date \"$j1_val\"" + } + + switch -regexp -matchvar m -- $cmp_args { + today { + if {[clock format $scanned -format %Y-%m-%d] ne [clock format $now -format %Y-%m-%d]} { + apply $mismatch "left date \"$j1_val\" is not today" + } + } + + {^within ([0-9]+) (second|minute|hour|day|week|month|year)s?$} { + lassign $m - offset unit + if {![tcl::mathop::<= [clock add $now -$offset $unit] $scanned [clock add $now $offset $unit]]} { + apply $mismatch "left date \"$j1_val\" is not within $offset $unit[s? $offset] of the current time" + } + } + + default { + error "Invalid date comparison syntax: \"$cmp_args\"" + } + } + #>>> + } + + G { # Glob match <<< + if {$j1_type ne "string"} { + apply $mismatch "left value isn't a string: $j1" + } + if {![string match $cmp_args $j1_val]} { + apply $mismatch "left value doesn't match glob: \"$cmp_args\"" + } + #>>> + } + + R { # Regex match <<< + if {$j1_type ne "string"} { + apply $mismatch "left value isn't a string: $j1" + } + if {![regexp $cmp_args $j1_val]} { + apply $mismatch "left value doesn't match regex: \"$cmp_args\"" + } + #>>> + } + + L { # Literal value <<< + set j2_val $cmp_args + set escape 1 + #>>> + } + + default { + error "Invalid custom comparison type \"$cmp_type\"" + } + } + if {!$escape} { + return + } + # Custom matching >>> + } + + if {$j1_type ne $j2_type} { + apply $mismatch "JSON value types differ: left $j1_type != right $j2_type" + } + + switch -- $j1_type { object { # Two JSON objects are considered to match if they have the same # keys (regardless of order), and the values stored in those keys # match according to this function if {[json get $j1 ?size] != [json get $j2 ?size]} { - return 0 + apply $mismatch "Object keys differ: left ([json get $j1 ?keys]) vs. right ([json get $j2 ?keys])" } lassign [intersect3 [json get $j1 ?keys] [json get $j2 ?keys]] \ j1_only both j2_only - if {[llength $j1_only] > 0 || [llength $j2_only] > 0} { - return 0 + switch -- [llength $j1_only],[llength $j2_only] { + 0,0 {} + *,0 {if {[dict get $opts -subset] ni {right intersection}} {apply $mismatch "Left object has extra keys: $j1_only"}} + 0,* {if {[dict get $opts -subset] ni {left intersection}} {apply $mismatch "Right object has extra keys: $j2_only"}} + *,* {if {[dict get $opts -subset] ne "intersection"} {apply $mismatch "Left object has extra keys: $j1_only and right object has extra keys: $j2_only"}} } foreach key $both { - if {![compare [json extract $j1 $key] [json extract $j2 $key]]} { - return 0 - } + _compare_json $opts [json extract $j1 $key] [json extract $j2 $key] [list {*}$path $key] } - - return 1 } array { @@ -42,27 +141,43 @@ proc compare {j1 j2} { #<<< # number of elements, and each element (in order) is considered to # match by this function if {[json get $j1 ?length] != [json get $j2 ?length]} { - return 0 + apply $mismatch "Arrays are different length: left [json get $j1 ?length] vs. right [json get $j2 ?length]" } + set idx -1 json foreach e1 $j1 e2 $j2 { - if {![compare $e1 $e2]} { - return 0 - } + incr idx + _compare_json $opts $e1 $e2 $idx } - return 1 } - string { expr {[json get $j1] eq [json get $j2]} } - number { expr {[json get $j1] == [json get $j2]} } - boolean { expr {[json get $j1] == [json get $j2]} } - null { return 1 } + string { if {[json get $j1] ne [json get $j2]} {apply $mismatch "Strings differ: left: \"[json get $j1]\" vs. right: \"[json get $j2]\""} } + number { if {[json get $j1] == [json get $j2]} {apply $mismatch "Numbers differ: left: [json extract $j1] vs. right: [json extract $j2]"} } + boolean { if {[json get $j1] == [json get $j2]} {apply $mismatch "Booleans differ: left: [json extract $j1] vs. right: [json extract $j2]"} } + null { } default { - error "Unsupported JSON type for compare: \"[json get $j1 ?type]\"" + error "Unsupported JSON type for compare: \"$j1_type\"" } } } +#>>> +proc compare_json args { #<<< + parse_args $args { + -subset {-default none} + j1 {} + j2 {} + } opts + + try { + _compare_json $opts [dict get $opts j1] [dict get $opts j2] + } trap {RL TEST JSON_MISMATCH} {errmsg options} { + return $errmsg + } on ok {} { + return match + } +} + #>>> test set-1.1 {Update a key in an object} -setup { #<<< @@ -74,16 +189,76 @@ test set-1.1 {Update a key in an object} -setup { #<<< } } -body { json set json bar {"Bar"} - compare { + compare_json { + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz" + } + } $json +} -cleanup { + unset -nocomplain json +} -result match +#>>> +test set-1.2 {Update a key in an object with a number} -setup { #<<< + set json { { "foo": "Foo", "bar": "Bar", "baz": "Baz" } + } +} -body { + json set json bar {1.6} + compare_json { + { + "foo": "Foo", + "bar": 1.6, + "baz": "Baz" + } + } $json +} -cleanup { + unset -nocomplain json +} -result match +#>>> +test set-1.3 {Update a key in an object with an object} -setup { #<<< + set json { + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz" + } + } +} -body { + json set json bar {{"x":true}} + compare_json { + { + "foo": "Foo", + "bar": { + "x": true + }, + "baz": "Baz" + } + } $json +} -cleanup { + unset -nocomplain json +} -result match +#>>> +test set-2.1 {Do Nothing Gracefully (sort of - don't blow up if an empty path is supplied} -setup { #<<< + set json { + { + "foo": "Foo", + "baz": "Baz" + } + } +} -body { + json set json {"Bar"} + compare_json { + "Bar" } $json } -cleanup { unset -nocomplain json -} -result 1 +} -result match #>>> ::tcltest::cleanupTests diff --git a/tests/template.test b/tests/template.test index c7735d3..ae499bc 100644 --- a/tests/template.test +++ b/tests/template.test @@ -97,6 +97,17 @@ test template-1.3 {Produce a JSON doc with interpolated values, subst object key } } -result {{"Foo":"Ba\"r","baz":["str",123,123.4,true,false,null,"~S:not a subst",{"inner":"~S:foo"},{"inner2":"Foo"},"Baz"]}} #>>> +test template-2.1 {Test interpolated numeric validation} -body { #<<< + json template { + { + "Foo": "~N:num", + "Bar": "baz" + } + } { + num "" + } +} -returnCodes error -result {Error substituting value from "num" into template, not a number: ""} +#>>> ::tcltest::cleanupTests return