Skip to content

Commit

Permalink
Improved JSON properties generation
Browse files Browse the repository at this point in the history
  • Loading branch information
cmaglie committed Oct 31, 2023
1 parent 3bd36c3 commit 80fec99
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 43 deletions.
112 changes: 70 additions & 42 deletions commands/debug/debug_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package debug
import (
"context"
"encoding/json"
"regexp"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -217,54 +217,82 @@ func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Expl
// my.booleanValue=[boolean]true
// my.numericValue=[number]20
func convertToJsonMap(in *properties.Map) string {
// Find the values that should be kept as is, and the indexed arrays
// that should be later converted into arrays.
arraysKeys := map[string]bool{}
scalarKeys := []string{}
trailingNumberMatcher := regexp.MustCompile(`^(.*)\.[0-9]+$`)
for _, k := range in.Keys() {
match := trailingNumberMatcher.FindAllStringSubmatch(k, -1)
if len(match) > 0 && len(match[0]) > 1 {
arraysKeys[match[0][1]] = true
} else {
scalarKeys = append(scalarKeys, k)
data, _ := json.MarshalIndent(convertToRawInterface(in), "", " ")
return string(data)
}

func allNumerics(in []string) bool {
for _, i := range in {
for _, c := range i {
if c < '0' || c > '9' {
return false
}
}
}
return true
}

func convertToRawInterface(in *properties.Map) any {
subtrees := in.FirstLevelOf()
keys := in.FirstLevelKeys()

if allNumerics(keys) {
// Compose an array
res := []any{}
slices.SortFunc(keys, func(x, y string) int {
nx, _ := strconv.Atoi(x)
ny, _ := strconv.Atoi(y)
return nx - ny
})
for _, k := range keys {
switch {
case subtrees[k] != nil:
res = append(res, convertToRawInterface(subtrees[k]))
default:
res = append(res, convertToRawValue(in.Get(k)))
}
}
return res
}

// Compose a map that can be later marshaled into JSON keeping
// the arrays where they are expected to be.
// Compose an object
res := map[string]any{}
for _, k := range scalarKeys {
v := in.Get(k)
for _, k := range keys {
switch {
case strings.HasPrefix(v, "[boolean]"):
v = strings.TrimSpace(strings.TrimPrefix(v, "[boolean]"))
if strings.EqualFold(v, "true") {
res[k] = true
} else if strings.EqualFold(v, "false") {
res[k] = false
}
case strings.HasPrefix(v, "[number]"):
v = strings.TrimPrefix(v, "[number]")
if i, err := strconv.Atoi(v); err == nil {
res[k] = i
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
res[k] = f
}
case strings.HasPrefix(v, "[object]"):
v = strings.TrimPrefix(v, "[object]")
var o interface{}
if err := json.Unmarshal([]byte(v), &o); err == nil {
res[k] = o
}
case subtrees[k] != nil:
res[k] = convertToRawInterface(subtrees[k])
default:
res[k] = v
res[k] = convertToRawValue(in.Get(k))
}
}
for k := range arraysKeys {
res[k] = in.ExtractSubIndexLists(k)
}
return res
}

data, _ := json.MarshalIndent(res, "", " ")
return string(data)
func convertToRawValue(v string) any {
switch {
case strings.HasPrefix(v, "[boolean]"):
v = strings.TrimSpace(strings.TrimPrefix(v, "[boolean]"))
if strings.EqualFold(v, "true") {
return true
} else if strings.EqualFold(v, "false") {
return false
}
case strings.HasPrefix(v, "[number]"):
v = strings.TrimPrefix(v, "[number]")
if i, err := strconv.Atoi(v); err == nil {
return i
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
return f
}
case strings.HasPrefix(v, "[object]"):
v = strings.TrimPrefix(v, "[object]")
var o interface{}
if err := json.Unmarshal([]byte(v), &o); err == nil {
return o
}
case strings.HasPrefix(v, "[string]"):
v = strings.TrimPrefix(v, "[string]")
}
// default or conversion error, return string as is
return v
}
97 changes: 97 additions & 0 deletions commands/debug/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"github.com/arduino/go-properties-orderedmap"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -90,3 +91,99 @@ func TestGetCommandLine(t *testing.T) {
commandToTest2 := strings.Join(command2, " ")
assert.Equal(t, filepath.FromSlash(goldCommand2), filepath.FromSlash(commandToTest2))
}

func TestConvertToJSONMap(t *testing.T) {
testIn := properties.NewFromHashmap(map[string]string{
"k": "v",
"string": "[string]aaa",
"bool": "[boolean]true",
"number": "[number]10",
"number2": "[number]10.2",
"object": `[object]{ "key":"value", "bool":true }`,
"array.0": "first",
"array.1": "second",
"array.2": "[boolean]true",
"array.3": "[number]10",
"array.4": `[object]{ "key":"value", "bool":true }`,
"array.5.k": "v",
"array.5.bool": "[boolean]true",
"array.5.number": "[number]10",
"array.5.number2": "[number]10.2",
"array.5.object": `[object]{ "key":"value", "bool":true }`,
"array.6.sub.k": "v",
"array.6.sub.bool": "[boolean]true",
"array.6.sub.number": "[number]10",
"array.6.sub.number2": "[number]10.2",
"array.6.sub.object": `[object]{ "key":"value", "bool":true }`,
"array.7.0": "v",
"array.7.1": "[boolean]true",
"array.7.2": "[number]10",
"array.7.3": "[number]10.2",
"array.7.4": `[object]{ "key":"value", "bool":true }`,
"array.8.array.0": "v",
"array.8.array.1": "[boolean]true",
"array.8.array.2": "[number]10",
"array.8.array.3": "[number]10.2",
"array.8.array.4": `[object]{ "key":"value", "bool":true }`,
"sub.k": "v",
"sub.bool": "[boolean]true",
"sub.number": "[number]10",
"sub.number2": "[number]10.2",
"sub.object": `[object]{ "key":"value", "bool":true }`,
})
jsonString := convertToJsonMap(testIn)
require.JSONEq(t, `{
"k": "v",
"string": "aaa",
"bool": true,
"number": 10,
"number2": 10.2,
"object": { "key":"value", "bool":true },
"array": [
"first",
"second",
true,
10,
{ "key":"value", "bool":true },
{
"k": "v",
"bool": true,
"number": 10,
"number2": 10.2,
"object": { "key":"value", "bool":true }
},
{
"sub": {
"k": "v",
"bool": true,
"number": 10,
"number2": 10.2,
"object": { "key":"value", "bool":true }
}
},
[
"v",
true,
10,
10.2,
{ "key":"value", "bool":true }
],
{
"array": [
"v",
true,
10,
10.2,
{ "key":"value", "bool":true }
]
}
],
"sub": {
"k": "v",
"bool": true,
"number": 10,
"number2": 10.2,
"object": { "key":"value", "bool":true }
}
}`, jsonString)
}
9 changes: 8 additions & 1 deletion docs/platform-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -1404,13 +1404,16 @@ will result in the following JSON to be merged in the Arduino IDE generated `lau
```

All the values are converted by default to a string in the resulting JSON. If another type is needed the value can be
prefixed with the tags `[boolean]`, `[number]`, or `[object]` to force a specific type in the JSON, for example:
prefixed with the tags `[boolean]`, `[number]`, `[string]` or `[object]` to force a specific type in the JSON. Moreover
the hierarchy of the properties may be used to build JSON objects. For example:

```
debug.cortex-debug.custom.aBoolean=[boolean]true
debug.cortex-debug.custom.aNumber=[number]10
debug.cortex-debug.custom.anotherNumber=[number]10.20
debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true}
debug.cortex-debug.custom.anotherObject.key=value
debug.cortex-debug.custom.anotherObject.boolean=[boolean]true
```

will result in the following JSON:
Expand All @@ -1423,6 +1426,10 @@ will result in the following JSON:
"anObject": {
"boolean": true,
"key": "value"
},
"anotherObject": {
"boolean": true,
"key": "value"
}
}
```
Expand Down
8 changes: 8 additions & 0 deletions internal/integrationtest/debug/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli
"boolean": true,
"key": "value"
},
"anotherObject": {
"boolean": true,
"key": "value"
},
"anotherStringParamer": "hellooo",
"overrideRestartCommands": [
"monitor reset halt",
Expand Down Expand Up @@ -194,6 +198,10 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli
"boolean": true,
"key": "value"
},
"anotherObject": {
"boolean": true,
"key": "value"
},
"anotherStringParamer": "hellooo",
"overrideRestartCommands": [
"monitor reset halt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ my.debug.cortex-debug.custom.aNumber=[number]10
my.debug.cortex-debug.custom.anotherNumber=[number]10.20
my.debug.cortex-debug.custom.aStringNumber=10
my.debug.cortex-debug.custom.anObject=[object]{"key":"value", "boolean":true}
my.debug.cortex-debug.custom.anotherObject.key=value
my.debug.cortex-debug.custom.anotherObject.boolean=[boolean]true
my.debug.svd_file=svd-file

my2.name=My Cool Board
Expand Down

0 comments on commit 80fec99

Please sign in to comment.