diff --git a/clickhouse-migrations/20240304143248_init.sql b/clickhouse-migrations/20240304143248_init.sql new file mode 100644 index 0000000..9466a21 --- /dev/null +++ b/clickhouse-migrations/20240304143248_init.sql @@ -0,0 +1,51 @@ +-- +goose Up +-- +goose StatementBegin +SELECT 'up SQL query'; +CREATE TABLE IF NOT EXISTS vss ( + Vehicle_Chassis_Axle_Row1_Wheel_Left_Tire_Pressure UInt16 COMMENT 'Tire pressure in kilo-Pascal.', + Vehicle_Chassis_Axle_Row1_Wheel_Right_Tire_Pressure UInt16 COMMENT 'Tire pressure in kilo-Pascal.', + Vehicle_Chassis_Axle_Row2_Wheel_Left_Tire_Pressure UInt16 COMMENT 'Tire pressure in kilo-Pascal.', + Vehicle_Chassis_Axle_Row2_Wheel_Right_Tire_Pressure UInt16 COMMENT 'Tire pressure in kilo-Pascal.', + Vehicle_CurrentLocation_Altitude Float64 COMMENT 'Current altitude relative to WGS 84 reference ellipsoid, as measured at the position of GNSS receiver antenna.', + Vehicle_CurrentLocation_Latitude Float64 COMMENT 'Current latitude of vehicle in WGS 84 geodetic coordinates, as measured at the position of GNSS receiver antenna.', + Vehicle_CurrentLocation_Longitude Float64 COMMENT 'Current longitude of vehicle in WGS 84 geodetic coordinates, as measured at the position of GNSS receiver antenna.', + Vehicle_CurrentLocation_Timestamp DateTime COMMENT 'Timestamp from GNSS system for current location, formatted according to ISO 8601 with UTC time zone.', + Vehicle_DIMO_DefinitionID String COMMENT 'ID for the vehicles definition', + Vehicle_DIMO_Source String COMMENT 'where the data was sourced from', + Vehicle_DIMO_Subject String COMMENT 'subjet of this vehicle data', + Vehicle_DIMO_Timestamp DateTime COMMENT 'timestamp of when this data was colllected', + Vehicle_DIMO_Type String COMMENT 'type of data collected', + Vehicle_DIMO_VehicleID String COMMENT 'unque DIMO ID for the vehicle', + Vehicle_Exterior_AirTemperature Float32 COMMENT 'Air temperature outside the vehicle.', + Vehicle_LowVoltageBattery_CurrentVoltage Float32 COMMENT 'Current Voltage of the low voltage battery.', + Vehicle_OBD_BarometricPressure Float32 COMMENT 'PID 33 - Barometric pressure', + Vehicle_OBD_EngineLoad Float32 COMMENT 'PID 04 - Engine load in percent - 0 = no load, 100 = full load', + Vehicle_OBD_IntakeTemp Float32 COMMENT 'PID 0F - Intake temperature', + Vehicle_OBD_RunTime Float32 COMMENT 'PID 1F - Engine run time', + Vehicle_Powertrain_CombustionEngine_ECT Int16 COMMENT 'Engine coolant temperature.', + Vehicle_Powertrain_CombustionEngine_EngineOilLevel String COMMENT 'Engine oil level.', + Vehicle_Powertrain_CombustionEngine_Speed UInt16 COMMENT 'Engine speed measured as rotations per minute.', + Vehicle_Powertrain_CombustionEngine_TPS UInt8 COMMENT 'Current throttle position.', + Vehicle_Powertrain_FuelSystem_AbsoluteLevel Float32 COMMENT 'Current available fuel in the fuel tank expressed in liters.', + Vehicle_Powertrain_FuelSystem_SupportedFuelTypes Array(String) COMMENT 'High level information of fuel types supported', + Vehicle_Powertrain_Range UInt32 COMMENT 'Remaining range in meters using all energy sources available in the vehicle.', + Vehicle_Powertrain_TractionBattery_Charging_ChargeLimit UInt8 COMMENT 'Target charge limit (state of charge) for battery.', + Vehicle_Powertrain_TractionBattery_Charging_IsCharging Bool COMMENT 'True if charging is ongoing. Charging is considered to be ongoing if energy is flowing from charger to vehicle.', + Vehicle_Powertrain_TractionBattery_GrossCapacity UInt16 COMMENT 'Gross capacity of the battery.', + Vehicle_Powertrain_TractionBattery_StateOfCharge_Current Float32 COMMENT 'Physical state of charge of the high voltage battery, relative to net capacity. This is not necessarily the state of charge being displayed to the customer.', + Vehicle_Powertrain_Transmission_TravelledDistance Float32 COMMENT 'Odometer reading, total distance travelled during the lifetime of the transmission.', + Vehicle_Speed Float32 COMMENT 'Vehicle speed.', + Vehicle_VehicleIdentification_Brand String COMMENT 'Vehicle brand or manufacturer.', + Vehicle_VehicleIdentification_Model String COMMENT 'Vehicle model.', + Vehicle_VehicleIdentification_VIN String COMMENT '17-character Vehicle Identification Number (VIN) as defined by ISO 3779.', + Vehicle_VehicleIdentification_Year UInt16 COMMENT 'Model year of the vehicle.', +) +ENGINE = MergeTree() +ORDER BY (Vehicle_DIMO_Subject, Vehicle_DIMO_Timestamp); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +SELECT 'down SQL query'; +Drop TABLE IF EXISTS vss; +-- +goose StatementEnd diff --git a/cmd/codegen/codegen.go b/cmd/codegen/codegen.go index e55b93c..eb707d7 100644 --- a/cmd/codegen/codegen.go +++ b/cmd/codegen/codegen.go @@ -16,6 +16,7 @@ func main() { vspecPath := flag.String("spec", "./vspec.csv", "Path to the vspec CSV file") migrationPath := flag.String("migrations", "./migrations.json", "Path to the migrations JSON file") packageName := flag.String("package", "vspec", "Name of the package to generate") + withTest := flag.Bool("convert.with-test", true, "Generate test functions for conversion functions. Default is true.") flag.Parse() err := codegen.EnsureDir(*outputDir) @@ -40,7 +41,7 @@ func main() { log.Fatalf("failed to generate ClickHouse file: %v", err) } - err = convert.Generate(&tmplData, *outputDir) + err = convert.Generate(&tmplData, *outputDir, *withTest) if err != nil { log.Fatalf("failed to generate convert file: %v", err) } diff --git a/go.mod b/go.mod index 6f6e3c5..4532fee 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,11 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect golang.org/x/mod v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c95b037..d0a5be9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -8,3 +14,6 @@ golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/codegen/convert/convert.go b/internal/codegen/convert/convert.go index 8c98d43..4f6fd17 100644 --- a/internal/codegen/convert/convert.go +++ b/internal/codegen/convert/convert.go @@ -1,3 +1,4 @@ +// Package convert provides a function to generate conversion functions for a vehicle struct. package convert import ( @@ -16,10 +17,13 @@ import ( ) //go:embed convert.tmpl -var convertTemplate string +var convertTemplateStr string //go:embed convertFunc.tmpl -var convertFuncTemplate string +var convertFuncTemplateStr string + +//go:embed convertTestFunc.tmpl +var convertTestsFuncTemplateStr string const header = `package %s @@ -28,9 +32,14 @@ const header = `package %s // Code generated by model-garage. ` +type funcTmplData struct { + PackageName string + Signal *codegen.SignalInfo +} + // Generate creates a conversion functions for each field of a vehicle struct. // as well as the entire vehicle struct. -func Generate(tmplData *codegen.TemplateData, outputDir string) (err error) { +func Generate(tmplData *codegen.TemplateData, outputDir string, withTest bool) (err error) { goTemplate, err := createGoTemplate() if err != nil { return err @@ -39,6 +48,10 @@ func Generate(tmplData *codegen.TemplateData, outputDir string) (err error) { if err != nil { return err } + convertTestFuncTemplate, err := createConvertTestFuncTemplate(tmplData.PackageName) + if err != nil { + return err + } // execute the struct template var outBuf bytes.Buffer @@ -60,45 +73,42 @@ func Generate(tmplData *codegen.TemplateData, outputDir string) (err error) { // get a list of SignalInfos that need new convert functions var needsConvertFunc []*codegen.SignalInfo + var needsConvertTestFunc []*codegen.SignalInfo for _, signal := range tmplData.DataSignals { - if signal.Conversion != nil && !existingFuncs[convertName(signal)] { - needsConvertFunc = append(needsConvertFunc, signal) + if signal.Conversion != nil { + if !existingFuncs[convertName(signal)] { + needsConvertFunc = append(needsConvertFunc, signal) + } + if withTest && !existingFuncs[convertTestName(signal)] { + needsConvertTestFunc = append(needsConvertTestFunc, signal) + } } } - if len(needsConvertFunc) == 0 { - return nil - } - - // check if we need to create convertFunc file - convertFuncPath := filepath.Join(outputDir, codegen.ConvertFuncFileName) - err = ensureFuncFile(convertFuncPath, tmplData.PackageName) - if err != nil { - return err - } - - convertData, err := os.ReadFile(convertFuncPath) - if err != nil { - return fmt.Errorf("error reading convertFunc file: %w", err) - } - convertBuff := bytes.NewBuffer(convertData) - for _, signal := range needsConvertFunc { - if err := convertFuncTemplate.Execute(convertBuff, signal); err != nil { - return fmt.Errorf("error executing convertFunc template: %w", err) + if len(needsConvertFunc) != 0 { + filePath := filepath.Join(outputDir, codegen.ConvertFuncFileName) + err = createConvertFuncs(needsConvertFunc, convertFuncTemplate, filePath, tmplData.PackageName) + if err != nil { + return err } } - err = codegen.FormatAndWriteToFile(convertBuff.Bytes(), convertFuncPath) - if err != nil { - return fmt.Errorf("error formatting and writing to file: %w", err) + if len(needsConvertTestFunc) != 0 { + ext := filepath.Ext(codegen.ConvertFuncFileName) + testFileName := strings.TrimSuffix(codegen.ConvertFuncFileName, ext) + "_test.go" + filePath := filepath.Join(outputDir, testFileName) + packageName := tmplData.PackageName + "_test" + err = createConvertFuncs(needsConvertTestFunc, convertTestFuncTemplate, filePath, packageName) + if err != nil { + return err + } } - return nil } func createGoTemplate() (*template.Template, error) { tmpl, err := template.New("convertTemplate").Funcs(template.FuncMap{ "convertName": convertName, - }).Parse(convertTemplate) + }).Parse(convertTemplateStr) if err != nil { return nil, fmt.Errorf("error parsing go struct template: %w", err) } @@ -108,7 +118,18 @@ func createGoTemplate() (*template.Template, error) { func createConvertFuncTemplate() (*template.Template, error) { tmpl, err := template.New("convertFuncTemplate").Funcs(template.FuncMap{ "convertName": convertName, - }).Parse(convertFuncTemplate) + }).Parse(convertFuncTemplateStr) + if err != nil { + return nil, fmt.Errorf("error parsing go struct template: %w", err) + } + return tmpl, nil +} + +func createConvertTestFuncTemplate(packageNameToTest string) (*template.Template, error) { + tmpl, err := template.New("convertTestFuncTemplate").Funcs(template.FuncMap{ + "convertName": func(sig *codegen.SignalInfo) string { return fmt.Sprintf("%s.%s", packageNameToTest, convertName(sig)) }, + "convertTestName": convertTestName, + }).Parse(convertTestsFuncTemplateStr) if err != nil { return nil, fmt.Errorf("error parsing go struct template: %w", err) } @@ -121,7 +142,7 @@ func getDeclaredFunctions(outputPath string) (map[string]bool, error) { list, err := os.ReadDir(outputPath) if err != nil { - return nil, err + return nil, fmt.Errorf("error reading directory: %w", err) } for _, d := range list { @@ -140,7 +161,6 @@ func getDeclaredFunctions(outputPath string) (map[string]bool, error) { } declaredFunctions[fn.Name.Name] = true } - } return declaredFunctions, nil @@ -150,6 +170,10 @@ func convertName(signal *codegen.SignalInfo) string { return "To" + signal.GOName } +func convertTestName(signal *codegen.SignalInfo) string { + return "Test" + convertName(signal) +} + // ensureFuncFile checks if the convertFunc file exists and creates it if it does not. // It also writes the package header to the file if it is created. func ensureFuncFile(convertFuncPath string, packageName string) error { @@ -177,3 +201,31 @@ func ensureFuncFile(convertFuncPath string, packageName string) error { return nil } + +func createConvertFuncs(needsConvertFunc []*codegen.SignalInfo, tmpl *template.Template, outputPath string, packageName string) error { + // check if we need to create convertFunc file + err := ensureFuncFile(outputPath, packageName) + if err != nil { + return err + } + + convertData, err := os.ReadFile(outputPath) + if err != nil { + return fmt.Errorf("error reading convertFunc file: %w", err) + } + convertBuff := bytes.NewBuffer(convertData) + for _, signal := range needsConvertFunc { + data := funcTmplData{ + PackageName: packageName, + Signal: signal, + } + if err := tmpl.Execute(convertBuff, &data); err != nil { + return fmt.Errorf("error executing convertFunc template: %w", err) + } + } + err = codegen.FormatAndWriteToFile(convertBuff.Bytes(), outputPath) + if err != nil { + return fmt.Errorf("error formatting and writing to file: %w", err) + } + return nil +} diff --git a/internal/codegen/convert/convertFunc.tmpl b/internal/codegen/convert/convertFunc.tmpl index 3b04990..b85fc75 100644 --- a/internal/codegen/convert/convertFunc.tmpl +++ b/internal/codegen/convert/convertFunc.tmpl @@ -1,7 +1,7 @@ -// {{ convertName . }} converts data as {{ .Conversion.OriginalType }} to {{ .GOType }} -func {{ convertName . }}(val {{ .Conversion.OriginalType }}) ({{ .GOType }}, error) { - {{ if eq .Conversion.OriginalType .GOType -}} +// {{ convertName .Signal }} converts data as {{ .Signal.Conversion.OriginalType }} to {{ .Signal.GOType }} +func {{ convertName .Signal }}(val {{ .Signal.Conversion.OriginalType }}) ({{ .Signal.GOType }}, error) { + {{ if eq .Signal.Conversion.OriginalType .Signal.GOType -}} return val, nil {{- else -}} panic("not implemented") diff --git a/internal/convert/convert.go b/internal/convert/convert.go index d4ea862..fbe269c 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -10,6 +10,9 @@ func Float64Tofloat32(val float64) float32 { if val > math.MaxFloat32 { return math.MaxFloat32 } + if val < math.SmallestNonzeroFloat32 { + return math.SmallestNonzeroFloat32 + } return float32(val) } diff --git a/pkg/vss/convert_test.go b/pkg/vss/convert_test.go new file mode 100644 index 0000000..81d86a4 --- /dev/null +++ b/pkg/vss/convert_test.go @@ -0,0 +1,98 @@ +package vss_test + +import ( + "testing" + "time" + + "github.com/KevinJoiner/model-garage/pkg/vss" + "github.com/stretchr/testify/require" +) + +func TestFullFromDataConversion(t *testing.T) { + vehicle, err := vss.FromData([]byte(fullInputJSON), false) + require.NoErrorf(t, err, "error converting full input data: %v", err) + require.Equalf(t, fullVehicle, vehicle, "converted vehicle does not match expected vehicle") +} + +var fullInputJSON = `{ + "data": { + "tires": { + "frontLeft": 30.5, + "frontRight": 31.0, + "backLeft": 32.2, + "backRight": 33.1 + }, + "altitude": 100.0, + "latitude": 37.7749, + "longitude": -122.4194, + "timestamp": "2022-01-01T12:34:56Z", + "definitionID": "123", + "vehicleID": "456", + "ambientTemp": 25.0, + "batteryVoltage": 12.5, + "barometricPressure": 1013.25, + "engineLoad": 75.0, + "intakeTemp": 30.0, + "runTime": 1200.0, + "coolantTemp": 90.0, + "oil": 0.10, + "engineSpeed": 3000.0, + "throttlePosition": 50.0, + "fuelPercentRemaining": 60.0, + "fuelType": "Gasoline", + "range": 300.0, + "chargeLimit": 80.0, + "charging": true, + "batteryCapacity": 60.0, + "soc": 70.0, + "odometer": 50000.0, + "speed": 60.0, + "make": "Toyota", + "model": "Camry", + "year": 2020 + "vin": "1234567890" + }, + "source": "SensorXYZ", + "subject": "Vehicle123", + "time": "2022-01-01T12:34:56Z", + "type": "DIMO" + }` +var fullVehicle = &vss.Vehicle{ + VehicleChassisAxleRow1WheelLeftTirePressure: 30, + VehicleChassisAxleRow1WheelRightTirePressure: 31, + VehicleChassisAxleRow2WheelLeftTirePressure: 32, + VehicleChassisAxleRow2WheelRightTirePressure: 33, + VehicleCurrentLocationAltitude: 100.0, + VehicleCurrentLocationLatitude: 37.7749, + VehicleCurrentLocationLongitude: -122.4194, + VehicleCurrentLocationTimestamp: time.Date(2022, 1, 1, 12, 34, 56, 0, time.UTC), + VehiclePowertrainCombustionEngineECT: 90.0, + VehiclePowertrainCombustionEngineEngineOilLevel: "CRITICALLY_LOW", + VehiclePowertrainCombustionEngineSpeed: 3000.0, + VehiclePowertrainCombustionEngineTPS: 50.0, + VehiclePowertrainFuelSystemAbsoluteLevel: 60.0, + VehiclePowertrainFuelSystemSupportedFuelTypes: []string{"Gasoline"}, + VehiclePowertrainRange: 300.0, + VehiclePowertrainTractionBatteryChargingChargeLimit: 80.0, + VehiclePowertrainTractionBatteryChargingIsCharging: true, + VehiclePowertrainTractionBatteryGrossCapacity: 60.0, + VehiclePowertrainTractionBatteryStateOfChargeCurrent: 70.0, + VehiclePowertrainTransmissionTravelledDistance: 50000.0, + VehicleSpeed: 60.0, + VehicleVehicleIdentificationBrand: "Toyota", + VehicleVehicleIdentificationModel: "Camry", + VehicleVehicleIdentificationYear: 2020, + VehicleVehicleIdentificationVIN: "1234567890", + VehicleDIMODefinitionID: "123", + VehicleDIMOSource: "SensorXYZ", + VehicleDIMOSubject: "Vehicle123", + VehicleDIMOTimestamp: time.Date(2022, 1, 1, 12, 34, 56, 0, time.UTC), + VehicleDIMOType: "DIMO", + VehicleDIMOVehicleID: "456", + VehicleExteriorAirTemperature: 25.0, + VehicleLowVoltageBatteryCurrentVoltage: 12.5, + VehicleOBDBarometricPressure: 1013.25, + VehicleOBDEngineLoad: 75.0, + VehicleOBDIntakeTemp: 30.0, + VehicleOBDRunTime: 1200.0, +} diff --git a/pkg/vss/vss-convert-funcs.go b/pkg/vss/vss-convert-funcs.go index 4bf4b88..00b0a7c 100644 --- a/pkg/vss/vss-convert-funcs.go +++ b/pkg/vss/vss-convert-funcs.go @@ -200,3 +200,8 @@ func ToVehicleVehicleIdentificationModel(val string) (string, error) { func ToVehicleVehicleIdentificationYear(val float64) (uint16, error) { return convert.Float64toUint16(val), nil } + +// ToVehicleVehicleIdentificationVIN converts data as string to string +func ToVehicleVehicleIdentificationVIN(val string) (string, error) { + return val, nil +} diff --git a/pkg/vss/vss-convert-funcs_test.go b/pkg/vss/vss-convert-funcs_test.go new file mode 100644 index 0000000..745ac59 --- /dev/null +++ b/pkg/vss/vss-convert-funcs_test.go @@ -0,0 +1,1564 @@ +package vss_test + +import ( + "fmt" + "math" + "testing" + "time" + + "github.com/KevinJoiner/model-garage/pkg/vss" + "github.com/stretchr/testify/require" +) + +// This file is automatically populated with conversion functions for each field of a vehicle struct. +// any conversion functions already defined in this package will not be generated. +// Code generated by model-garage. + +const ( + passThroughString = "DIMO" + passThroughFloat64 = 123.456 +) + +func TestToVehicleChassisAxleRow1WheelLeftTirePressure(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint16 + expectedError bool + }{ + { + name: "Valid value", + input: 32, + expected: 32, + expectedError: false, + }, + { + name: "Zero value", + input: 0, + expected: 0, + expectedError: false, + }, + { + name: "Negative value", + input: -15.75, + expected: 0, + expectedError: false, + }, + { + name: "Large value", + input: math.MaxFloat64, + expected: math.MaxUint16, + expectedError: false, + }, + { + name: "Decimal above 0.5", + input: 32.9, + expected: 32, + expectedError: false, + }, + { + name: "Decimal below 0.5", + input: 32.1, + expected: 32, + expectedError: false, + }, + } + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleChassisAxleRow1WheelLeftTirePressure(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehicleChassisAxleRow1WheelRightTirePressure(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint16 + expectedError bool + }{ + { + name: "Valid value", + input: 32, + expected: 32, + expectedError: false, + }, + { + name: "Zero value", + input: 0, + expected: 0, + expectedError: false, + }, + { + name: "Negative value", + input: -15.75, + expected: 0, + expectedError: false, + }, + { + name: "Large value", + input: math.MaxFloat64, + expected: math.MaxUint16, + expectedError: false, + }, + { + name: "Decimal above 0.5", + input: 32.9, + expected: 32, + expectedError: false, + }, + { + name: "Decimal below 0.5", + input: 32.1, + expected: 32, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleChassisAxleRow1WheelRightTirePressure(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehicleChassisAxleRow2WheelLeftTirePressure(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint16 + expectedError bool + }{ + { + name: "Valid value", + input: 32, + expected: 32, + expectedError: false, + }, + { + name: "Zero value", + input: 0, + expected: 0, + expectedError: false, + }, + { + name: "Negative value", + input: -15.75, + expected: 0, + expectedError: false, + }, + { + name: "Large value", + input: math.MaxFloat64, + expected: math.MaxUint16, + expectedError: false, + }, + { + name: "Decimal above 0.5", + input: 32.9, + expected: 32, + expectedError: false, + }, + { + name: "Decimal below 0.5", + input: 32.1, + expected: 32, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleChassisAxleRow2WheelLeftTirePressure(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehicleChassisAxleRow2WheelRightTirePressure(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint16 + expectedError bool + }{ + { + name: "Valid value", + input: 32, + expected: 32, + expectedError: false, + }, + { + name: "Zero value", + input: 0, + expected: 0, + expectedError: false, + }, + { + name: "Negative value", + input: -15.75, + expected: 0, + expectedError: false, + }, + { + name: "Large value", + input: math.MaxFloat64, + expected: math.MaxUint16, + expectedError: false, + }, + { + name: "Decimal above 0.5", + input: 32.9, + expected: 32, + expectedError: false, + }, + { + name: "Decimal below 0.5", + input: 32.1, + expected: 32, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleChassisAxleRow2WheelRightTirePressure(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehicleCurrentLocationAltitude(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleCurrentLocationAltitude(passThroughFloat64) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughFloat64, result, "Unexpected result") +} +func TestToVehicleCurrentLocationLatitude(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleCurrentLocationLatitude(passThroughFloat64) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughFloat64, result, "Unexpected result") +} + +func TestToVehicleCurrentLocationLongitude(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleCurrentLocationLongitude(passThroughFloat64) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughFloat64, result, "Unexpected result") +} +func TestToVehicleCurrentLocationTimestamp(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + expected time.Time + expectedError bool + }{ + { + name: "Valid RFC3339 timestamp", + input: "2022-01-15T10:30:00Z", + expected: time.Date(2022, 1, 15, 10, 30, 0, 0, time.UTC), + expectedError: false, + }, + { + name: "Timestamp with timezone", + input: "2024-02-24T12:00:00-08:00", + expected: time.Date(2024, 2, 24, 12, 0, 0, 0, time.FixedZone("", -8*60*60)), + expectedError: false, + }, + { + name: "Invalid timestamp format", + input: "2021/12/25 08:00:00", + expected: time.Time{}, + expectedError: true, + }, + { + name: "Empty string", + input: "", + expected: time.Time{}, + expectedError: true, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleCurrentLocationTimestamp(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehicleDIMODefinitionID(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleDIMODefinitionID(passThroughString) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughString, result, "Unexpected result") +} +func TestToVehicleDIMOSource(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleDIMOSource(passThroughString) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughString, result, "Unexpected result") +} +func TestToVehicleDIMOSubject(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleDIMOSubject(passThroughString) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughString, result, "Unexpected result") +} +func TestToVehicleDIMOTimestamp(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + expected time.Time + expectedError bool + }{ + { + name: "Valid RFC3339 timestamp", + input: "2022-01-15T10:30:00Z", + expected: time.Date(2022, 1, 15, 10, 30, 0, 0, time.UTC), + expectedError: false, + }, + { + name: "Timestamp with timezone", + input: "2024-02-24T12:00:00-08:00", + expected: time.Date(2024, 2, 24, 12, 0, 0, 0, time.FixedZone("", -8*60*60)), + expectedError: false, + }, + { + name: "Invalid timestamp format", + input: "2021/12/25 08:00:00", + expected: time.Time{}, + expectedError: true, + }, + { + name: "Empty string", + input: "", + expected: time.Time{}, + expectedError: true, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleDIMOTimestamp(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehicleDIMOType(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleDIMOType(passThroughString) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughString, result, "Unexpected result") +} + +func TestToVehicleDIMOVehicleID(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleDIMOVehicleID(passThroughString) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughString, result, "Unexpected result") +} +func TestToVehicleExteriorAirTemperature(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleExteriorAirTemperature(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehicleLowVoltageBatteryCurrentVoltage(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleLowVoltageBatteryCurrentVoltage(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehicleOBDBarometricPressure(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleOBDBarometricPressure(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehicleOBDEngineLoad(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleOBDEngineLoad(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehicleOBDIntakeTemp(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleOBDIntakeTemp(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehicleOBDRunTime(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleOBDRunTime(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehiclePowertrainCombustionEngineECT(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected int16 + expectedError bool + }{ + { + name: "Postive Value", + input: 90.5, + expected: 90, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxInt16, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: 0, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainCombustionEngineECT(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehiclePowertrainCombustionEngineEngineOilLevel(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected string + expectedError bool + }{ + { + name: "Critically Low", + input: 0.2, + expected: "CRITICALLY_LOW", + expectedError: false, + }, + { + name: "Low", + input: 0.4, + expected: "LOW", + expectedError: false, + }, + { + name: "Normal", + input: 0.6, + expected: "NORMAL", + expectedError: false, + }, + { + name: "High", + input: 0.9, + expected: "HIGH", + expectedError: false, + }, + { + name: "Critically High", + input: 1.0, + expected: "CRITICALLY_HIGH", + expectedError: false, + }, + { + name: "Above 1.0", + input: 1.1, + expected: "CRITICALLY_HIGH", + expectedError: false, + }, + { + name: "Negative Value", + input: -0.1, + expected: "CRITICALLY_LOW", + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainCombustionEngineEngineOilLevel(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehiclePowertrainCombustionEngineSpeed(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint16 + expectedError bool + }{ + { + name: "Valid Value", + input: 65, + expected: 65, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: 0, + expectedError: false, + }, + { + name: "Max Float64 Value", + input: math.MaxFloat64, + expected: math.MaxUint16, + expectedError: false, + }, + { + name: "Min Float64 Value", + input: math.SmallestNonzeroFloat64, + expected: 0, + expectedError: false, + }, + { + name: "Decimal above 0.5 no rounding", + input: 65.9, + expected: 65, + expectedError: false, + }, + { + name: "Decimal below 0.5 no rounding", + input: 65.1, + expected: 65, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainCombustionEngineSpeed(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehiclePowertrainCombustionEngineTPS(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint8 + expectedError bool + }{ + { + name: "Valid Value", + input: 65, + expected: 65, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: 0, + expectedError: false, + }, + { + name: "Max Float64 Value", + input: math.MaxFloat64, + expected: math.MaxUint8, + expectedError: false, + }, + { + name: "Min Float64 Value", + input: math.SmallestNonzeroFloat64, + expected: 0, + expectedError: false, + }, + { + name: "Decimal above 0.5 no rounding", + input: 65.9, + expected: 65, + expectedError: false, + }, + { + name: "Decimal below 0.5 no rounding", + input: 65.1, + expected: 65, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainCombustionEngineTPS(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehiclePowertrainFuelSystemAbsoluteLevel(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainFuelSystemAbsoluteLevel(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehiclePowertrainFuelSystemSupportedFuelTypes(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + expected []string + expectedError bool + }{ + // TODO: test cases. + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainFuelSystemSupportedFuelTypes(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehiclePowertrainRange(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint32 + expectedError bool + }{ + { + name: "Valid Value", + input: 65, + expected: 65, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: 0, + expectedError: false, + }, + { + name: "Max Float64 Value", + input: math.MaxFloat64, + expected: math.MaxUint32, + expectedError: false, + }, + { + name: "Min Float64 Value", + input: math.SmallestNonzeroFloat64, + expected: 0, + expectedError: false, + }, + { + name: "Decimal above 0.5 no rounding", + input: 65.9, + expected: 65, + expectedError: false, + }, + { + name: "Decimal below 0.5 no rounding", + input: 65.1, + expected: 65, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainRange(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehiclePowertrainTractionBatteryChargingChargeLimit(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint8 + expectedError bool + }{ + { + name: "Valid Value", + input: 65, + expected: 65, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: 0, + expectedError: false, + }, + { + name: "Max Float64 Value", + input: math.MaxFloat64, + expected: math.MaxUint8, + expectedError: false, + }, + { + name: "Min Float64 Value", + input: math.SmallestNonzeroFloat64, + expected: 0, + expectedError: false, + }, + { + name: "Decimal above 0.5 no rounding", + input: 65.9, + expected: 65, + expectedError: false, + }, + { + name: "Decimal below 0.5 no rounding", + input: 65.1, + expected: 65, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainTractionBatteryChargingChargeLimit(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehiclePowertrainTractionBatteryChargingIsCharging(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainTractionBatteryChargingIsCharging(true) + require.NoError(t, err, "Unexpected error") + require.Equal(t, true, result, "Unexpected result") +} +func TestToVehiclePowertrainTractionBatteryGrossCapacity(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint16 + expectedError bool + }{ + // Add test cases. + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainTractionBatteryGrossCapacity(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehiclePowertrainTractionBatteryStateOfChargeCurrent(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainTractionBatteryStateOfChargeCurrent(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehiclePowertrainTransmissionTravelledDistance(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + // Add test cases. + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehiclePowertrainTransmissionTravelledDistance(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehicleSpeed(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected float32 + expectedError bool + }{ + { + name: "Positive Value", + input: 25.5, + expected: 25.5, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: -10.8, + expectedError: false, + }, + { + name: "Value too high", + input: math.MaxFloat64, + expected: math.MaxFloat32, + expectedError: false, + }, + { + name: "Value too low", + input: math.SmallestNonzeroFloat64, + expected: math.SmallestNonzeroFloat32, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0.0, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleSpeed(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.InDeltaf(t, test.expected, result, 1e45, "Unexpected result got %f expected %f", result, test.expected) + } + }) + } +} +func TestToVehicleVehicleIdentificationBrand(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + expected string + expectedError bool + }{ + // Add test cases. + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleVehicleIdentificationBrand(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} +func TestToVehicleVehicleIdentificationModel(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleVehicleIdentificationModel(passThroughString) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughString, result, "Unexpected result") +} +func TestToVehicleVehicleIdentificationVIN(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleVehicleIdentificationVIN(passThroughString) + require.NoError(t, err, "Unexpected error") + require.Equal(t, passThroughString, result, "Unexpected result") +} +func TestToVehicleVehicleIdentificationYear(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input float64 + expected uint16 + expectedError bool + }{ + { + name: "Valid Value", + input: 65, + expected: 65, + expectedError: false, + }, + { + name: "Zero Value", + input: 0.0, + expected: 0, + expectedError: false, + }, + { + name: "Negative Value", + input: -10.8, + expected: 0, + expectedError: false, + }, + { + name: "Max Float64 Value", + input: math.MaxFloat64, + expected: math.MaxUint16, + expectedError: false, + }, + { + name: "Min Float64 Value", + input: math.SmallestNonzeroFloat64, + expected: 0, + expectedError: false, + }, + { + name: "Decimal above 0.5 no rounding", + input: 65.9, + expected: 65, + expectedError: false, + }, + { + name: "Decimal below 0.5 no rounding", + input: 65.1, + expected: 65, + expectedError: false, + }, + } + + for i := range tests { + test := tests[i] + name := test.name + if name == "" { + name = fmt.Sprintf("Input: %v", test.input) + } + t.Run(name, func(t *testing.T) { + t.Parallel() + result, err := vss.ToVehicleVehicleIdentificationYear(test.input) + if test.expectedError { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Unexpected error") + require.Equal(t, test.expected, result, "Unexpected result") + } + }) + } +} diff --git a/pkg/vss/vss-convert.go b/pkg/vss/vss-convert.go index c223632..f0d7e11 100644 --- a/pkg/vss/vss-convert.go +++ b/pkg/vss/vss-convert.go @@ -560,6 +560,21 @@ func FromData(jsonData []byte, skipNotFound bool) (*Vehicle, error) { return nil, fmt.Errorf("%w, field 'data.model'", errNotFound) } + // convert data.vin to VehicleVehicleIdentificationVIN + result = gjson.GetBytes(jsonData, "data.vin") + if result.Exists() { + valVehicleVehicleIdentificationVIN, ok := result.Value().(string) + if !ok { + return nil, fmt.Errorf("%w, field 'data.vin' is not of type string", errInvalidType) + } + vehicle.VehicleVehicleIdentificationVIN, err = ToVehicleVehicleIdentificationVIN(valVehicleVehicleIdentificationVIN) + if err != nil { + return nil, fmt.Errorf("failed to convert 'data.vin': %w", err) + } + } else if !skipNotFound { + return nil, fmt.Errorf("%w, field 'data.vin'", errNotFound) + } + // convert data.year to VehicleVehicleIdentificationYear result = gjson.GetBytes(jsonData, "data.year") if result.Exists() { diff --git a/pkg/vss/vss-structs.go b/pkg/vss/vss-structs.go index 3aa3837..8d7aeb0 100644 --- a/pkg/vss/vss-structs.go +++ b/pkg/vss/vss-structs.go @@ -80,6 +80,8 @@ const ( FieldVehicleVehicleIdentificationBrand = "Vehicle_VehicleIdentification_Brand" // FieldVehicleVehicleIdentificationModel Vehicle model. FieldVehicleVehicleIdentificationModel = "Vehicle_VehicleIdentification_Model" + // FieldVehicleVehicleIdentificationVIN 17-character Vehicle Identification Number (VIN) as defined by ISO 3779. + FieldVehicleVehicleIdentificationVIN = "Vehicle_VehicleIdentification_VIN" // FieldVehicleVehicleIdentificationYear Model year of the vehicle. FieldVehicleVehicleIdentificationYear = "Vehicle_VehicleIdentification_Year" ) @@ -155,11 +157,13 @@ type Vehicle struct { VehicleVehicleIdentificationBrand string `ch:"Vehicle_VehicleIdentification_Brand" json:"Vehicle_VehicleIdentification_Brand,omitempty"` // VehicleVehicleIdentificationModel Vehicle model. VehicleVehicleIdentificationModel string `ch:"Vehicle_VehicleIdentification_Model" json:"Vehicle_VehicleIdentification_Model,omitempty"` + // VehicleVehicleIdentificationVIN 17-character Vehicle Identification Number (VIN) as defined by ISO 3779. + VehicleVehicleIdentificationVIN string `ch:"Vehicle_VehicleIdentification_VIN" json:"Vehicle_VehicleIdentification_VIN,omitempty"` // VehicleVehicleIdentificationYear Model year of the vehicle. VehicleVehicleIdentificationYear uint16 `ch:"Vehicle_VehicleIdentification_Year" json:"Vehicle_VehicleIdentification_Year,omitempty"` } // InsertStatement for inserting a vehicle and all its fields into the table. -const InsertStatment = "INSERT INTO vss (Vehicle_Chassis_Axle_Row1_Wheel_Left_Tire_Pressure, Vehicle_Chassis_Axle_Row1_Wheel_Right_Tire_Pressure, Vehicle_Chassis_Axle_Row2_Wheel_Left_Tire_Pressure, Vehicle_Chassis_Axle_Row2_Wheel_Right_Tire_Pressure, Vehicle_CurrentLocation_Altitude, Vehicle_CurrentLocation_Latitude, Vehicle_CurrentLocation_Longitude, Vehicle_CurrentLocation_Timestamp, Vehicle_DIMO_DefinitionID, Vehicle_DIMO_Source, Vehicle_DIMO_Subject, Vehicle_DIMO_Timestamp, Vehicle_DIMO_Type, Vehicle_DIMO_VehicleID, Vehicle_Exterior_AirTemperature, Vehicle_LowVoltageBattery_CurrentVoltage, Vehicle_OBD_BarometricPressure, Vehicle_OBD_EngineLoad, Vehicle_OBD_IntakeTemp, Vehicle_OBD_RunTime, Vehicle_Powertrain_CombustionEngine_ECT, Vehicle_Powertrain_CombustionEngine_EngineOilLevel, Vehicle_Powertrain_CombustionEngine_Speed, Vehicle_Powertrain_CombustionEngine_TPS, Vehicle_Powertrain_FuelSystem_AbsoluteLevel, Vehicle_Powertrain_FuelSystem_SupportedFuelTypes, Vehicle_Powertrain_Range, Vehicle_Powertrain_TractionBattery_Charging_ChargeLimit, Vehicle_Powertrain_TractionBattery_Charging_IsCharging, Vehicle_Powertrain_TractionBattery_GrossCapacity, Vehicle_Powertrain_TractionBattery_StateOfCharge_Current, Vehicle_Powertrain_Transmission_TravelledDistance, Vehicle_Speed, Vehicle_VehicleIdentification_Brand, Vehicle_VehicleIdentification_Model, Vehicle_VehicleIdentification_Year)" +const InsertStatment = "INSERT INTO vss (Vehicle_Chassis_Axle_Row1_Wheel_Left_Tire_Pressure, Vehicle_Chassis_Axle_Row1_Wheel_Right_Tire_Pressure, Vehicle_Chassis_Axle_Row2_Wheel_Left_Tire_Pressure, Vehicle_Chassis_Axle_Row2_Wheel_Right_Tire_Pressure, Vehicle_CurrentLocation_Altitude, Vehicle_CurrentLocation_Latitude, Vehicle_CurrentLocation_Longitude, Vehicle_CurrentLocation_Timestamp, Vehicle_DIMO_DefinitionID, Vehicle_DIMO_Source, Vehicle_DIMO_Subject, Vehicle_DIMO_Timestamp, Vehicle_DIMO_Type, Vehicle_DIMO_VehicleID, Vehicle_Exterior_AirTemperature, Vehicle_LowVoltageBattery_CurrentVoltage, Vehicle_OBD_BarometricPressure, Vehicle_OBD_EngineLoad, Vehicle_OBD_IntakeTemp, Vehicle_OBD_RunTime, Vehicle_Powertrain_CombustionEngine_ECT, Vehicle_Powertrain_CombustionEngine_EngineOilLevel, Vehicle_Powertrain_CombustionEngine_Speed, Vehicle_Powertrain_CombustionEngine_TPS, Vehicle_Powertrain_FuelSystem_AbsoluteLevel, Vehicle_Powertrain_FuelSystem_SupportedFuelTypes, Vehicle_Powertrain_Range, Vehicle_Powertrain_TractionBattery_Charging_ChargeLimit, Vehicle_Powertrain_TractionBattery_Charging_IsCharging, Vehicle_Powertrain_TractionBattery_GrossCapacity, Vehicle_Powertrain_TractionBattery_StateOfCharge_Current, Vehicle_Powertrain_Transmission_TravelledDistance, Vehicle_Speed, Vehicle_VehicleIdentification_Brand, Vehicle_VehicleIdentification_Model, Vehicle_VehicleIdentification_VIN, Vehicle_VehicleIdentification_Year)" const InsertStatmentValues = InsertStatment + " Values" diff --git a/pkg/vss/vss-table.sql b/pkg/vss/vss-table.sql index ba81063..51f71d2 100644 --- a/pkg/vss/vss-table.sql +++ b/pkg/vss/vss-table.sql @@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS vss ( Vehicle_Speed Float32 COMMENT 'Vehicle speed.', Vehicle_VehicleIdentification_Brand String COMMENT 'Vehicle brand or manufacturer.', Vehicle_VehicleIdentification_Model String COMMENT 'Vehicle model.', + Vehicle_VehicleIdentification_VIN String COMMENT '17-character Vehicle Identification Number (VIN) as defined by ISO 3779.', Vehicle_VehicleIdentification_Year UInt16 COMMENT 'Model year of the vehicle.', ) ENGINE = MergeTree() diff --git a/schema/migrations.json b/schema/migrations.json index 3f1bda6..5bd0e1d 100644 --- a/schema/migrations.json +++ b/schema/migrations.json @@ -22,7 +22,7 @@ } }, { - "vspecName": "", + "vspecName": "Vehicle.VehicleIdentification.VIN", "conversion": { "originalName": "data.vin", "originalType": "string"