Add more robust value comparison
This commit is contained in:
@@ -36,9 +36,127 @@ type StateValue struct {
|
||||
marshal ValueSerializer
|
||||
}
|
||||
|
||||
// IsNumeric checks whether a given StateValue model represents a numeric value or
|
||||
// not. Numeric types are defined as those that can be used to store number-like
|
||||
// values. The following types are considered numeric: UI1, UI2, UI4, I1, I2,
|
||||
// I4, Int, R4, R8, Number and Fixed14_4.
|
||||
//
|
||||
// t: StateVarType to check if it's a numeric type or not.
|
||||
//
|
||||
// Returns true if the given StateValue represents a numeric value; false
|
||||
// otherwise.
|
||||
func (sv StateValue) IsNumeric() bool {
|
||||
return sv.valueType.IsNumeric()
|
||||
}
|
||||
|
||||
// BitSize returns the number of bits that will be used to represent values
|
||||
// when this type is used in an UPnP State Variable. The returned value can be
|
||||
// either 8, 16, 24, 32, or 64, depending on whether t is Byte, Boolean, I2, Ui2,
|
||||
// I4, Ui4 respectively. If none of these types match, it will return -1.
|
||||
func (sv StateValue) BitSize() int {
|
||||
return sv.valueType.BitSize()
|
||||
}
|
||||
|
||||
// IsInteger checks if the state variable type is integer or not.
|
||||
//
|
||||
// It returns a boolean value indicating whether the provided StateValue
|
||||
// is an integer type or not.
|
||||
//
|
||||
// Returns: bool: If the state variable type is any of the defined integer types
|
||||
// (UI1, UI2, UI4, I1, I2, I4, Int), it returns true. Otherwise, it returns false.
|
||||
func (sv StateValue) IsInteger() bool {
|
||||
return sv.valueType.IsInteger()
|
||||
}
|
||||
|
||||
// IsUnsignedInt checks if the state value type represents an unsigned integer.
|
||||
// The method returns a boolean indicating whether the state value type is an
|
||||
// unsigned integer.
|
||||
func (sv StateValue) IsUnsignedInt() bool {
|
||||
return sv.valueType.IsUnsignedInt()
|
||||
}
|
||||
|
||||
// IsSignedInt checks if the state value type is a signed int.
|
||||
//
|
||||
// The return value will be a boolean indicating whether the state value type is
|
||||
// a signed integer (true) or not (false).
|
||||
func (sv StateValue) IsSignedInt() bool {
|
||||
return sv.valueType.IsSignedInt()
|
||||
}
|
||||
|
||||
// IsFloat returns true if the StateValue's value type represents a floating point
|
||||
// number; false otherwise.
|
||||
func (sv StateValue) IsFloat() bool {
|
||||
return sv.valueType.IsFloat()
|
||||
}
|
||||
|
||||
// IsBool checks if the state value is of boolean type.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// None
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// (bool) : Indicates whether the StateValue is of boolean type or not.
|
||||
func (sv StateValue) IsBool() bool {
|
||||
return sv.valueType.IsBool()
|
||||
}
|
||||
|
||||
// isString checks if the underlying value type of a StateValue object represents a string.
|
||||
//
|
||||
// Parameters:
|
||||
// - None.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// bool: True if the underlying value type is a string, false otherwise.
|
||||
func (sv StateValue) IsString() bool {
|
||||
return sv.valueType.IsString()
|
||||
}
|
||||
|
||||
func (sv StateValue) IsTime() bool {
|
||||
return sv.valueType.IsTime()
|
||||
}
|
||||
|
||||
func (sv StateValue) IsURI() bool {
|
||||
return sv.valueType.IsURI()
|
||||
}
|
||||
|
||||
func (sv StateValue) IsBinary() bool {
|
||||
return sv.valueType.IsBinary()
|
||||
}
|
||||
|
||||
// IsComparable function checks if a state variable is comparable or not.
|
||||
//
|
||||
// It returns false for binary types (bin.base64 and bin.hex)
|
||||
// as they are non-comparable. For all other types, it returns true indicating
|
||||
// that these types can be compared.
|
||||
// //
|
||||
// Returns: bool: A boolean value indicating whether the given StateValue is
|
||||
// comparable or not. True means it's comparable, False means it isn't.
|
||||
func (sv StateValue) IsComparable() bool {
|
||||
return sv.valueType.IsComparable()
|
||||
}
|
||||
|
||||
func (sv StateValue) Add(a, b interface{}) (interface{}, error) {
|
||||
return sv.valueType.Add(a, b)
|
||||
}
|
||||
|
||||
func (sv StateValue) Sub(a, b interface{}) (interface{}, error) {
|
||||
return sv.valueType.Sub(a, b)
|
||||
}
|
||||
|
||||
func (sv StateValue) Mul(a, b interface{}) (interface{}, error) {
|
||||
return sv.valueType.Mul(a, b)
|
||||
}
|
||||
|
||||
func (sv StateValue) Div(a, b interface{}) (interface{}, error) {
|
||||
return sv.valueType.Div(a, b)
|
||||
}
|
||||
|
||||
// Name returns the state variable's name (e.g., "Volume", "Brightness").
|
||||
func (state *StateValue) Name() string {
|
||||
return state.name
|
||||
func (sv StateValue) Name() string {
|
||||
return sv.name
|
||||
}
|
||||
|
||||
// Type returns the UPnP data type of the state variable.
|
||||
@@ -70,35 +188,37 @@ func (sv *StateValue) SetMinDelta(minDelta interface{}) error {
|
||||
|
||||
minDelta, err := sv.Cast(minDelta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: invalid minimum delta value %v : %v", sv.name, minDelta,err)
|
||||
return fmt.Errorf("%s: invalid minimum delta value %v : %v", sv.name, minDelta, err)
|
||||
}
|
||||
|
||||
mdf := func(instance *StateValueInstance) bool {
|
||||
o := instance.previousValue
|
||||
n := instance.value
|
||||
// mdf := func(instance *StateValueInstance) bool {
|
||||
// o := instance.previousValue
|
||||
// n := instance.value
|
||||
|
||||
// r,err := instance.model.valueType.ValueRange(o, n)
|
||||
|
||||
r,err := instance.model.valueType.ValueRange(o, n)
|
||||
// if err != nil {
|
||||
// return false
|
||||
// }
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// return instance.model.valueType.InRange()
|
||||
// }
|
||||
|
||||
return instance.model.valueType.InRange()
|
||||
}
|
||||
|
||||
sv.eventConditions["MinDelta"] = mdf
|
||||
// sv.eventConditions["MinDelta"] = mdf
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state *StateValue) SetDefault(value interface{}) error {
|
||||
if state.IsValidValue(value) {
|
||||
var err error
|
||||
var valid bool
|
||||
|
||||
if valid, err = state.IsValidValue(value); valid && err == nil {
|
||||
cvalue, _ := state.valueType.Cast(value)
|
||||
state.defaultValue = cvalue
|
||||
log.Debugf("🐞 Setting default value for %v to %v", state.name, cvalue)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid default value for %v (%v)", state.name, value)
|
||||
return fmt.Errorf("invalid default value for %v (%v) : %v", state.name, value, err)
|
||||
}
|
||||
|
||||
func (state *StateValue) HasDefault() bool {
|
||||
@@ -224,7 +344,7 @@ func (state *StateValue) UpdateMaximalValue(value interface{}) error {
|
||||
// Returns:
|
||||
//
|
||||
// bool: True if within range or no range defined
|
||||
func (state *StateValue) IsValueInRange(value interface{}) bool {
|
||||
func (state *StateValue) IsValueInRange(value interface{}) (bool, error) {
|
||||
return state.valueType.InRange(value, state.valueRange)
|
||||
}
|
||||
|
||||
@@ -294,22 +414,22 @@ func (state *StateValue) AppendAllowedValue(value ...interface{}) error {
|
||||
// Returns:
|
||||
//
|
||||
// bool: True if value is permitted or no list defined
|
||||
func (state *StateValue) IsValueAllowed(value interface{}) bool {
|
||||
func (state *StateValue) IsValueAllowed(value interface{}) (bool, error) {
|
||||
if !state.HasAllowedValues() {
|
||||
return true // No list = any value valid
|
||||
return true, nil // No list = any value valid
|
||||
}
|
||||
|
||||
cvalue, err := state.valueType.Cast(value)
|
||||
if err != nil {
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, allowed := range state.allowedValues {
|
||||
if reflect.DeepEqual(cvalue, allowed) {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// IsValidValue performs full validation against all constraints.
|
||||
@@ -321,12 +441,22 @@ func (state *StateValue) IsValueAllowed(value interface{}) bool {
|
||||
// Returns:
|
||||
//
|
||||
// bool: True if value passes all applicable constraints
|
||||
func (state *StateValue) IsValidValue(value interface{}) bool {
|
||||
func (state *StateValue) IsValidValue(value interface{}) (bool, error) {
|
||||
cvalue, err := state.valueType.Cast(value)
|
||||
if err != nil {
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
return state.IsValueInRange(cvalue) && state.IsValueAllowed(cvalue)
|
||||
|
||||
inrange, err1 := state.IsValueInRange(cvalue)
|
||||
allowed, err2 := state.IsValueAllowed(cvalue)
|
||||
if err1 != nil || err2 != nil {
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
} else {
|
||||
err = err2
|
||||
}
|
||||
}
|
||||
return inrange && allowed, err
|
||||
}
|
||||
|
||||
func (state *StateValue) HasDescription() bool {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -103,6 +102,18 @@ var typeStrings = [...]string{
|
||||
"uri",
|
||||
}
|
||||
|
||||
// StateVarTypeFactory takes a string and attempts to build from it a valid
|
||||
// StateVarType. The input string is cleaned before processing -
|
||||
// leading/trailing spaces are trimmed, the case is lowered for comparison with
|
||||
// known types, and if no match is found 'StateType_Unknown' is returned.
|
||||
func StateVarTypeFactory(s string) StateVarType {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
if val, ok := typeNames[s]; ok {
|
||||
return val
|
||||
}
|
||||
return StateType_Unknown
|
||||
}
|
||||
|
||||
// String returns a string representation of the StateVarType. It defaults to
|
||||
// "unknown" if the type is not recognized.
|
||||
func (t StateVarType) String() string {
|
||||
@@ -112,6 +123,29 @@ func (t StateVarType) String() string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// BitSize returns the bit size of the StateVarType value, or -1 if not numeric.
|
||||
// The possible return values are 8 for StateTypes I1, UI1, and 64 for others.
|
||||
func (t StateVarType) BitSize() int {
|
||||
// If t isn't numeric, return -1
|
||||
if !t.IsNumeric() {
|
||||
return -1
|
||||
}
|
||||
|
||||
// Check the value of t and return the appropriate bit size
|
||||
switch t {
|
||||
case StateType_I1, StateType_UI1:
|
||||
return 8
|
||||
case StateType_I2, StateType_UI2:
|
||||
return 16
|
||||
case StateType_I4, StateType_UI4, StateType_Int, StateType_R4:
|
||||
return 32
|
||||
case StateType_R8, StateType_Number, StateType_Fixed14_4:
|
||||
return 64
|
||||
default:
|
||||
return 64
|
||||
}
|
||||
}
|
||||
|
||||
// IsNumeric checks whether a given StateVarType represents a numeric type or
|
||||
// not. Numeric types are defined as those that can be used to store number-like
|
||||
// values. The following types are considered numeric: UI1, UI2, UI4, I1, I2,
|
||||
@@ -157,6 +191,111 @@ func (t StateVarType) IsInteger() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsSignedInt checks if the state variable type is a signed integer type.
|
||||
// The function returns true for StateType_I1, StateType_I2, StateType_I4, and
|
||||
// StateType_Int, otherwise it will return false. This method is part of the
|
||||
// StateVarType enumeration in statevaluetype package. It takes no parameters
|
||||
// but operates on the receiver 't' of type StateVarType.
|
||||
//
|
||||
// The returned value is a boolean.
|
||||
func (t StateVarType) IsSignedInt() bool {
|
||||
switch t {
|
||||
case StateType_I1, StateType_I2, StateType_I4, StateType_Int:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsUnsignedInt checks if the state variable type is an unsigned integer. It
|
||||
// returns a boolean indicating whether or not the current StateVarType
|
||||
// represents an unsigned integer type, namely: StateType_UI1, StateType_UI2,
|
||||
// and StateType_UI4.
|
||||
func (t StateVarType) IsUnsignedInt() bool {
|
||||
switch t {
|
||||
case StateType_UI1, StateType_UI2, StateType_UI4:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsFloat returns a boolean indicating whether the given state variable type
|
||||
// represents a float number. If the state variable type is one of R4, R8, Number or
|
||||
// Fixed14_4 it returns true; otherwise, it returns false.
|
||||
func (t StateVarType) IsFloat() bool {
|
||||
switch t {
|
||||
case StateType_R4, StateType_R8, StateType_Number, StateType_Fixed14_4:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsString reports whether or not the state variable type represents a string
|
||||
// value.
|
||||
//
|
||||
// This method returns true if the StateVarType is either StateType_String or
|
||||
// StateType_Char, otherwise it returns false.
|
||||
func (t StateVarType) IsString() bool {
|
||||
switch t {
|
||||
case StateType_String, StateType_Char:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsBool checks if a StateVarType is of type Boolean. It returns true if the
|
||||
// StateVarType equals to StateType_Boolean, false otherwise.
|
||||
func (t StateVarType) IsBool() bool {
|
||||
return t == StateType_Boolean
|
||||
}
|
||||
|
||||
// IsTime checks whether a given StateVarType is of time type or not. It accepts
|
||||
// a StateVarType parameter 't' and returns a boolean value based on the check.
|
||||
//
|
||||
// The possible values for 't' are: StateType_Date, StateType_DateTime,
|
||||
// StateType_DateTimeTZ, StateType_Time, StateType_TimeTZ. If 't' is any of
|
||||
// these types, the function returns true; otherwise, it returns false.
|
||||
func (t StateVarType) IsTime() bool {
|
||||
switch t {
|
||||
case StateType_Date, StateType_DateTime, StateType_DateTimeTZ,
|
||||
StateType_Time, StateType_TimeTZ:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsUUID reports whether the receiver represents a UUID (Universally Unique
|
||||
// Identifier). The StateVarType should be of type StateType_UUID to return
|
||||
// true. Otherwise, it returns false.
|
||||
func (t StateVarType) IsUUID() bool {
|
||||
return t == StateType_UUID
|
||||
}
|
||||
|
||||
// IsURI checks if the given state variable type is a URI.
|
||||
//
|
||||
// This function returns true if and only if the receiver (StateVarType t)
|
||||
// equals StateType_URI, which represents URIs in UPnP protocol. Otherwise, it
|
||||
// returns false.
|
||||
func (t StateVarType) IsURI() bool {
|
||||
return t == StateType_URI
|
||||
}
|
||||
|
||||
// IsBinary checks if the given StateVarType is binary type or not. It returns
|
||||
// true for types StateType_BinBase64 and StateType_BinHex, otherwise it returns
|
||||
// false.
|
||||
func (t StateVarType) IsBinary() bool {
|
||||
switch t {
|
||||
case StateType_BinBase64, StateType_BinHex:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsComparable function checks if a StateVarType is comparable or not.
|
||||
//
|
||||
// It returns false for binary types (StateType_BinBase64 and StateType_BinHex)
|
||||
@@ -201,6 +340,10 @@ func (t StateVarType) Add(a, b interface{}) (interface{}, error) {
|
||||
return t.Cast(af + bf)
|
||||
}
|
||||
|
||||
// Sub subtracts 'b' from 'a'. It converts both values to numeric types
|
||||
// and then performs a subtraction operation, casting the result back to its
|
||||
// original type. If either conversion fails or an unsupported type is used,
|
||||
// it returns an error.
|
||||
func (t StateVarType) Sub(a, b interface{}) (interface{}, error) {
|
||||
af, bf, err := valuesToNumericOperands(t, a, b)
|
||||
if err != nil {
|
||||
@@ -210,6 +353,14 @@ func (t StateVarType) Sub(a, b interface{}) (interface{}, error) {
|
||||
return t.Cast(af - bf)
|
||||
}
|
||||
|
||||
// Mul takes in two interface types 'a' and 'b', multiplies them together
|
||||
// and returns the result along with any error encountered during this
|
||||
// process. If either of the inputs is not compatible with numeric values, an
|
||||
// error will be returned. The multiplication operation is performed between two
|
||||
// numbers represented as 'float64' types (since Go does not support generic
|
||||
// types on its own). The resulting value will be cast to the type represented
|
||||
// by the receiver of this method 't'. If a casting error occurs, it will also
|
||||
// be returned along with nil for the result.
|
||||
func (t StateVarType) Mul(a, b interface{}) (interface{}, error) {
|
||||
af, bf, err := valuesToNumericOperands(t, a, b)
|
||||
if err != nil {
|
||||
@@ -219,6 +370,15 @@ func (t StateVarType) Mul(a, b interface{}) (interface{}, error) {
|
||||
return t.Cast(af * bf)
|
||||
}
|
||||
|
||||
// Div performs division between the provided arguments 'a' and 'b'. The function casts both operands to their numeric equivalents using valuesToNumericOperands() before performing the division.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: first operand of type interface{}, can be of any type, will be converted if necessary
|
||||
// - b: second operand of type interface{}, can be of any type, will be converted if necessary
|
||||
//
|
||||
// Returns:
|
||||
// - result: the division result in numeric form after casting it with function t.Cast()
|
||||
// - err: error that might occur during the conversion or division operation
|
||||
func (t StateVarType) Div(a, b interface{}) (interface{}, error) {
|
||||
af, bf, err := valuesToNumericOperands(t, a, b)
|
||||
if err != nil {
|
||||
@@ -228,16 +388,6 @@ func (t StateVarType) Div(a, b interface{}) (interface{}, error) {
|
||||
return t.Cast(af / bf)
|
||||
}
|
||||
|
||||
// ParseStateVarType converts a UPnP type name to its StateVarType constant.
|
||||
// Case-insensitive and trims whitespace. Returns StateType_Unknown for unrecognized types.
|
||||
func ParseStateVarType(s string) StateVarType {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
if val, ok := typeNames[s]; ok {
|
||||
return val
|
||||
}
|
||||
return StateType_Unknown
|
||||
}
|
||||
|
||||
// Cast converts a value to the Go type corresponding to the UPnP type.
|
||||
// Supports conversion from various primitive types and strings.
|
||||
// Returns an error for unsupported conversions or invalid values.
|
||||
@@ -381,108 +531,157 @@ func (t StateVarType) Cast(val interface{}) (interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cmp compares two values of the UPnP type. Returns:
|
||||
// - -1 if v1 < v2
|
||||
// - 0 if v1 == v2
|
||||
// - 1 if v1 > v2
|
||||
//
|
||||
// Panics if values can't be cast to the type. Handles all supported types
|
||||
// including binaries, times, and UUIDs.
|
||||
func (t StateVarType) Cmp(v1, v2 interface{}) int {
|
||||
// Helper: compare float64
|
||||
compareFloat := func(f1, f2 float64) int {
|
||||
switch {
|
||||
case f1 < f2:
|
||||
return -1
|
||||
case f1 > f2:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
castV1, err1 := t.Cast(v1)
|
||||
castV2, err2 := t.Cast(v2)
|
||||
func (t StateVarType) Cmp(a, b interface{}) (int, error) {
|
||||
a, err1 := t.Cast(a)
|
||||
b, err2 := t.Cast(b)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
log.Fatalf("Failed to cast for comparison: %v vs %v (errors: %v, %v)", v1, v2, err1, err2)
|
||||
log.Fatalf("Failed to cast for comparison: %v vs %v (errors: %v, %v)", a, b, err1, err2)
|
||||
}
|
||||
|
||||
switch t {
|
||||
case StateType_UI1, StateType_UI2, StateType_UI4,
|
||||
StateType_Int, StateType_I1, StateType_I2, StateType_I4:
|
||||
i1 := reflect.ValueOf(castV1).Int()
|
||||
i2 := reflect.ValueOf(castV2).Int()
|
||||
|
||||
switch {
|
||||
case i1 < i2:
|
||||
return -1
|
||||
case i1 > i2:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
case t.IsInteger():
|
||||
ai, err := toInt(a, t.BitSize())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid int value for a: %w", err)
|
||||
}
|
||||
bi, err := toInt(b, t.BitSize())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid int value for b: %w", err)
|
||||
}
|
||||
return cmpInt(ai, bi), nil
|
||||
|
||||
case StateType_R4:
|
||||
f1 := float64(castV1.(float32))
|
||||
f2 := float64(castV2.(float32))
|
||||
return compareFloat(f1, f2)
|
||||
case t.IsUnsignedInt():
|
||||
ai, err := toUint(a, t.BitSize())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid uint value for a: %w", err)
|
||||
}
|
||||
bi, err := toUint(b, t.BitSize())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid uint value for b: %w", err)
|
||||
}
|
||||
return cmpUint(ai, bi), nil
|
||||
|
||||
case StateType_R8, StateType_Number, StateType_Fixed14_4:
|
||||
f1 := castV1.(float64)
|
||||
f2 := castV2.(float64)
|
||||
return compareFloat(f1, f2)
|
||||
case t.IsFloat():
|
||||
af, err := toFloat(a, t.BitSize())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid float value for a: %w", err)
|
||||
}
|
||||
bf, err := toFloat(b, t.BitSize())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid float value for b: %w", err)
|
||||
}
|
||||
return cmpFloat64(af, bf), nil
|
||||
|
||||
case StateType_Boolean:
|
||||
b1 := castV1.(bool)
|
||||
b2 := castV2.(bool)
|
||||
case t == StateType_Boolean:
|
||||
ab, err := toBool(a)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid bool value for a: %w", err)
|
||||
}
|
||||
bb, err := toBool(b)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid bool value for b: %w", err)
|
||||
}
|
||||
return cmpBool(ab, bb), nil
|
||||
|
||||
case t == StateType_String || t == StateType_Char:
|
||||
as, err := toString(a)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid string value for a")
|
||||
}
|
||||
bs, err := toString(b)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid string value for b")
|
||||
}
|
||||
return strings.Compare(as, bs), nil
|
||||
|
||||
case t.IsTime():
|
||||
at, err := toTime(a)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid time value for a")
|
||||
}
|
||||
bt, err := toTime(b)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid time value for b")
|
||||
}
|
||||
return cmpTime(at, bt), nil
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("comparison not supported for type %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (t StateVarType) Equal(a, b interface{}) (bool, error) {
|
||||
switch {
|
||||
case b1 == b2:
|
||||
return 0
|
||||
case !b1 && b2:
|
||||
return -1
|
||||
default:
|
||||
return 1
|
||||
case t.IsInteger():
|
||||
ai, err1 := toInt(a, 64)
|
||||
bi, err2 := toInt(b, 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false, fmt.Errorf("invalid integer value for type %s", t.String())
|
||||
}
|
||||
return ai == bi, nil
|
||||
|
||||
case StateType_Char:
|
||||
r1 := castV1.(rune)
|
||||
r2 := castV2.(rune)
|
||||
|
||||
switch {
|
||||
case r1 < r2:
|
||||
return -1
|
||||
case r1 > r2:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
case t.IsFloat():
|
||||
af, err := toFloat(a, 64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid float value for type %s: %v", t.String(), err)
|
||||
}
|
||||
|
||||
case StateType_String, StateType_UUID, StateType_URI:
|
||||
s1 := castV1.(string)
|
||||
s2 := castV2.(string)
|
||||
return strings.Compare(s1, s2)
|
||||
|
||||
case StateType_BinBase64, StateType_BinHex:
|
||||
b1 := castV1.([]byte)
|
||||
b2 := castV2.([]byte)
|
||||
return bytes.Compare(b1, b2)
|
||||
|
||||
case StateType_Date, StateType_DateTime, StateType_DateTimeTZ,
|
||||
StateType_Time, StateType_TimeTZ:
|
||||
t1 := castV1.(time.Time)
|
||||
t2 := castV2.(time.Time)
|
||||
if t1.Before(t2) {
|
||||
return -1
|
||||
} else if t1.After(t2) {
|
||||
return 1
|
||||
bf, err := toFloat(b, 64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid float value for type %s: %v", t.String(), err)
|
||||
}
|
||||
return 0
|
||||
return af == bf, nil
|
||||
|
||||
case t.IsString():
|
||||
as, ok1 := a.(string)
|
||||
bs, ok2 := b.(string)
|
||||
if !ok1 || !ok2 {
|
||||
return false, fmt.Errorf("invalid string value for type %s", t.String())
|
||||
}
|
||||
return as == bs, nil
|
||||
|
||||
case t.IsBool():
|
||||
ab, err1 := toBool(a)
|
||||
bb, err2 := toBool(b)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false, fmt.Errorf("invalid boolean value for type %s", t.String())
|
||||
}
|
||||
return ab == bb, nil
|
||||
|
||||
case t.IsTime():
|
||||
at, err1 := toTime(a)
|
||||
bt, err2 := toTime(b)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false, fmt.Errorf("invalid time.Time value for type %s", t.String())
|
||||
}
|
||||
return at.Equal(bt), nil
|
||||
|
||||
case t.IsUUID():
|
||||
au, err1 := toUUID(a)
|
||||
bu, err2 := toUUID(b)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false, fmt.Errorf("invalid uuid.UUID value for type %s", t.String())
|
||||
}
|
||||
return au == bu, nil
|
||||
|
||||
case t.IsURI():
|
||||
au, err1 := toURI(a)
|
||||
bu, err2 := toURI(b)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false, fmt.Errorf("invalid *url.URL value for type %s", t.String())
|
||||
}
|
||||
return au.String() == bu.String(), nil
|
||||
|
||||
case t.IsBinary():
|
||||
ab, err1 := toBinary(a)
|
||||
bb, err2 := toBinary(b)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false, fmt.Errorf("invalid []byte value for type %s", t.String())
|
||||
}
|
||||
return bytes.Equal(ab, bb), nil
|
||||
|
||||
default:
|
||||
s1 := fmt.Sprint(castV1)
|
||||
s2 := fmt.Sprint(castV2)
|
||||
return strings.Compare(s1, s2)
|
||||
return false, fmt.Errorf("equality not supported for type %s", t.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,12 +692,28 @@ func (t StateVarType) Cmp(v1, v2 interface{}) int {
|
||||
//
|
||||
// range := ValueRange{min: uint16(10), max: uint16(100)}
|
||||
// StateType_UI2.InRange(uint16(50), range) // true
|
||||
func (t StateVarType) InRange(val interface{}, interval *ValueRange) bool {
|
||||
return interval == nil || t.Cmp(val, interval.min) >= 0 && t.Cmp(val, interval.max) <= 0
|
||||
func (t StateVarType) InRange(val interface{}, interval *ValueRange) (bool, error) {
|
||||
if interval == nil {
|
||||
return true, nil
|
||||
}
|
||||
cmp1, err1 := t.Cmp(val, interval.min)
|
||||
cmp2, err2 := t.Cmp(val, interval.max)
|
||||
if err1 != nil || err2 != nil {
|
||||
err := err1
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return cmp1 >= 0 && cmp2 <= 0, nil
|
||||
}
|
||||
|
||||
// NewAtomicValue crée une valeur simple
|
||||
func (t StateVarType) NewAtomicValue(name string) *StateValue {
|
||||
// NewStateValue creates and returns a new StateValue struct instance with the given name
|
||||
// and the receiver's state variable type. The created StateValue is initialized with an
|
||||
// empty map for event conditions. If name is an empty string, it will cause panic in later
|
||||
// usage. Name is typically used to identify or represent a specific value or condition
|
||||
// associated with the variable type 't'.
|
||||
func (t StateVarType) NewStateValue(name string) *StateValue {
|
||||
return &StateValue{
|
||||
name: name,
|
||||
valueType: t,
|
||||
|
||||
33
upnp/utils_binary.go
Normal file
33
upnp/utils_binary.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// toBinary tries to convert v into a []byte.
|
||||
// - if v is []byte, returns it directly
|
||||
// - if v is string, attempts base64 decode, then hex decode
|
||||
func toBinary(v interface{}) ([]byte, error) {
|
||||
switch val := v.(type) {
|
||||
case []byte:
|
||||
return val, nil
|
||||
|
||||
case string:
|
||||
// Try base64 first
|
||||
if data, err := base64.StdEncoding.DecodeString(val); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Try hex next
|
||||
if data, err := hex.DecodeString(val); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("cannot parse string as base64 or hex: %q", val)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot convert type %T to binary", v)
|
||||
}
|
||||
}
|
||||
@@ -47,3 +47,13 @@ func toBool(val interface{}) (bool, error) {
|
||||
return false, errors.New("numeric value cannot be converted to bool unless 0 or 1")
|
||||
}
|
||||
}
|
||||
|
||||
func cmpBool(a, b bool) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if !a && b {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -75,3 +75,14 @@ func toFloat(v interface{}, bits int) (float64, error) {
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func cmpFloat64(a, b float64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,3 +114,25 @@ func checkIntBounds(v int64, bits int) (int64, error) {
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func cmpInt(a, b int64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func cmpUint(a, b uint64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
37
upnp/utils_string.go
Normal file
37
upnp/utils_string.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// toString converts v to string if possible.
|
||||
func toString(v interface{}) (string, error) {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val, nil
|
||||
|
||||
case []byte:
|
||||
return string(val), nil
|
||||
|
||||
case fmt.Stringer:
|
||||
return val.String(), nil
|
||||
|
||||
case int, int8, int16, int32, int64:
|
||||
return fmt.Sprintf("%d", val), nil
|
||||
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return fmt.Sprintf("%d", val), nil
|
||||
|
||||
case float32, float64:
|
||||
return fmt.Sprintf("%v", val), nil
|
||||
|
||||
case bool:
|
||||
if val {
|
||||
return "true", nil
|
||||
}
|
||||
return "false", nil
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("cannot convert type %T to string", v)
|
||||
}
|
||||
}
|
||||
55
upnp/utils_time.go
Normal file
55
upnp/utils_time.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func toTime(v interface{}) (time.Time, error) {
|
||||
switch val := v.(type) {
|
||||
case time.Time:
|
||||
return val, nil
|
||||
|
||||
case string:
|
||||
// Liste de layouts communs à UPnP / ISO-8601 / RFC
|
||||
layouts := []string{
|
||||
time.RFC3339Nano, // 2006-01-02T15:04:05.999999999Z07:00
|
||||
time.RFC3339, // 2006-01-02T15:04:05Z07:00
|
||||
"2006-01-02", // date only
|
||||
"15:04:05", // time only
|
||||
"15:04:05Z07:00", // time with TZ
|
||||
"2006-01-02T15:04:05", // no TZ
|
||||
"2006-01-02T15:04:05Z", // UTC
|
||||
"2006-01-02 15:04:05", // space-separated
|
||||
}
|
||||
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.Parse(layout, val); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("cannot parse time string: %q", val)
|
||||
|
||||
case int64:
|
||||
return time.Unix(val, 0), nil
|
||||
|
||||
case float64:
|
||||
sec := int64(val)
|
||||
nsec := int64((val - float64(sec)) * 1e9)
|
||||
return time.Unix(sec, nsec), nil
|
||||
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("unsupported type for time conversion: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpTime(a, b time.Time) int {
|
||||
switch {
|
||||
case a.Before(b):
|
||||
return -1
|
||||
case a.After(b):
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
25
upnp/utils_uri.go
Normal file
25
upnp/utils_uri.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// toURI converts a value to *url.URL if possible.
|
||||
func toURI(v interface{}) (*url.URL, error) {
|
||||
switch val := v.(type) {
|
||||
case *url.URL:
|
||||
// Already a URL
|
||||
return val, nil
|
||||
|
||||
case string:
|
||||
u, err := url.Parse(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid URI string %q: %v", val, err)
|
||||
}
|
||||
return u, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot convert type %T to URI", v)
|
||||
}
|
||||
}
|
||||
32
upnp/utils_uuid.go
Normal file
32
upnp/utils_uuid.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// toUUID converts a value to uuid.UUID if possible.
|
||||
func toUUID(v interface{}) (uuid.UUID, error) {
|
||||
switch val := v.(type) {
|
||||
case uuid.UUID:
|
||||
return val, nil
|
||||
|
||||
case string:
|
||||
u, err := uuid.Parse(val)
|
||||
if err != nil {
|
||||
return uuid.UUID{}, fmt.Errorf("invalid UUID string %q: %v", val, err)
|
||||
}
|
||||
return u, nil
|
||||
|
||||
case []byte:
|
||||
u, err := uuid.FromBytes(val)
|
||||
if err != nil {
|
||||
return uuid.UUID{}, fmt.Errorf("invalid UUID bytes: %v", err)
|
||||
}
|
||||
return u, nil
|
||||
|
||||
default:
|
||||
return uuid.UUID{}, fmt.Errorf("cannot convert type %T to UUID", v)
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (t StateVarType) ValueRange(min, max interface{}) (*ValueRange, error) {
|
||||
return nil, fmt.Errorf("max value %v is not castable to type %s", min, t.String())
|
||||
}
|
||||
|
||||
if t.Cmp(cmin, cmax) > 0 {
|
||||
if cmp, err := t.Cmp(cmin, cmax); err != nil && cmp > 0 {
|
||||
cmax, cmin = cmin, cmax
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user