diff --git a/commands/debug/debug_info.go b/commands/debug/debug_info.go index a8731ab1d6a..d7a792953d7 100644 --- a/commands/debug/debug_info.go +++ b/commands/debug/debug_info.go @@ -18,7 +18,8 @@ package debug import ( "context" "encoding/json" - "regexp" + "slices" + "strconv" "strings" "github.com/arduino/arduino-cli/arduino" @@ -209,33 +210,89 @@ func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Expl // my.indexed.array.2=third // // into the corresponding JSON arrays. +// If a value should be converted into a JSON type different from string, the value +// may be prefiex with "[boolean]", "[number]", or "[object]": +// +// my.stringValue=a string +// my.booleanValue=[boolean]true +// my.numericValue=[number]20 func convertToJsonMap(in *properties.Map) string { - // XXX: Maybe this method could be a good candidate for propertis.Map? - - // Find the values that should be kept as is, and the indexed arrays - // that should be later converted into arrays. - arraysKeys := map[string]bool{} - stringKeys := []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 { - stringKeys = append(stringKeys, 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 +} - // Compose a map that can be later marshaled into JSON keeping - // the arrays where they are expected to be. - res := map[string]any{} - for _, k := range stringKeys { - res[k] = in.Get(k) +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 } - for k := range arraysKeys { - res[k] = in.ExtractSubIndexLists(k) + + // Compose an object + res := map[string]any{} + for _, k := range keys { + switch { + case subtrees[k] != nil: + res[k] = convertToRawInterface(subtrees[k]) + default: + res[k] = convertToRawValue(in.Get(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 } diff --git a/commands/debug/debug_test.go b/commands/debug/debug_test.go index 0b99a105c64..3adf4fbfee6 100644 --- a/commands/debug/debug_test.go +++ b/commands/debug/debug_test.go @@ -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" ) @@ -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) +} diff --git a/docs/platform-specification.md b/docs/platform-specification.md index c34047bf8d9..83fbf4279ff 100644 --- a/docs/platform-specification.md +++ b/docs/platform-specification.md @@ -1403,6 +1403,37 @@ 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]`, `[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: + +```json +{ + "aBoolean": true, + "aNumber": 10, + "anotherNumber": 10.2, + "anObject": { + "boolean": true, + "key": "value" + }, + "anotherObject": { + "boolean": true, + "key": "value" + } +} +``` + ### Optimization level for debugging The compiler optimization level that is appropriate for normal usage will often not provide a good experience while diff --git a/internal/integrationtest/debug/debug_test.go b/internal/integrationtest/debug/debug_test.go index 0d3a0cc9dbf..8c547ba04fa 100644 --- a/internal/integrationtest/debug/debug_test.go +++ b/internal/integrationtest/debug/debug_test.go @@ -133,6 +133,19 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli }, "svd_file": "svd-file", "cortex-debug_custom_configuration": { + "aBoolean": true, + "aStringBoolean": "true", + "aStringNumber": "10", + "aNumber": 10, + "anotherNumber": 10.2, + "anObject": { + "boolean": true, + "key": "value" + }, + "anotherObject": { + "boolean": true, + "key": "value" + }, "anotherStringParamer": "hellooo", "overrideRestartCommands": [ "monitor reset halt", @@ -176,6 +189,19 @@ func testAllDebugInformation(t *testing.T, env *integrationtest.Environment, cli }, "svd_file": "svd-file", "cortex-debug_custom_configuration": { + "aBoolean": true, + "aStringBoolean": "true", + "aStringNumber": "10", + "aNumber": 10, + "anotherNumber": 10.2, + "anObject": { + "boolean": true, + "key": "value" + }, + "anotherObject": { + "boolean": true, + "key": "value" + }, "anotherStringParamer": "hellooo", "overrideRestartCommands": [ "monitor reset halt", diff --git a/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt b/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt index 987aa196fdd..7a96eac3186 100644 --- a/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt +++ b/internal/integrationtest/debug/testdata/hardware/my/samd/boards.txt @@ -44,6 +44,14 @@ my.debug.cortex-debug.custom.overrideRestartCommands.1=monitor gdb_sync my.debug.cortex-debug.custom.overrideRestartCommands.2=thb setup my.debug.cortex-debug.custom.overrideRestartCommands.3=c my.debug.cortex-debug.custom.anotherStringParamer=hellooo +my.debug.cortex-debug.custom.aBoolean=[boolean]true +my.debug.cortex-debug.custom.aStringBoolean=true +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