From 42118a7450dcc6d7ebc5391500387a0feebbaa31 Mon Sep 17 00:00:00 2001 From: Erik Jaegervall Date: Mon, 10 Jul 2023 12:59:04 +0200 Subject: [PATCH] Refactor array handling and add tests --- .github/workflows/kuksa-go.yml | 46 ++++++ kuksa-client/README.md | 73 +++++++++ kuksa-client/kuksa_client/__main__.py | 14 ++ kuksa-client/kuksa_client/grpc/__init__.py | 114 +++++++++----- kuksa-client/tests/test_datapoint.py | 163 +++++++++++++++++++++ kuksa_go_client/client_test.go | 127 ++++++++++++++++ kuksa_go_client/kuksa_client/grpc.go | 86 ++++++----- kuksa_go_client/main.go | 10 +- 8 files changed, 563 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/kuksa-go.yml create mode 100644 kuksa-client/tests/test_datapoint.py create mode 100644 kuksa_go_client/client_test.go diff --git a/.github/workflows/kuksa-go.yml b/.github/workflows/kuksa-go.yml new file mode 100644 index 000000000..948718b80 --- /dev/null +++ b/.github/workflows/kuksa-go.yml @@ -0,0 +1,46 @@ +# /******************************************************************************** +# * Copyright (c) 2022 Contributors to the Eclipse Foundation +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * This program and the accompanying materials are made available under the +# * terms of the Apache License 2.0 which is available at +# * http://www.apache.org/licenses/LICENSE-2.0 +# * +# * SPDX-License-Identifier: Apache-2.0 +# ********************************************************************************/ + +name: kuksa_go_client + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + paths: + - ".github/workflows/kuksa-go.yml" + - "kuksa_go_client/**" + workflow_dispatch: + +jobs: + + kuksa-go-client-test: + runs-on: ubuntu-latest + steps: + - name: Checkout kuksa.val + uses: actions/checkout@v3 + - name: Run go tests + run: | + cd kuksa_go_client + # We cannot use sudo apt install protobuf-compiler + # as default in Ubuntu 22.04 (3.12) consider optional as experimental feature + go run protocInstall/protocInstall.go + export PATH=$PATH:$HOME/protoc/bin + sudo chmod +x $HOME/protoc/bin/protoc + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + export PATH=$PATH:$HOME/go/bin + go generate . + go test . + diff --git a/kuksa-client/README.md b/kuksa-client/README.md index be83acd4f..ff4b5ed4d 100644 --- a/kuksa-client/README.md +++ b/kuksa-client/README.md @@ -182,6 +182,79 @@ This is an example showing how some of the commands can be used: ![try kuksa-client out](https://raw.githubusercontent.com/eclipse/kuksa.val/master/doc/pictures/testclient_basic.gif "test client usage") +### Syntax for specifying data in the command line interface + +Values used as argument to for example `setValue` shall match the type given. Quotes (single and double) are +generally not needed, except in a few special cases. A few valid examples on setting float is shown below: + +``` +setValue Vehicle.Speed 43 +setValue Vehicle.Speed "45" +setValue Vehicle.Speed '45.2' +``` + +For strings escaped quotes are needed if you want quotes to be sent to Server/Databroker, like if you want to store +`Almost "red"` as value. Alternatively you can use outer single quotes and inner double quotes. + +The two examples below are equal: + +``` +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect 'Almost \"red\"' +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect 'Almost "red"' +``` + +Alternatively you can use inner single quotes, but then the value will be represented by double quotes (`Almost "blue"`) +when stored anyhow. + +``` +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect "Almost 'blue'" +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect "Almost \'blue\'" +``` + +If not using outer quotes the inner quotes will be lost, the examples below are equal. +Leading/trialing spaces are ignored. + +``` +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect Almost 'green' +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect Almost green +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect 'Almost green' +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect "Almost green" +setValue Vehicle.Cabin.Light.InteractiveLightBar.Effect 'Almost green ' +``` + + +It is possible to set array values. Setting a string array with simple identifiers is not a problem. Also not if they +contain blanks + +``` +// Array with two elements +setValue Vehicle.OBD.DTCList [abc,def] +// Array with two elements, "hello there" and "def" +setValue Vehicle.OBD.DTCList [hello there,def] +``` + +Setting values that includes comma or quotation marks is more tricky, as the shell argument parser handling affects +how they are interpreted. The recommended approach is to have outer quotes of one type and user inner quotes of the +other type. + +Example 1: First item should be `hello, there` + +``` +setValue Vehicle.OBD.DTCList '[ "hello, there",def]' +``` + +Example 2: First item should be `hello, "there"` + +``` +setValue Vehicle.OBD.DTCList '["hello, \"there\"",def]' +``` + +Example 3: First item should be `hello, 'there'` + +``` +setValue Vehicle.OBD.DTCList "['hello, \'there\'',def]" +``` + ### Updating VSS Structure Using the test client, it is also possible to update and extend the VSS data structure. diff --git a/kuksa-client/kuksa_client/__main__.py b/kuksa-client/kuksa_client/__main__.py index c301cc43a..948cf561a 100755 --- a/kuksa-client/kuksa_client/__main__.py +++ b/kuksa-client/kuksa_client/__main__.py @@ -301,6 +301,20 @@ def do_authorize(self, args): def do_setValue(self, args): """Set the value of a path""" if self.checkConnection(): + # If there is a blank before a single/double quote on the kuksa-client cli then + # the argparser shell will remove it, there is nothing we can do to it + # This gives off behavior for examples like: + # setValue Vehicle.OBD.DTCList [ "dtc1, dtc2", ddd] + # which will be treated as input of 3 elements + # The recommended approach is to have quotes (of a different type) around the whole value + # if your strings includes quotes, commas or other items + # setValue Vehicle.OBD.DTCList '[ "dtc1, dtc2", ddd]' + # or + # setValue Vehicle.OBD.DTCList "[ 'dtc1, dtc2', ddd]" + # If you really need to include a quote in the values use backslash and use the quote type + # you want as inner value: + # setValue Vehicle.OBD.DTCList "[ 'dtc1, \'dtc2', ddd]" + # Will result in two elements in the array; "dtc1, 'dtc2" and "ddd" value = str(' '.join(args.Value)) resp = self.commThread.setValue( args.Path, value, args.attribute) diff --git a/kuksa-client/kuksa_client/grpc/__init__.py b/kuksa-client/kuksa_client/grpc/__init__.py index fe5d78222..a30430f20 100644 --- a/kuksa-client/kuksa_client/grpc/__init__.py +++ b/kuksa-client/kuksa_client/grpc/__init__.py @@ -315,6 +315,59 @@ def from_message(cls, message: types_pb2.Datapoint): ) if message.HasField('timestamp') else None, ) + + def cast_array_values(cast, array): + """ + Parses array input and cast individual values to wanted type. + Note that input value to this function is not the same as given if you use kuksa-client command line + as parts (e.g. surrounding quotes) are removed by shell, and then do_setValue also do some magic. + """ + array = array.strip('[]') + + # Split the input string into separate values + # First alternative, not quotes including escaped single or double quote, ends at comma, whitespace or EOL + # Second group is double quoted string, may contain quoted strings and single quotes inside, ends at non-escaped + # double quote + # Third is similar but for single quote + # Using raw strings with surrounding single/double quotes to minimize need for escapes + pattern = r'(?:\\"|\\' + \ + r"'|[^'" + r'",])+|"(?:\\"|[^"])*"|' + \ + r"'(?:\\'|[^'])*'" + values = re.findall(pattern, array) + for item in values: + # We may in some cases match blanks, that is intended as we want to be able to write arguments like + # My Way + # ... without quotes + if item.strip() == '': + #skip + pass + else: + yield cast(item) + + def cast_bool(value) -> bool: + if value in ('False', 'false', 'F', 'f'): + value = 0 + return bool(value) + + def cast_str(value) -> str: + """ + Strip based on the following rules. + - Leading/Trailing blanks are removed + - If there are single or double quote at both start and end they are removed + - Finally any quote escapes are removed + """ + logger.info("cast_str %s", value) + new_val = value.strip() + if new_val.startswith('"') and new_val.endswith('"'): + new_val = new_val[1:-1] + if new_val.startswith("'") and new_val.endswith("'"): + new_val = new_val[1:-1] + # Replace escaped quotes with normal quotes + new_val = new_val.replace('\\\"', '\"') + new_val = new_val.replace("\\\'", "\'") + logger.info("new_val %s", new_val) + return new_val + def to_message(self, value_type: DataType) -> types_pb2.Datapoint: message = types_pb2.Datapoint() @@ -323,27 +376,6 @@ def set_array_attr(obj, attr, values): array.Clear() array.values.extend(values) - def cast_array_values(cast, array): - array = array.strip('[]') - pattern = r'(?:\\.|[^",])*"(?:\\.|[^"])*"|[^",]+' - values = re.findall(pattern, array) - for item in values: - if item == '': - #skip - pass - else: - if cast == str: - item = item.replace('\"', '').replace('\\', '"').strip() - yield cast(item) - - def cast_bool(value): - if value in ('False', 'false', 'F', 'f'): - value = 0 - return bool(value) - - def cast_str(value): - return str(value).replace('\"', '').replace('\\', '"').strip() - field, set_field, cast_field = { DataType.INT8: ('int32', setattr, int), @@ -356,20 +388,32 @@ def cast_str(value): DataType.INT64: ('int64', setattr, int), DataType.FLOAT: ('float', setattr, float), DataType.DOUBLE: ('double', setattr, float), - DataType.BOOLEAN: ('bool', setattr, cast_bool), - DataType.STRING: ('string', setattr, cast_str), - DataType.INT8_ARRAY: ('int32_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.INT16_ARRAY: ('int32_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.INT32_ARRAY: ('int32_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.UINT8_ARRAY: ('uint32_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.UINT16_ARRAY: ('uint32_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.UINT32_ARRAY: ('uint32_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.UINT64_ARRAY: ('uint64_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.INT64_ARRAY: ('int64_array', set_array_attr, lambda array: cast_array_values(int, array)), - DataType.FLOAT_ARRAY: ('float_array', set_array_attr, lambda array: cast_array_values(float, array)), - DataType.DOUBLE_ARRAY: ('double_array', set_array_attr, lambda array: cast_array_values(float, array)), - DataType.BOOLEAN_ARRAY: ('bool_array', set_array_attr, lambda array: cast_array_values(cast_bool, array)), - DataType.STRING_ARRAY: ('string_array', set_array_attr, lambda array: cast_array_values(str, array)), + DataType.BOOLEAN: ('bool', setattr, Datapoint.cast_bool), + DataType.STRING: ('string', setattr, Datapoint.cast_str), + DataType.INT8_ARRAY: ('int32_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.INT16_ARRAY: ('int32_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.INT32_ARRAY: ('int32_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.UINT8_ARRAY: ('uint32_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.UINT16_ARRAY: ('uint32_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.UINT32_ARRAY: ('uint32_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.UINT64_ARRAY: ('uint64_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.INT64_ARRAY: ('int64_array', set_array_attr, + lambda array: Datapoint.cast_array_values(int, array)), + DataType.FLOAT_ARRAY: ('float_array', set_array_attr, + lambda array: Datapoint.cast_array_values(float, array)), + DataType.DOUBLE_ARRAY: ('double_array', set_array_attr, + lambda array: Datapoint.cast_array_values(float, array)), + DataType.BOOLEAN_ARRAY: ('bool_array', set_array_attr, + lambda array: Datapoint.cast_array_values(Datapoint.cast_bool, array)), + DataType.STRING_ARRAY: ('string_array', set_array_attr, + lambda array: Datapoint.cast_array_values(Datapoint.cast_str, array)), }.get(value_type, (None, None, None)) if self.value is not None: if all((field, set_field, cast_field)): diff --git a/kuksa-client/tests/test_datapoint.py b/kuksa-client/tests/test_datapoint.py new file mode 100644 index 000000000..ac0934d83 --- /dev/null +++ b/kuksa-client/tests/test_datapoint.py @@ -0,0 +1,163 @@ +# /******************************************************************************** +# * Copyright (c) 2023 Contributors to the Eclipse Foundation +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * This program and the accompanying materials are made available under the +# * terms of the Apache License 2.0 which is available at +# * http://www.apache.org/licenses/LICENSE-2.0 +# * +# * SPDX-License-Identifier: Apache-2.0 +# ********************************************************************************/ + +import pytest +from kuksa_client.grpc import Datapoint + +# +# Client rules: +# For simple strings like abd it is optional to quote them ("abc") or not (abc) +# Quotes are needed if you have commas ("ab, c") +# If you have duoble quotes in strings you must escape them + +def test_array_parse_no_quote(): + """ + No need for quotes just because you have a blank + """ + test_str = r'[say hello, abc]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "say hello" + assert my_array[1] == "abc" + +def test_array_parse_no_inside_quote(): + """Quotes are OK""" + test_str = r'["say hello","abc"]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "say hello" + assert my_array[1] == "abc" + +def test_array_parse_no_inside_quote_single(): + """Quotes are OK""" + test_str = "['say hello','abc']" + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "say hello" + assert my_array[1] == "abc" + +def test_array_parse_double_quote(): + test_str = r'["say \"hello\"","abc"]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "say \"hello\"" + assert my_array[1] == "abc" + +def test_array_parse_single_quote(): + test_str = r'[say \'hello\',abc]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "say 'hello'" + assert my_array[1] == "abc" + +def test_array_parse_comma(): + test_str = r'["say, hello","abc"]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == r'say, hello' + assert my_array[1] == "abc" + +def test_array_square(): + """No problem having square brackets as part of strings""" + test_str = r'[say hello[], abc]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "say hello[]" + assert my_array[1] == "abc" + +def test_array_empty_string_quoted(): + test_str = r'["", abc]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "" + assert my_array[1] == "abc" + +def test_array_empty_string_not_quoted(): + """In this case the first item is ignored""" + test_str = r'[, abc]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 1 + assert my_array[0] == "abc" + +def test_double_comma(): + """In this case the middle item is ignored""" + test_str = r'[def,, abc]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 2 + assert my_array[0] == "def" + assert my_array[1] == "abc" + +def test_quotes_in_string_values(): + """Escaped double quotes, so in total 4 items""" + test_str = r'["dtc1, dtc2", dtc3, \" dtc4, dtc4\"]' + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 4 + assert my_array[0] == "dtc1, dtc2" + assert my_array[1] == "dtc3" + assert my_array[2] == "\" dtc4" + assert my_array[3] == "dtc4\"" + +def test_quotes_in_string_values_2(): + """Doubee quotes in double quotes so in total three values""" + test_str = "['dtc1, dtc2', dtc3, \" dtc4, dtc4\"]" + my_array = list(Datapoint.cast_array_values(Datapoint.cast_str,test_str)) + assert len(my_array) == 3 + assert my_array[0] == 'dtc1, dtc2' + assert my_array[1] == "dtc3" + assert my_array[2] == " dtc4, dtc4" + +def test_int_no_quote(): + test_str = r'[123,456]' + my_array = list(Datapoint.cast_array_values(int,test_str)) + assert len(my_array) == 2 + assert my_array[0] == 123 + assert my_array[1] == 456 + +def test_int_quote(): + """Quoting individual int values is not allowed""" + test_str = r'["123","456"]' + with pytest.raises(ValueError): + my_array = list(Datapoint.cast_array_values(int,test_str)) + + +def test_float_no_quote(): + test_str = r'[123,456.23]' + my_array = list(Datapoint.cast_array_values(float,test_str)) + assert len(my_array) == 2 + assert my_array[0] == 123 + assert my_array[1] == 456.23 + +def test_cast_str(): + """Unquoted quotation marks shall be removed, quoted kept without quotes""" + test_str = r'"say hello"' + assert Datapoint.cast_str(test_str) == r'say hello' + test_str = r'"say \"hello\""' + assert Datapoint.cast_str(test_str) == r'say "hello"' + test_str = r'say "hello"' + assert Datapoint.cast_str(test_str) == r'say "hello"' + +def test_cast_bool(): + assert Datapoint.cast_bool("true") is True + assert Datapoint.cast_bool("True") is True + assert Datapoint.cast_bool("T") is True + assert Datapoint.cast_bool("t") is True + assert Datapoint.cast_bool("false") is False + assert Datapoint.cast_bool("False") is False + assert Datapoint.cast_bool("F") is False + assert Datapoint.cast_bool("f") is False + + # And then some other, treated as true for now + assert Datapoint.cast_bool("Ja") is True + assert Datapoint.cast_bool("Nein") is True + assert Datapoint.cast_bool("Doch") is True + diff --git a/kuksa_go_client/client_test.go b/kuksa_go_client/client_test.go new file mode 100644 index 000000000..f4bf95925 --- /dev/null +++ b/kuksa_go_client/client_test.go @@ -0,0 +1,127 @@ + +//******************************************************************************** +// Copyright (c) 2022 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License 2.0 which is available at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ********************************************************************************/ + +package main + +import ( + "testing" + "github.com/eclipse/kuksa.val/kuksa_go_client/kuksa_client" +) + +// Note: Go support two methods to write a string +// +// Using double quotes +// var myvar := "hello \"there\"" +// +// Using back quotes (raw strings) +// var myvar := `hello "there"` +// +// Single quotes only allowed for literals/runes, like 'w' + +func TestArrayParseNoQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`[say hello, abc]`) + if len(array) != 2 { t.Fail()} + if array[0] != "say hello" { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArrayParseNoInsideQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["say hello","abc"]`) + if len(array) != 2 { t.Fail()} + if array[0] != "say hello" { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArrayParseNoInsideQuoteSingle(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`['say hello','abc']`) + if len(array) != 2 { t.Fail()} + if array[0] != "say hello" { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArrayParseDoubleQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["say \"hello\"","abc"]`) + if len(array) != 2 { t.Fail()} + if array[0] != `say "hello"` { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArrayParseSingleQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`[say \'hello\',"abc"]`) + if len(array) != 2 { t.Fail()} + if array[0] != `say 'hello'` { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArrayParseComma(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["say, hello","abc"]`) + if len(array) != 2 { t.Fail()} + if array[0] != `say, hello` { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArraySquare(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`[say hello[], abc]`) + if len(array) != 2 { t.Fail()} + if array[0] != `say hello[]` { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArrayEmptyStringQuoted(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["", abc]`) + if len(array) != 2 { t.Fail()} + if array[0] != `` { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestArrayEmptyStringNotQuoted(t *testing.T) { + // First item shall be ignored + array, _ := kuksa_client.GetArrayFromInput[string](`[, abc]`) + if len(array) != 1 { t.Fail()} + if array[0] != "abc" { t.Fail()} +} + +func TestDoubleComma(t *testing.T) { + // In this case the middle item is ignored + array, _ := kuksa_client.GetArrayFromInput[string](`[def,, abc]`) + if len(array) != 2 { t.Fail()} + if array[0] != "def" { t.Fail()} + if array[1] != "abc" { t.Fail()} +} + +func TestQuotesInStringValues(t *testing.T) { + array, _ := kuksa_client.GetArrayFromInput[string](`["dtc1, dtc2", dtc3, \" dtc4, dtc4\"]`) + if len(array) != 4 { t.Fail()} + if array[0] != "dtc1, dtc2" { t.Fail()} + if array[1] != "dtc3" { t.Fail()} + if array[2] != "\" dtc4" { t.Fail()} + if array[3] != "dtc4\"" { t.Fail()} +} + +func TestQuotesInStringValues2(t *testing.T) { + array, _ := kuksa_client.GetArrayFromInput[string]("['dtc1, dtc2', dtc3, \" dtc4, dtc4\"]") + if len(array) != 3 { t.Fail()} + if array[0] != "dtc1, dtc2" { t.Fail()} + if array[1] != "dtc3" { t.Fail()} + if array[2] != " dtc4, dtc4" { t.Fail()} +} + diff --git a/kuksa_go_client/kuksa_client/grpc.go b/kuksa_go_client/kuksa_client/grpc.go index 9c4f0376f..1c2980540 100644 --- a/kuksa_go_client/kuksa_client/grpc.go +++ b/kuksa_go_client/kuksa_client/grpc.go @@ -72,34 +72,54 @@ func (cg *KuksaClientCommGrpc) GetValueFromKuksaVal(path string, attr string) ([ } respEntries := resp.GetEntries() response := make([]interface{}, len(respEntries)) - for i, entry := range respEntries { - response[i] = interface{}(entry) - } + for i, entry := range respEntries { + response[i] = interface{}(entry) + } return response, nil } -func getArrayFromInput[T any](input string) ([]T, error) { +func GetArrayFromInput[T any](input string) ([]T, error) { // Strip the brackets from the input + + log.Println("String is " + input) input = strings.TrimSuffix(strings.TrimPrefix(input, "["), "]") + log.Println("String is now " + input) // Split the input string into separate values - pattern := `(?:\\.|[^",])*"(?:\\.|[^"])*"|'(?:\\.|[^']|\\')*'|[^",]+` + // First alternative, not quotes including escaped double quote, ends at comma, single/double quote or EOL + // Second group is double quoted string, may contain double quoted strings inside, ends at non-escaped + // double quote + pattern := `(?:\\"|\\'|[^"',])+|"(?:\\"|[^"])*"|'(?:\\'|[^'])*'` r := regexp.MustCompile(pattern) values := r.FindAllString(input, -1) - + // Parse each value as type T and append to the array - array := make([]T, len(values)) - for i, v := range values { - // make the ' disappear - v = strings.ReplaceAll(v,"'", "") - value, err := parseValue[T](v) - if err != nil { - return nil, ParseError{} + array := make([]T, 0) + for _, v := range values { + + log.Println("value is " + v) + v = strings.TrimSpace(v) + var consider = len(v) > 0 + // make the ' or " disappear + if len(v) > 1 && (v[0] == '"') && (v[len(v)-1] == '"') { + v = v[1:len(v)-1] + } + if len(v) > 1 && (v[0] == '\'') && (v[len(v)-1] == '\'') { + v = v[1:len(v)-1] + } + v = strings.ReplaceAll(v,"\\\"", "\"") + v = strings.ReplaceAll(v,"\\'", "'") + log.Println("value2 is " + v + " len is ", len(v)) + if consider { + value, err := parseValue[T](v) + if err != nil { + return nil, ParseError{} + } + array = append(array,value) } - array[i] = value } - + return array, nil } @@ -164,7 +184,7 @@ func parseValue[T any](value string) (T, error) { return v, nil case reflect.String: v := reflect.New(t).Elem().Interface().(T) - reflect.ValueOf(&v).Elem().SetString(strings.TrimSpace(value)) + reflect.ValueOf(&v).Elem().SetString(value) return v, nil default: return reflect.Zero(t).Interface().(T), errors.New("unsupported type") @@ -265,73 +285,73 @@ func (cg *KuksaClientCommGrpc) SetValueFromKuksaVal(path string, value string, a } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Double{Double: float_}} case 20: - array, err := getArrayFromInput[string](value) + array, err := GetArrayFromInput[string](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_StringArray{StringArray: &grpcpb.StringArray{Values: array}}} case 21: - array, err := getArrayFromInput[bool](value) + array, err := GetArrayFromInput[bool](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_BoolArray{BoolArray: &grpcpb.BoolArray{Values: array}}} case 22: - array, err := getArrayFromInput[int32](value) + array, err := GetArrayFromInput[int32](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32Array{Int32Array: &grpcpb.Int32Array{Values: array}}} case 23: - array, err := getArrayFromInput[int32](value) + array, err := GetArrayFromInput[int32](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32Array{Int32Array: &grpcpb.Int32Array{Values: array}}} case 24: - array, err := getArrayFromInput[int32](value) + array, err := GetArrayFromInput[int32](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32Array{Int32Array: &grpcpb.Int32Array{Values: array}}} case 25: - array, err := getArrayFromInput[int64](value) + array, err := GetArrayFromInput[int64](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int64Array{Int64Array: &grpcpb.Int64Array{Values: array}}} case 26: - array, err := getArrayFromInput[uint32](value) + array, err := GetArrayFromInput[uint32](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32Array{Uint32Array: &grpcpb.Uint32Array{Values: array}}} case 27: - array, err := getArrayFromInput[uint32](value) + array, err := GetArrayFromInput[uint32](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32Array{Uint32Array: &grpcpb.Uint32Array{Values: array}}} case 28: - array, err := getArrayFromInput[uint32](value) + array, err := GetArrayFromInput[uint32](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32Array{Uint32Array: &grpcpb.Uint32Array{Values: array}}} case 29: - array, err := getArrayFromInput[uint64](value) + array, err := GetArrayFromInput[uint64](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint64Array{Uint64Array: &grpcpb.Uint64Array{Values: array}}} case 30: - array, err := getArrayFromInput[float32](value) + array, err := GetArrayFromInput[float32](value) if err != nil { return err } datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_FloatArray{FloatArray: &grpcpb.FloatArray{Values: array}}} case 31: - array, err := getArrayFromInput[float64](value) + array, err := GetArrayFromInput[float64](value) if err != nil { return err } @@ -426,16 +446,16 @@ func (cg *KuksaClientCommGrpc) AuthorizeKuksaValConn(TokenOrTokenfile string) er log.Printf("Using token: %s", tokenString) info, err := os.Stat(tokenString) - if err != nil { - // the TokenOrTokenfile is read like its a token - } else if info.Mode().IsRegular() { - tokenByteString, err := os.ReadFile(tokenString) + if err != nil { + // the TokenOrTokenfile is read like its a token + } else if info.Mode().IsRegular() { + tokenByteString, err := os.ReadFile(tokenString) tokenString = string(tokenByteString) if err != nil { log.Fatal("Error reading token: ", err) return err } - } + } cg.authorizationHeader = "Bearer " + tokenString return nil } diff --git a/kuksa_go_client/main.go b/kuksa_go_client/main.go index 3c7cb8654..e34b684a9 100644 --- a/kuksa_go_client/main.go +++ b/kuksa_go_client/main.go @@ -106,11 +106,17 @@ if err != nil { } // set string with "" and \" - err = backend.SetValueFromKuksaVal("Vehicle.OBD.DTCList", "['dtc1, dtc2', dtc2, \"dtc3, dtc3\"]", "value") + // Expected result is 4 items in the list + // dtc1, dtc2 + // dtc2 + // dtc3, dtc3 + // dtc4 + var valstr = "['dtc1, dtc2', dtc2, \"dtc3, dtc3\", dtc4]" + err = backend.SetValueFromKuksaVal("Vehicle.OBD.DTCList", valstr, "value") if err != nil { log.Printf("Set Value Error: %v", err) } else { - log.Printf("Vehicle.OBD.DTCList Set: ['dtc1, dtc2', dtc2, \"dtc3, dtc3\"]") + log.Printf("Vehicle.OBD.DTCList Set: " + valstr) } values, err = backend.GetValueFromKuksaVal("Vehicle.OBD.DTCList", "value")