Skip to content

Commit

Permalink
Implement function 'parse_json'
Browse files Browse the repository at this point in the history
Kinda like the Python JSON loads function. Takes in a string and tries
to parse it as json, converting it recursively into native RSL types.
For example, if you have a valid JSON dictionary as a string, and pass
that in, it will return an RslMap with the constituent fields (all the
way through) likewise converted to native RSL types.

This should be useful for operations where you get a JSON string returns
(e.g. via a shell command or a rad request) and you want to
inspect/interact with the json blob as it is.
  • Loading branch information
amterp committed Nov 14, 2024
1 parent 78dc9fd commit 9a93702
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 47 deletions.
4 changes: 4 additions & 0 deletions VERSION_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Only for major & minor version releases. Contains only notable items.
- Emoji support
- Basic syntax highlighter
- Reworked JSON field extraction algo
- `errdefer`
- Reworked string character escaping
- Improved rad block sorting operation, added matching `sort` function
- Added more functions: `confirm`, `range`, `split` etc

## 0.4

Expand Down
1 change: 1 addition & 0 deletions core/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ const (
UNIQUE = "unique"
SORT_FUNC = "sort"
CONFIRM = "confirm"
PARSE_JSON = "parse_json"
)
40 changes: 3 additions & 37 deletions core/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ func (e *Env) SetAndImplyTypeWithToken(token Token, varName string, value interf
case bool:
e.Vars[varName] = coerced
case []interface{}:
converted := e.recursivelyConvertTypes(token, coerced)
converted := ConvertToNativeTypes(e.i, token, coerced)
e.Vars[varName] = converted.([]interface{})
case RslMap:
converted := e.recursivelyConvertTypes(token, coerced)
converted := ConvertToNativeTypes(e.i, token, coerced)
e.Vars[varName] = converted.(RslMap)
case map[string]interface{}:
converted := e.recursivelyConvertTypes(token, coerced)
converted := ConvertToNativeTypes(e.i, token, coerced)
e.Vars[varName] = converted.(RslMap)
default:
e.i.error(token, fmt.Sprintf("Unknown type, cannot set: '%T' %q = %q", value, varName, value))
Expand Down Expand Up @@ -168,37 +168,3 @@ func (e *Env) get(token Token, varName string, acceptableTypes ...RslTypeEnum) (
e.i.error(token, fmt.Sprintf("Variable type mismatch: %v, expected: %v", varName, acceptableTypes))
panic(UNREACHABLE)
}

// todo since supporting maps, I think I can get rid of this
// it was originally implemented because we might capture JSON as a list of unhandled types, but
// now we should be able to capture json and convert it entirely to native RSL types up front
func (e *Env) recursivelyConvertTypes(token Token, arr interface{}) interface{} {
switch coerced := arr.(type) {
// strictly speaking, I don't think ints are necessary to handle, since it seems Go unmarshalls
// json 'ints' into floats
case string, int64, float64, bool:
return coerced
case int:
return int64(coerced)
case []interface{}:
output := make([]interface{}, len(coerced))
for i, val := range coerced {
output[i] = e.recursivelyConvertTypes(token, val)
}
return output
case map[string]interface{}:
m := NewRslMap()
sortedKeys := SortedKeys(coerced)
for _, key := range sortedKeys {
m.Set(key, e.recursivelyConvertTypes(token, coerced[key]))
}
return *m
case RslMap:
return coerced
case nil:
return nil
default:
e.i.error(token, fmt.Sprintf("Unhandled type in array: %T", arr))
panic(UNREACHABLE)
}
}
26 changes: 26 additions & 0 deletions core/rsl_func_parse_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package core

import (
"encoding/json"
"fmt"
)

func runParseJson(i *MainInterpreter, function Token, args []interface{}) interface{} {
if len(args) != 1 {
i.error(function, PARSE_JSON+fmt.Sprintf("() takes exactly one argument, got %d", len(args)))
}

switch coerced := args[0].(type) {
case string:
var m interface{}
err := json.Unmarshal([]byte(coerced), &m)
if err != nil {
i.error(function, fmt.Sprintf("Error parsing JSON: %v", err))
}
return ConvertToNativeTypes(i, function, m)
default:
// maybe a bit harsh, should allow just passthrough of e.g. int64?
i.error(function, PARSE_JSON+fmt.Sprintf("() expects string, got %s", TypeAsString(args[0])))
panic(UNREACHABLE)
}
}
4 changes: 4 additions & 0 deletions core/rsl_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ func RunRslNonVoidFunction(
assertExpectedNumReturnValues(i, function, functionName, numExpectedReturnValues, 1)
validateExpectedNamedArgs(i, function, NO_NAMED_ARGS, namedArgsMap)
return runConfirm(i, function, args)
case PARSE_JSON:
assertExpectedNumReturnValues(i, function, functionName, numExpectedReturnValues, 1)
validateExpectedNamedArgs(i, function, NO_NAMED_ARGS, namedArgsMap)
return runParseJson(i, function, args)
default:
i.error(function, fmt.Sprintf("Unknown function: %v", functionName))
panic(UNREACHABLE)
Expand Down
33 changes: 33 additions & 0 deletions core/rsl_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,36 @@ func TypeAsString(val interface{}) string {
panic(UNREACHABLE)
}
}

// it was originally implemented because we might capture JSON as a list of unhandled types, but
// now we should be able to capture json and convert it entirely to native RSL types up front
func ConvertToNativeTypes(interp *MainInterpreter, token Token, arr interface{}) interface{} {
switch coerced := arr.(type) {
// strictly speaking, I don't think ints are necessary to handle, since it seems Go unmarshalls
// json 'ints' into floats
case string, int64, float64, bool:
return coerced
case int:
return int64(coerced)
case []interface{}:
output := make([]interface{}, len(coerced))
for i, val := range coerced {
output[i] = ConvertToNativeTypes(interp, token, val)
}
return output
case map[string]interface{}:
m := NewRslMap()
sortedKeys := SortedKeys(coerced)
for _, key := range sortedKeys {
m.Set(key, ConvertToNativeTypes(interp, token, coerced[key]))
}
return *m
case RslMap:
return coerced
case nil:
return nil
default:
interp.error(token, fmt.Sprintf("Unhandled type in array: %T", arr))
panic(UNREACHABLE)
}
}
99 changes: 99 additions & 0 deletions core/testing/func_parse_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package testing

import "testing"

func TestParseJson_Int(t *testing.T) {
rsl := `
a = parse_json("2")
print(a + 1)
`
setupAndRunCode(t, rsl)
assertOnlyOutput(t, stdOutBuffer, "3\n")
assertNoErrors(t)
resetTestState()
}

func TestParseJson_Float(t *testing.T) {
rsl := `
a = parse_json("2.1")
print(a + 1)
`
setupAndRunCode(t, rsl)
assertOnlyOutput(t, stdOutBuffer, "3.1\n")
assertNoErrors(t)
resetTestState()
}

func TestParseJson_Bool(t *testing.T) {
rsl := `
a = parse_json("true")
print(a or false)
`
setupAndRunCode(t, rsl)
assertOnlyOutput(t, stdOutBuffer, "true\n")
assertNoErrors(t)
resetTestState()
}

func TestParseJson_String(t *testing.T) {
rsl := `
a = parse_json('"alice"')
print(a + "e")
`
setupAndRunCode(t, rsl)
assertOnlyOutput(t, stdOutBuffer, "alicee\n")
assertNoErrors(t)
resetTestState()
}

func TestParseJson_Map(t *testing.T) {
rsl := `
a = parse_json('\{"name": "alice", "age": 20, "height": 5.5, "is_student": true, "cars": ["audi", "bmw"], "friends": \{"bob": 1, "charlie": 2}}')
print(a["name"] + "e")
print(a["age"] + 1)
print(a["height"] + 1.1)
print(a["is_student"] or false)
print(a["cars"][0] + "e")
print(a["friends"]["bob"] + 1)
`
setupAndRunCode(t, rsl)
assertOnlyOutput(t, stdOutBuffer, "alicee\n21\n6.6\ntrue\naudie\n2\n")
assertNoErrors(t)
resetTestState()
}

func TestParseJson_ErrorsOnInvalidJson(t *testing.T) {
rsl := `
parse_json('\{asd asd}')
`
setupAndRunCode(t, rsl)
assertError(t, 1, "RslError at L2/10 on 'parse_json': Error parsing JSON: invalid character 'a' looking for beginning of object key string\n")
resetTestState()
}

func TestParseJson_ErrorsOnInvalidType(t *testing.T) {
rsl := `
parse_json(10)
`
setupAndRunCode(t, rsl)
assertError(t, 1, "RslError at L2/10 on 'parse_json': parse_json() expects string, got int\n")
resetTestState()
}

func TestParseJson_ErrorsOnNoArgs(t *testing.T) {
rsl := `
parse_json()
`
setupAndRunCode(t, rsl)
assertError(t, 1, "RslError at L2/10 on 'parse_json': parse_json() takes exactly one argument, got 0\n")
resetTestState()
}

func TestParseJson_ErrorsOnTooManyArgs(t *testing.T) {
rsl := `
parse_json("1", "2")
`
setupAndRunCode(t, rsl)
assertError(t, 1, "RslError at L2/10 on 'parse_json': parse_json() takes exactly one argument, got 2\n")
resetTestState()
}
6 changes: 6 additions & 0 deletions docs-web/docs/reference/ref-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ sort([3, 4, 2, 1], reversed=true) // [4, 3, 2, 1]
sort([3, 4, "2", 1, true]) // [true, 1, 3, 4, "2"]
```

### parse_json

```rsl
parse_json(input string)
```

## Time

### now_date
Expand Down
18 changes: 9 additions & 9 deletions docs-web/docs/strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ title: Strings
## Escaping

- `"double quote strings"` and `'single quote strings'` have the same rules around escaping.
- `` `backtick strings` `` has slightly different rules (less escaping).
- `` `backtick strings` `` have slightly different rules (less escaping).

### Double & Single Quotes

- `\` will escape:
- `{` (to prevent string interpolation)
- `\n` new line
- `\t` tab
- `\` i.e. itself, so you can write backslashes
- The respective quote char itself, so `"\""` and `'\''`
- However, it's advised to instead mix string delimiters instead, especially with backticks. So respectively: `` `"` ``, `` `'` ``.
- `{` (to prevent string interpolation)
- `\n` new line
- `\t` tab
- `\` i.e. itself, so you can write backslashes
- The respective quote char itself, so `"\""` and `'\''`
- However, it's advised to instead mix string delimiters instead, especially with backticks. So respectively: `` `"` ``, `` `'` ``.

### Backticks

- `\` will escape:
- `{` (to prevent string interpolation)
- `` ` `` to allow backticks in the string
- `{` (to prevent string interpolation)
- `` ` `` to allow backticks in the string
2 changes: 1 addition & 1 deletion new_release.rsl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fail:
print("❌ Failed to build and test!")

// Update Version in ./core/cobra_root.go
$`sed -i '' "s/Version: \\".*\\"/Version: \\"{new_version}\\"/" ./core/cobra_root.go`
$`sed -i '' "s/Version: \".*\"/Version: \"{new_version}\"/" ./core/cobra_root.go`
fail:
print("❌ Failed to update version in code!")

Expand Down

0 comments on commit 9a93702

Please sign in to comment.