diff --git a/builtin/builtin.go b/builtin/builtin.go index e6680ac96..9f6e65cbb 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -876,13 +876,8 @@ var Builtins = []*Function{ // Check if it's milliseconds (timestamp > year 2001) // Unix timestamp for Jan 1, 2001 is 978307200 - if epoch > 978307200000 { - // Treat as milliseconds - t = time.Unix(epoch/1000, (epoch%1000)*1000000) - } else { - // Treat as seconds - t = time.Unix(epoch, 0) - } + // Treat as milliseconds + t = time.Unix(epoch/1000, (epoch%1000)*1000000) if tz != nil { t = t.In(tz) @@ -894,17 +889,11 @@ var Builtins = []*Function{ var t time.Time // Check if it's milliseconds - if epoch > 978307200000 { - // Treat as milliseconds - sec := int64(epoch / 1000) - nsec := int64((epoch - float64(sec*1000)) * 1000000) - t = time.Unix(sec, nsec) - } else { - // Treat as seconds (can have fractional part) - sec := int64(epoch) - nsec := int64((epoch - float64(sec)) * 1000000000) - t = time.Unix(sec, nsec) - } + + // Treat as milliseconds + sec := int64(epoch / 1000) + nsec := int64((epoch - float64(sec*1000)) * 1000000) + t = time.Unix(sec, nsec) if tz != nil { t = t.In(tz) @@ -1609,4 +1598,376 @@ var Builtins = []*Function{ return integerType, nil }, }, + // JSON manipulation functions + { + Name: "mergeJson", + Func: func(args ...any) (any, error) { + if len(args) != 2 { + return nil, fmt.Errorf("invalid number of arguments for mergeJson (expected 2, got %d)", len(args)) + } + + // Helper function to convert input to map[string]any + convertToMap := func(input any) (map[string]any, error) { + if input == nil { + return make(map[string]any), nil + } + + // If it's already a map, convert it + if obj, ok := input.(map[string]any); ok { + return obj, nil + } + if m, ok := input.(map[any]any); ok { + result := make(map[string]any) + for k, v := range m { + if keyStr, ok := k.(string); ok { + result[keyStr] = v + } + } + return result, nil + } + + // If it's a string, try to parse as JSON + if str, ok := input.(string); ok { + var parsed any + err := json.Unmarshal([]byte(str), &parsed) + if err != nil { + return nil, fmt.Errorf("invalid JSON string: %v", err) + } + if obj, ok := parsed.(map[string]any); ok { + return obj, nil + } + return nil, fmt.Errorf("JSON string does not represent an object") + } + + return nil, fmt.Errorf("invalid argument: expected object or JSON string, got %T", input) + } + + obj1, err := convertToMap(args[0]) + if err != nil { + return nil, fmt.Errorf("first argument: %v", err) + } + + obj2, err := convertToMap(args[1]) + if err != nil { + return nil, fmt.Errorf("second argument: %v", err) + } + + // Create result map and copy from first object + result := make(map[string]any) + for k, v := range obj1 { + result[k] = v + } + + // Merge from second object (overwrites existing keys) + for k, v := range obj2 { + result[k] = v + } + + return result, nil + }, + Types: types( + new(func(map[string]any, map[string]any) map[string]any), + new(func(map[any]any, map[any]any) map[string]any), + new(func(string, string) map[string]any), + new(func(string, map[string]any) map[string]any), + new(func(map[string]any, string) map[string]any), + ), + }, + { + Name: "addJsonKey", + Func: func(args ...any) (any, error) { + if len(args) != 3 { + return nil, fmt.Errorf("invalid number of arguments for addJsonKey (expected 3, got %d)", len(args)) + } + + // Helper function to convert input to map[string]any + convertToMap := func(input any) (map[string]any, error) { + if input == nil { + return make(map[string]any), nil + } + + // If it's already a map, convert it + if obj, ok := input.(map[string]any); ok { + return obj, nil + } + if m, ok := input.(map[any]any); ok { + result := make(map[string]any) + for k, v := range m { + if keyStr, ok := k.(string); ok { + result[keyStr] = v + } + } + return result, nil + } + + // If it's a string, try to parse as JSON + if str, ok := input.(string); ok { + var parsed any + err := json.Unmarshal([]byte(str), &parsed) + if err != nil { + return nil, fmt.Errorf("invalid JSON string: %v", err) + } + if obj, ok := parsed.(map[string]any); ok { + return obj, nil + } + return nil, fmt.Errorf("JSON string does not represent an object") + } + + return nil, fmt.Errorf("invalid argument: expected object or JSON string, got %T", input) + } + + obj, err := convertToMap(args[0]) + if err != nil { + return nil, fmt.Errorf("first argument: %v", err) + } + + // Handle nil key + if args[1] == nil { + return nil, fmt.Errorf("invalid key for addJsonKey: key cannot be null") + } + key, ok := args[1].(string) + if !ok { + return nil, fmt.Errorf("invalid key for addJsonKey: expected string, got %T", args[1]) + } + + // Create result map and copy existing keys + result := make(map[string]any) + for k, v := range obj { + result[k] = v + } + + // Add new key-value pair + result[key] = args[2] + + return result, nil + }, + Types: types( + new(func(map[string]any, string, any) map[string]any), + new(func(map[any]any, string, any) map[string]any), + new(func(string, string, any) map[string]any), + ), + }, + { + Name: "updateJsonKey", + Func: func(args ...any) (any, error) { + if len(args) != 3 { + return nil, fmt.Errorf("invalid number of arguments for updateJsonKey (expected 3, got %d)", len(args)) + } + + // Helper function to convert input to map[string]any + convertToMap := func(input any) (map[string]any, error) { + if input == nil { + return nil, fmt.Errorf("object cannot be null") + } + + // If it's already a map, convert it + if obj, ok := input.(map[string]any); ok { + return obj, nil + } + if m, ok := input.(map[any]any); ok { + result := make(map[string]any) + for k, v := range m { + if keyStr, ok := k.(string); ok { + result[keyStr] = v + } + } + return result, nil + } + + // If it's a string, try to parse as JSON + if str, ok := input.(string); ok { + var parsed any + err := json.Unmarshal([]byte(str), &parsed) + if err != nil { + return nil, fmt.Errorf("invalid JSON string: %v", err) + } + if obj, ok := parsed.(map[string]any); ok { + return obj, nil + } + return nil, fmt.Errorf("JSON string does not represent an object") + } + + return nil, fmt.Errorf("invalid argument: expected object or JSON string, got %T", input) + } + + obj, err := convertToMap(args[0]) + if err != nil { + return nil, fmt.Errorf("first argument: %v", err) + } + + // Handle nil key + if args[1] == nil { + return nil, fmt.Errorf("invalid key for updateJsonKey: key cannot be null") + } + key, ok := args[1].(string) + if !ok { + return nil, fmt.Errorf("invalid key for updateJsonKey: expected string, got %T", args[1]) + } + + // Check if key exists + if _, exists := obj[key]; !exists { + return nil, fmt.Errorf("key '%s' does not exist in object", key) + } + + // Create result map and copy existing keys + result := make(map[string]any) + for k, v := range obj { + result[k] = v + } + + // Update the key with new value + result[key] = args[2] + + return result, nil + }, + Types: types( + new(func(map[string]any, string, any) map[string]any), + new(func(map[any]any, string, any) map[string]any), + new(func(string, string, any) map[string]any), + ), + }, + { + Name: "deleteJsonKey", + Func: func(args ...any) (any, error) { + if len(args) != 2 { + return nil, fmt.Errorf("invalid number of arguments for deleteJsonKey (expected 2, got %d)", len(args)) + } + + // Helper function to convert input to map[string]any + convertToMap := func(input any) (map[string]any, error) { + if input == nil { + return make(map[string]any), nil + } + + // If it's already a map, convert it + if obj, ok := input.(map[string]any); ok { + return obj, nil + } + if m, ok := input.(map[any]any); ok { + result := make(map[string]any) + for k, v := range m { + if keyStr, ok := k.(string); ok { + result[keyStr] = v + } + } + return result, nil + } + + // If it's a string, try to parse as JSON + if str, ok := input.(string); ok { + var parsed any + err := json.Unmarshal([]byte(str), &parsed) + if err != nil { + return nil, fmt.Errorf("invalid JSON string: %v", err) + } + if obj, ok := parsed.(map[string]any); ok { + return obj, nil + } + return nil, fmt.Errorf("JSON string does not represent an object") + } + + return nil, fmt.Errorf("invalid argument: expected object or JSON string, got %T", input) + } + + obj, err := convertToMap(args[0]) + if err != nil { + return nil, fmt.Errorf("first argument: %v", err) + } + + // Handle nil key + if args[1] == nil { + return nil, fmt.Errorf("invalid key for deleteJsonKey: key cannot be null") + } + key, ok := args[1].(string) + if !ok { + return nil, fmt.Errorf("invalid key for deleteJsonKey: expected string, got %T", args[1]) + } + + // Create result map and copy all keys except the one to delete + result := make(map[string]any) + for k, v := range obj { + if k != key { + result[k] = v + } + } + + return result, nil + }, + Types: types( + new(func(map[string]any, string) map[string]any), + new(func(map[any]any, string) map[string]any), + new(func(string, string) map[string]any), + ), + }, + { + Name: "diffJson", + Func: func(args ...any) (any, error) { + if len(args) != 2 { + return nil, fmt.Errorf("invalid number of arguments for diffJson (expected 2, got %d)", len(args)) + } + + // Helper function to convert input to map[string]any + convertToMap := func(input any) (map[string]any, error) { + if input == nil { + return make(map[string]any), nil + } + + // If it's already a map, convert it + if obj, ok := input.(map[string]any); ok { + return obj, nil + } + if m, ok := input.(map[any]any); ok { + result := make(map[string]any) + for k, v := range m { + if keyStr, ok := k.(string); ok { + result[keyStr] = v + } + } + return result, nil + } + + // If it's a string, try to parse as JSON + if str, ok := input.(string); ok { + var parsed any + err := json.Unmarshal([]byte(str), &parsed) + if err != nil { + return nil, fmt.Errorf("invalid JSON string: %v", err) + } + if obj, ok := parsed.(map[string]any); ok { + return obj, nil + } + return nil, fmt.Errorf("JSON string does not represent an object") + } + + return nil, fmt.Errorf("invalid argument: expected object or JSON string, got %T", input) + } + + obj1, err := convertToMap(args[0]) + if err != nil { + return nil, fmt.Errorf("first argument: %v", err) + } + + obj2, err := convertToMap(args[1]) + if err != nil { + return nil, fmt.Errorf("second argument: %v", err) + } + + // Create result with keys from A that are NOT in B (A - B) + result := make(map[string]any) + for key, value := range obj1 { + if _, existsInB := obj2[key]; !existsInB { + result[key] = value + } + } + + return result, nil + }, + Types: types( + new(func(map[string]any, map[string]any) map[string]any), + new(func(map[any]any, map[any]any) map[string]any), + new(func(string, string) map[string]any), + new(func(string, map[string]any) map[string]any), + new(func(map[string]any, string) map[string]any), + ), + }, } diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index 7118498e7..279fdb6f4 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -861,3 +861,232 @@ func TestBuiltin_random(t *testing.T) { assert.Contains(t, err.Error(), "max must be positive") }) } + +func TestBuiltin_JSON_Functions(t *testing.T) { + t.Run("mergeJson", func(t *testing.T) { + env := map[string]any{ + "obj1": map[string]any{"a": 1, "b": 2}, + "obj2": map[string]any{"b": 3, "c": 4}, + } + + // Test basic merge + program, err := expr.Compile(`mergeJson(obj1, obj2)`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + expected := map[string]any{"a": 1, "b": 3, "c": 4} + assert.Equal(t, expected, out) + + // Test with JSON strings + out, err = expr.Eval(`mergeJson('{"x": 1}', '{"y": 2}')`, env) + require.NoError(t, err) + expected = map[string]any{"x": float64(1), "y": float64(2)} // JSON numbers are float64 + assert.Equal(t, expected, out) + + // Test mixed string and object + out, err = expr.Eval(`mergeJson('{"x": 1}', {"y": 2})`, env) + require.NoError(t, err) + expected = map[string]any{"x": float64(1), "y": 2} + assert.Equal(t, expected, out) + + // Test with nil objects + out, err = expr.Eval(`mergeJson(null, {"x": 1})`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{"x": 1}, out) + + out, err = expr.Eval(`mergeJson({"x": 1}, null)`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{"x": 1}, out) + + out, err = expr.Eval(`mergeJson(null, null)`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{}, out) + + // Test error with invalid JSON string + _, err = expr.Eval(`mergeJson('{"invalid": json}', {"a": 1})`, env) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid JSON string") + }) + + t.Run("addJsonKey", func(t *testing.T) { + env := map[string]any{ + "obj": map[string]any{"a": 1, "b": 2}, + } + + // Test adding new key + program, err := expr.Compile(`addJsonKey(obj, "c", 3)`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + expected := map[string]any{"a": 1, "b": 2, "c": 3} + assert.Equal(t, expected, out) + + // Test with JSON string + out, err = expr.Eval(`addJsonKey('{"a": 1}', "b", 2)`, env) + require.NoError(t, err) + expected = map[string]any{"a": float64(1), "b": 2} + assert.Equal(t, expected, out) + + // Test adding to nil object + out, err = expr.Eval(`addJsonKey(null, "x", "value")`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{"x": "value"}, out) + + // Test overwriting existing key + out, err = expr.Eval(`addJsonKey({"a": 1}, "a", 2)`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{"a": 2}, out) + + // Test error with null key + _, err = expr.Eval(`addJsonKey({"a": 1}, null, 2)`, env) + assert.Error(t, err) + assert.Contains(t, err.Error(), "key cannot be null") + }) + + t.Run("updateJsonKey", func(t *testing.T) { + env := map[string]any{ + "obj": map[string]any{"a": 1, "b": 2}, + } + + // Test updating existing key + program, err := expr.Compile(`updateJsonKey(obj, "a", 10)`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + expected := map[string]any{"a": 10, "b": 2} + assert.Equal(t, expected, out) + + // Test with JSON string + out, err = expr.Eval(`updateJsonKey('{"a": 1, "b": 2}', "a", 10)`, env) + require.NoError(t, err) + expected = map[string]any{"a": 10, "b": float64(2)} + assert.Equal(t, expected, out) + + // Test error with non-existent key + _, err = expr.Eval(`updateJsonKey({"a": 1}, "b", 2)`, env) + assert.Error(t, err) + assert.Contains(t, err.Error(), "does not exist") + + // Test error with null object + _, err = expr.Eval(`updateJsonKey(null, "a", 1)`, env) + assert.Error(t, err) + assert.Contains(t, err.Error(), "object cannot be null") + + // Test error with null key + _, err = expr.Eval(`updateJsonKey({"a": 1}, null, 2)`, env) + assert.Error(t, err) + assert.Contains(t, err.Error(), "key cannot be null") + }) + + t.Run("deleteJsonKey", func(t *testing.T) { + env := map[string]any{ + "obj": map[string]any{"a": 1, "b": 2, "c": 3}, + } + + // Test deleting existing key + program, err := expr.Compile(`deleteJsonKey(obj, "b")`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + expected := map[string]any{"a": 1, "c": 3} + assert.Equal(t, expected, out) + + // Test with JSON string + out, err = expr.Eval(`deleteJsonKey('{"a": 1, "b": 2}', "a")`, env) + require.NoError(t, err) + expected = map[string]any{"b": float64(2)} + assert.Equal(t, expected, out) + + // Test deleting non-existent key (should not error) + out, err = expr.Eval(`deleteJsonKey({"a": 1}, "b")`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{"a": 1}, out) + + // Test with nil object + out, err = expr.Eval(`deleteJsonKey(null, "a")`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{}, out) + + // Test error with null key + _, err = expr.Eval(`deleteJsonKey({"a": 1}, null)`, env) + assert.Error(t, err) + assert.Contains(t, err.Error(), "key cannot be null") + }) + + t.Run("diffJson", func(t *testing.T) { + env := map[string]any{ + "obj1": map[string]any{"a": 1, "b": 2, "d": 4}, + "obj2": map[string]any{"a": 1, "b": 3, "c": 4}, + } + + // Test basic diff - A minus B (keys in A but not in B) + program, err := expr.Compile(`diffJson(obj1, obj2)`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + + // Should return keys from obj1 that are NOT in obj2 + // obj1 has: {"a": 1, "b": 2, "d": 4} + // obj2 has: {"a": 1, "b": 3, "c": 4} + // Keys in obj1 but NOT in obj2: {"d": 4} + expected := map[string]any{"d": 4} + assert.Equal(t, expected, out) + + // Test with JSON strings + out, err = expr.Eval(`diffJson('{"a": 1, "b": 2}', '{"a": 1, "c": 3}')`, env) + require.NoError(t, err) + // Keys in first object but not in second: {"b": 2} + expected = map[string]any{"b": float64(2)} + assert.Equal(t, expected, out) + + // Test with identical objects - should return empty + out, err = expr.Eval(`diffJson({"a": 1}, {"a": 1})`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{}, out) + + // Test with completely different objects + out, err = expr.Eval(`diffJson({"x": 1, "y": 2}, {"z": 3})`, env) + require.NoError(t, err) + expected = map[string]any{"x": 1, "y": 2} + assert.Equal(t, expected, out) + + // Test with nil objects + out, err = expr.Eval(`diffJson({"a": 1}, null)`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{"a": 1}, out) // all keys from A since B is empty + + out, err = expr.Eval(`diffJson(null, {"a": 1})`, env) + require.NoError(t, err) + assert.Equal(t, map[string]any{}, out) // empty A minus anything is empty + }) + + t.Run("JSON_functions_with_fromJSON", func(t *testing.T) { + // Test integration with existing fromJSON function + env := map[string]any{} + + // Test merging JSON strings + out, err := expr.Eval(`mergeJson(fromJSON('{"a": 1}'), fromJSON('{"b": 2}'))`, env) + require.NoError(t, err) + expected := map[string]any{"a": float64(1), "b": float64(2)} // JSON unmarshals numbers as float64 + assert.Equal(t, expected, out) + + // Test adding key to JSON string result + out, err = expr.Eval(`addJsonKey(fromJSON('{"a": 1}'), "b", 2)`, env) + require.NoError(t, err) + expected = map[string]any{"a": float64(1), "b": 2} + assert.Equal(t, expected, out) + + // Test converting result back to JSON + out, err = expr.Eval(`toJSON(mergeJson(fromJSON('{"a": 1}'), {"b": 2}))`, env) + require.NoError(t, err) + // The exact order may vary, but it should be valid JSON containing both keys + jsonStr := out.(string) + assert.Contains(t, jsonStr, `"a":1`) + assert.Contains(t, jsonStr, `"b":2`) + }) +}