From 5fe23327c961b5cdadd65df230f8c2137d8a27e4 Mon Sep 17 00:00:00 2001 From: Tim Jurcka Date: Mon, 15 Jul 2013 17:25:47 +0200 Subject: [PATCH] Add JSON This closes #37, #11 --- builtin_json.go | 276 +++++++++++++++++++++++++++++++++++++++++++++++ clone.go | 1 + global_test.go | 2 +- inline | 16 +++ inline.go | 81 ++++++++++++++ json_test.go | 185 +++++++++++++++++++++++++++++++ otto_.go | 15 ++- runtime.go | 1 + type_function.go | 4 + 9 files changed, 575 insertions(+), 6 deletions(-) create mode 100644 builtin_json.go create mode 100644 json_test.go diff --git a/builtin_json.go b/builtin_json.go new file mode 100644 index 0000000..6476073 --- /dev/null +++ b/builtin_json.go @@ -0,0 +1,276 @@ +package otto + +import ( + "bytes" + "encoding/json" + "math" + "strings" +) + +type _builtinJSON_parseContext struct { + call FunctionCall + reviver Value +} + +func builtinJSON_parse(call FunctionCall) Value { + ctx := _builtinJSON_parseContext{ + call: call, + } + revive := false + if reviver := call.Argument(1); reviver.isCallable() { + revive = true + ctx.reviver = reviver + } + + var root interface{} + err := json.Unmarshal([]byte(toString(call.Argument(0))), &root) + if err != nil { + panic(newSyntaxError(err.Error())) + } + value, exists := builtinJSON_parseWalk(ctx, root) + if !exists { + value = UndefinedValue() + } + if revive { + root := ctx.call.runtime.newObject() + root.put("", value, false) + return builtinJSON_reviveWalk(ctx, root, "") + } + return value +} + +func builtinJSON_reviveWalk(ctx _builtinJSON_parseContext, holder *_object, name string) Value { + value := holder.get(name) + if object := value._object(); object != nil { + if isArray(object) { + length := int64(objectLength(object)) + for index := int64(0); index < length; index += 1 { + name := arrayIndexToString(index) + value := builtinJSON_reviveWalk(ctx, object, name) + if value.IsUndefined() { + object.delete(name, false) + } else { + object.defineProperty(name, value, 0111, false) + } + } + } else { + object.enumerate(false, func(name string) bool { + value := builtinJSON_reviveWalk(ctx, object, name) + if value.IsUndefined() { + object.delete(name, false) + } else { + object.defineProperty(name, value, 0111, false) + } + return true + }) + } + } + return ctx.reviver.call(toValue_object(holder), name, value) +} + +func builtinJSON_parseWalk(ctx _builtinJSON_parseContext, rawValue interface{}) (Value, bool) { + switch value := rawValue.(type) { + case nil: + return NullValue(), true + case bool: + return toValue_bool(value), true + case string: + return toValue_string(value), true + case float64: + return toValue_float64(value), true + case []interface{}: + arrayValue := make([]Value, len(value)) + for index, rawValue := range value { + if value, exists := builtinJSON_parseWalk(ctx, rawValue); exists { + arrayValue[index] = value + } + } + return toValue_object(ctx.call.runtime.newArrayOf(arrayValue)), true + case map[string]interface{}: + object := ctx.call.runtime.newObject() + for name, rawValue := range value { + if value, exists := builtinJSON_parseWalk(ctx, rawValue); exists { + object.put(name, value, false) + } + } + return toValue_object(object), true + } + return Value{}, false +} + +type _builtinJSON_stringifyContext struct { + call FunctionCall + stack []*_object + propertyList []string + replacerFunction *Value + gap string +} + +func builtinJSON_stringify(call FunctionCall) Value { + ctx := _builtinJSON_stringifyContext{ + call: call, + stack: []*_object{nil}, + } + replacer := call.Argument(1)._object() + if replacer != nil { + if isArray(replacer) { + length := objectLength(replacer) + seen := map[string]bool{} + propertyList := make([]string, length) + length = 0 + for index, _ := range propertyList { + value := replacer.get(arrayIndexToString(int64(index))) + switch value._valueType { + case valueObject: + switch value.value.(*_object).class { + case "String": + case "Number": + default: + continue + } + case valueString: + case valueNumber: + default: + continue + } + name := toString(value) + if seen[name] { + continue + } + seen[name] = true + length += 1 + propertyList[index] = name + } + ctx.propertyList = propertyList[0:length] + } else if replacer.class == "Function" { + value := toValue_object(replacer) + ctx.replacerFunction = &value + } + } + if spaceValue, exists := call.getArgument(2); exists { + if spaceValue._valueType == valueObject { + switch spaceValue.value.(*_object).class { + case "String": + spaceValue = toValue_string(toString(spaceValue)) + case "Number": + spaceValue = toNumber(spaceValue) + } + } + switch spaceValue._valueType { + case valueString: + value := toString(spaceValue) + if len(value) > 10 { + ctx.gap = value[0:10] + } else { + ctx.gap = value + } + case valueNumber: + value := toInteger(spaceValue).value + if value > 10 { + value = 10 + } else if value < 0 { + value = 0 + } + ctx.gap = strings.Repeat(" ", int(value)) + } + } + holder := call.runtime.newObject() + holder.put("", call.Argument(0), false) + value, exists := builtinJSON_stringifyWalk(ctx, "", holder) + if !exists { + return UndefinedValue() + } + valueJSON, err := json.Marshal(value) + if err != nil { + panic(newTypeError(err.Error())) + } + if ctx.gap != "" { + valueJSON1 := bytes.Buffer{} + json.Indent(&valueJSON1, valueJSON, "", ctx.gap) + valueJSON = valueJSON1.Bytes() + } + return toValue_string(string(valueJSON)) +} + +func builtinJSON_stringifyWalk(ctx _builtinJSON_stringifyContext, key string, holder *_object) (interface{}, bool) { + value := holder.get(key) + + if value.IsObject() { + if toJSON := value._object().get("toJSON"); toJSON.IsFunction() { + value = toJSON.call(value, key) + } + } + + if ctx.replacerFunction != nil { + value = (*ctx.replacerFunction).call(toValue_object(holder), key, value) + } + + if value._valueType == valueObject { + switch value.value.(*_object).class { + case "Boolean": + value = value._object().value.(Value) + case "String": + value = toValue_string(toString(value)) + case "Number": + value = toNumber(value) + } + } + + switch value._valueType { + case valueBoolean: + return toBoolean(value), true + case valueString: + return toString(value), true + case valueNumber: + value := toFloat(value) + if math.IsNaN(value) || math.IsInf(value, 0) { + return nil, true + } + return value, true + case valueNull: + return nil, true + case valueObject: + holder := value._object() + if value := value._object(); nil != value { + for _, object := range ctx.stack { + if holder == object { + panic(newTypeError("Converting circular structure to JSON")) + } + } + ctx.stack = append(ctx.stack, value) + defer func() { ctx.stack = ctx.stack[:len(ctx.stack)-1] }() + } + if isArray(holder) { + length := holder.get("length").value.(uint32) + array := make([]interface{}, length) + for index, _ := range array { + name := arrayIndexToString(int64(index)) + value, _ := builtinJSON_stringifyWalk(ctx, name, holder) + array[index] = value + } + return array, true + } else if holder.class != "Function" { + object := map[string]interface{}{} + if ctx.propertyList != nil { + for _, name := range ctx.propertyList { + value, exists := builtinJSON_stringifyWalk(ctx, name, holder) + if exists { + object[name] = value + } + } + } else { + // Go maps are without order, so this doesn't conform to the ECMA ordering + // standard, but oh well... + holder.enumerate(false, func(name string) bool { + value, exists := builtinJSON_stringifyWalk(ctx, name, holder) + if exists { + object[name] = value + } + return true + }) + } + return object, true + } + } + return nil, false +} diff --git a/clone.go b/clone.go index 3223aa6..fbdde81 100644 --- a/clone.go +++ b/clone.go @@ -43,6 +43,7 @@ func (runtime *_runtime) clone() *_runtime { clone.object(runtime.Global.ReferenceError), clone.object(runtime.Global.SyntaxError), clone.object(runtime.Global.URIError), + clone.object(runtime.Global.JSON), clone.object(runtime.Global.ObjectPrototype), clone.object(runtime.Global.FunctionPrototype), diff --git a/global_test.go b/global_test.go index fa34e68..d3f3fd1 100644 --- a/global_test.go +++ b/global_test.go @@ -62,7 +62,7 @@ func TestGlobal(t *testing.T) { test(` Object.getOwnPropertyNames(Function('return this')()).sort(); - `, "Array,Boolean,Date,Error,EvalError,Function,Infinity,Math,NaN,Number,Object,RangeError,ReferenceError,RegExp,String,SyntaxError,TypeError,URIError,console,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,escape,eval,isFinite,isNaN,parseFloat,parseInt,undefined,unescape") + `, "Array,Boolean,Date,Error,EvalError,Function,Infinity,JSON,Math,NaN,Number,Object,RangeError,ReferenceError,RegExp,String,SyntaxError,TypeError,URIError,console,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,escape,eval,isFinite,isNaN,parseFloat,parseInt,undefined,unescape") // __defineGetter__,__defineSetter__,__lookupGetter__,__lookupSetter__,constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf test(` diff --git a/inline b/inline index 12d88a3..2e10a4d 100755 --- a/inline +++ b/inline @@ -582,6 +582,21 @@ sub newContext { }); } qw/Eval Type Range Reference Syntax URI/), + # JSON + $self->block(sub { + my $class = "JSON"; + return + ".$class =", + $self->globalObject( + $class, + $self->functionDeclare( + $class, + "parse", 2, + "stringify", 3, + ), + ), + }), + # Global $self->block(sub { my $class = "Global"; @@ -618,6 +633,7 @@ sub newContext { "ReferenceError", "SyntaxError", "URIError", + "JSON", ), $self->property("undefined", $self->undefinedValue(), "0"), $self->property("NaN", $self->numberValue("math.NaN()"), "0"), diff --git a/inline.go b/inline.go index fe89b25..bbb1487 100644 --- a/inline.go +++ b/inline.go @@ -5464,6 +5464,79 @@ func _newContext(runtime *_runtime) { }, } } + { + parse_function := &_object{ + runtime: runtime, + class: "Function", + objectClass: _classObject, + prototype: runtime.Global.FunctionPrototype, + extensible: true, + property: map[string]_property{ + "length": _property{ + mode: 0, + value: Value{ + _valueType: valueNumber, + value: 2, + }, + }, + }, + propertyOrder: []string{ + "length", + }, + value: _functionObject{ + call: _nativeCallFunction(builtinJSON_parse), + }, + } + stringify_function := &_object{ + runtime: runtime, + class: "Function", + objectClass: _classObject, + prototype: runtime.Global.FunctionPrototype, + extensible: true, + property: map[string]_property{ + "length": _property{ + mode: 0, + value: Value{ + _valueType: valueNumber, + value: 3, + }, + }, + }, + propertyOrder: []string{ + "length", + }, + value: _functionObject{ + call: _nativeCallFunction(builtinJSON_stringify), + }, + } + runtime.Global.JSON = &_object{ + runtime: runtime, + class: "JSON", + objectClass: _classObject, + prototype: runtime.Global.ObjectPrototype, + extensible: true, + property: map[string]_property{ + "parse": _property{ + mode: 0101, + value: Value{ + _valueType: valueObject, + value: parse_function, + }, + }, + "stringify": _property{ + mode: 0101, + value: Value{ + _valueType: valueObject, + value: stringify_function, + }, + }, + }, + propertyOrder: []string{ + "parse", + "stringify", + }, + } + } { eval_function := &_object{ runtime: runtime, @@ -5897,6 +5970,13 @@ func _newContext(runtime *_runtime) { value: runtime.Global.URIError, }, }, + "JSON": _property{ + mode: 0101, + value: Value{ + _valueType: valueObject, + value: runtime.Global.JSON, + }, + }, "undefined": _property{ mode: 0, value: Value{ @@ -5946,6 +6026,7 @@ func _newContext(runtime *_runtime) { "ReferenceError", "SyntaxError", "URIError", + "JSON", "undefined", "NaN", "Infinity", diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..3ad4141 --- /dev/null +++ b/json_test.go @@ -0,0 +1,185 @@ +package otto + +import ( + . "./terst" + "testing" + "time" +) + +func BenchmarkJSON_parse(b *testing.B) { + otto := New() + for i := 0; i < b.N; i++ { + otto.Run(`JSON.parse("1")`) + otto.Run(`JSON.parse("[1,2,3]")`) + otto.Run(`JSON.parse('{"a":{"x":100,"y":110},"b":[10,20,30],"c":"zazazaza"}')`) + otto.Run(`JSON.parse("[1,2,3]", function(k, v) { return undefined })`) + } +} + +func TestJSON_parse(t *testing.T) { + Terst(t) + + test := runTest() + + test(` + JSON.parse("1"); + `, "1") + + test(` + JSON.parse("null"); + `, "null") + + test(` + var abc = JSON.parse('"a\uFFFFbc"'); + [ abc[0], abc[2], abc[3], abc.length ]; + `, "a,b,c,4") + + test(` + JSON.parse("[1, 2, 3]"); + `, "1,2,3") + + test(` + JSON.parse('{ "abc": 1, "def":2 }').abc; + `, "1") + + test(` + JSON.parse('{ "abc": { "x": 100, "y": 110 }, "def": [ 10, 20 ,30 ], "ghi": "zazazaza" }').def; + `, "10,20,30") + + test(`raise: + JSON.parse("12\t\r\n 34"); + `, "SyntaxError: invalid character '3' after top-level value") + + test(` + JSON.parse("[1, 2, 3]", function() { return undefined }); + `, "undefined") + + test(`raise: + JSON.parse(""); + `, "SyntaxError: unexpected end of JSON input") + + test(`raise: + JSON.parse("[1, 2, 3"); + `, "SyntaxError: unexpected end of JSON input") + + test(`raise: + JSON.parse("[1, 2, ; abc=10"); + `, "SyntaxError: invalid character ';' looking for beginning of value") + + test(`raise: + JSON.parse("[1, 2, function(){}]"); + `, "SyntaxError: invalid character 'u' in literal false (expecting 'a')") +} + +func TestJSON_stringify(t *testing.T) { + Terst(t) + + test := runTest() + + defer mockTimeLocal(time.UTC)() + + test(` + JSON.stringify(function(){}); + `, "undefined") + + test(` + JSON.stringify(new Boolean(false)); + `, "false") + + test(` + JSON.stringify({a1: {b1: [1,2,3,4], b2: {c1: 1, c2: 2}}, a2: 'a2'}, null, -5); + `, `{"a1":{"b1":[1,2,3,4],"b2":{"c1":1,"c2":2}},"a2":"a2"}`) + + test(` + JSON.stringify(undefined); + `, "undefined") + + test(` + JSON.stringify(1); + `, "1") + + test(` + JSON.stringify("abc def"); + `, "\"abc def\"") + + test(` + JSON.stringify(3.14159); + `, "3.14159") + + test(` + JSON.stringify([]); + `, "[]") + + test(` + JSON.stringify([1, 2, 3]); + `, "[1,2,3]") + + test(` + JSON.stringify([true, false, null]); + `, "[true,false,null]") + + test(` + JSON.stringify({ + abc: { x: 100, y: 110 }, + def: [ 10, 20, 30 ], + ghi: "zazazaza" + }); + `, `{"abc":{"x":100,"y":110},"def":[10,20,30],"ghi":"zazazaza"}`) + + test(` + JSON.stringify([ + 'e', + {pluribus: 'unum'} + ], null, '\t'); + `, "[\n\t\"e\",\n\t{\n\t\t\"pluribus\": \"unum\"\n\t}\n]") + + test(` + JSON.stringify(new Date(0)); + `, `"1970-01-01T00:00:00.000Z"`) + + test(` + JSON.stringify([ new Date(0) ], function(key, value){ + return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value + }); + `, `["Date(Thu, 01 Jan 1970 00:00:00 UTC)"]`) + + test(` + JSON.stringify({ + abc: 1, + def: 2, + ghi: 3 + }, ['abc','def']); + `, `{"abc":1,"def":2}`) + + test(`raise: + var abc = { + def: null + }; + abc.def = abc; + JSON.stringify(abc) + `, "TypeError: Converting circular structure to JSON") + + test(`raise: + var abc= [ null ]; + abc[0] = abc; + JSON.stringify(abc); + `, "TypeError: Converting circular structure to JSON") + + test(`raise: + var abc = { + def: {} + }; + abc.def.ghi = abc; + JSON.stringify(abc) + `, "TypeError: Converting circular structure to JSON") + + test(` + var ghi = { "pi": 3.14159 }; + var abc = { + def: {} + }; + abc.ghi = ghi; + abc.def.ghi = ghi; + JSON.stringify(abc); + `, `{"def":{"ghi":{"pi":3.14159}},"ghi":{"pi":3.14159}}`) +} diff --git a/otto_.go b/otto_.go index 36b5021..e9ff666 100644 --- a/otto_.go +++ b/otto_.go @@ -55,14 +55,19 @@ func arrayIndexToString(index int64) string { return strconv.FormatInt(index, 10) } -func valueOfArrayIndex(list []Value, index int) Value { - if index >= 0 && index < len(list) { - value := list[index] +func valueOfArrayIndex(array []Value, index int) Value { + value, _ := getValueOfArrayIndex(array, index) + return value +} + +func getValueOfArrayIndex(array []Value, index int) (Value, bool) { + if index >= 0 && index < len(array) { + value := array[index] if !value.isEmpty() { - return value + return value, true } } - return UndefinedValue() + return UndefinedValue(), false } // A range index can be anything from 0 up to length. It is NOT safe to use as an index diff --git a/runtime.go b/runtime.go index 8f8dea4..27b9244 100644 --- a/runtime.go +++ b/runtime.go @@ -22,6 +22,7 @@ type _global struct { ReferenceError *_object SyntaxError *_object URIError *_object + JSON *_object ObjectPrototype *_object // Object.prototype FunctionPrototype *_object // Function.prototype diff --git a/type_function.go b/type_function.go index 1e56b46..29e3e82 100644 --- a/type_function.go +++ b/type_function.go @@ -276,6 +276,10 @@ func (self FunctionCall) Argument(index int) Value { return valueOfArrayIndex(self.ArgumentList, index) } +func (self FunctionCall) getArgument(index int) (Value, bool) { + return getValueOfArrayIndex(self.ArgumentList, index) +} + func (self FunctionCall) slice(index int) []Value { if index < len(self.ArgumentList) { return self.ArgumentList[index:]