├── .travis.yml ├── LICENSE ├── README.md ├── convert.go ├── convert_test.go ├── examples └── main.go ├── go.mod └── go.sum /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | env: 4 | - GO111MODULE=on 5 | 6 | go: 7 | - '1.12.x' 8 | - '1.13.x' 9 | - tip 10 | 11 | matrix: 12 | allow_failures: 13 | - go: tip 14 | 15 | script: 16 | - go test ./... 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 epic labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elastic 2 | [![Build Status](https://travis-ci.org/epiclabs-io/elastic.svg?branch=master)](https://travis-ci.org/epiclabs-io/elastic) 3 | [![GoDoc](https://godoc.org/github.com/epiclabs-io/elastic?status.svg)](https://godoc.org/github.com/epiclabs-io/elastic) 4 | 5 | 6 | Converts go types no matter what 7 | 8 | `elastic` is a simple library that converts any type to another the best way possible. This is useful when the type is only known at run-time, which usually happens when serializing data. `elastic` allows your code to be flexible regarding type conversion if that is what you're looking for. 9 | 10 | It is also capable of seeing through alias types and converting slices and maps to and from other types of slices and maps, providing there is some logical way to convert them. 11 | 12 | Default conversion can be overridden by providing custom conversion functions for specific types. 13 | Struct types can also implement the `ConverterTo` interface to help with conversion to and from specific types. 14 | 15 | 16 | ## Quick examples: 17 | 18 | ### convert value types: 19 | 20 | ```go 21 | // note that using elastic wouldn't make sense if you are certain 22 | // f is a float64 at compile time. 23 | var f interface{} = float64(5.5) 24 | var i int 25 | 26 | err := elastic.Set(&i, f) 27 | if err != nil { 28 | log.Fatal(f) 29 | } 30 | 31 | fmt.Println(i) // prints 5 32 | ``` 33 | 34 | ### convert slices: 35 | 36 | ```go 37 | var ints []int 38 | err = elastic.Set(&ints, []interface{}{1, 2, 3, "4", float64(5), 6}) 39 | if err != nil { 40 | log.Fatal(f) 41 | } 42 | 43 | fmt.Println(ints) // prints [1 2 3 4 5 6] 44 | ``` 45 | 46 | ### convert maps: 47 | 48 | ```go 49 | someMap := map[string]interface{}{ 50 | "1": "uno", 51 | "2": "dos", 52 | "3": "tres", 53 | } 54 | 55 | intmap := make(map[int]string) 56 | err = elastic.Set(&intmap, someMap) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | fmt.Println(intmap) // prints map[1:uno 2:dos 3:tres] 61 | ``` 62 | 63 | # Simple API: 64 | 65 | ## `elastic.Convert()` 66 | Converts the passed value to the target type 67 | #### Syntax: 68 | `elastic.Convert(source interface{}, targetType reflect.Type) (interface{}, error)` 69 | 70 | * `source`: value to convert 71 | * `targetType` the type you want to convert `source` to 72 | 73 | #### Returns 74 | The converted value or an error if it fails. 75 | 76 | ## `elastic.Set()` 77 | Sets the given variable to the passed value 78 | #### Syntax: 79 | `elastic.Set(target, source interface{}) error` 80 | * `target`: value to set. Must be a pointer. 81 | * `source` the value to convert 82 | 83 | #### Returns 84 | Only an error if it fails. 85 | 86 | # Advanced API: 87 | 88 | You can create different instances of the elastic conversion engine so that you can customize conversions independently 89 | 90 | ## `elastic.New()` 91 | Returns a new conversion engine. It has a `.Set()` and `.Convert()` as above that will work according to the rules set for this engine 92 | 93 | ## `AddSourceConverter() and AddTargetConverter()` 94 | Registers a conversion function for the given type, either when the type is found on the source side or the target side. 95 | 96 | These are useful when you do not control the type you whish to make convertible 97 | 98 | #### Syntax: 99 | `engine.AddSourceConverter(sourceType reflect.Type, f ConverterFunc)` 100 | * `sourceType`: type you want to set a custom conversion function for 101 | * `f`: Conversion function to invoke when this type is found as a source 102 | 103 | `engine.AddTargetConverter(targetType reflect.Type, f ConverterFunc)` 104 | * `targetType`: type you want to set a custom conversion function for 105 | * `f`: Conversion function to invoke when this type is found as a target 106 | 107 | The value returned by your function does not have to be *exactly* of type `targetType`. For example if a `float64` is requested and you return an integer, `elastic` will deal with it. 108 | 109 | #### Example: 110 | ```go 111 | package main 112 | 113 | type Vector struct { 114 | X float64 115 | Y float64 116 | } 117 | 118 | func main() { 119 | 120 | // Add a custom converter to convert Vector to float64 or int 121 | // (Calculates the modulus of the vector) 122 | elastic.Default.AddSourceConverter(reflect.TypeOf(Vector{}), func(source interface{}, targetType reflect.Type) (interface{}, error) { 123 | vector := source.(Vector) 124 | switch targetType.Kind() { 125 | case reflect.Float64, reflect.Int: 126 | return math.Sqrt(float64(vector.X*vector.X) + float64(vector.Y*vector.Y)), nil 127 | case reflect.String: 128 | return fmt.Sprintf("(%g, %g)", vector.X, vector.Y), nil 129 | } 130 | return nil, elastic.ErrNoConversionAvailable 131 | 132 | }) 133 | 134 | v := Vector{ 135 | X: 3.0, 136 | Y: 4.0, 137 | } 138 | 139 | f, err = elastic.Convert(v, reflect.TypeOf(float64(0))) 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | fmt.Println(f) // prints 5 144 | 145 | var s string 146 | elastic.Set(&s, v) 147 | fmt.Println(s) // prints (3, 4) 148 | 149 | } 150 | 151 | ``` 152 | 153 | ## `ConverterTo` interface 154 | 155 | ```go 156 | type ConverterTo interface { 157 | ConvertTo(targetType reflect.Type) (interface{}, error) 158 | } 159 | ``` 160 | 161 | Implement this interface in your type to provide conversion to another types. This function will be invoked every time your type is on the right-hand side of a conversion. 162 | The value returned by your function does not have to be *exactly* of type `targetType`. For example if a `float64` is requested and you return an integer, `elastic` will deal with it. 163 | 164 | #### Example: 165 | 166 | ```go 167 | type Vector struct { 168 | X float64 169 | Y float64 170 | } 171 | 172 | func (v *Vector) ConvertTo(targetType reflect.Type) (interface{}, error) { 173 | switch targetType.Kind() { 174 | case reflect.Float64, reflect.Int: 175 | return math.Sqrt(float64(v.X*v.X) + float64(v.Y*v.Y)), nil 176 | case reflect.String: 177 | return fmt.Sprintf("(%g, %g)", v.X, v.Y), nil 178 | } 179 | return nil, elastic.ErrNoConversionAvailable 180 | } 181 | 182 | func main() { 183 | v := Vector{ 184 | X: 3.0, 185 | Y: 4.0, 186 | } 187 | 188 | var i int 189 | elastic.Set(&i, v) 190 | fmt.Println(i) // prints 5 191 | 192 | var s string 193 | elastic.Set(&s, v) 194 | fmt.Println(s) // prints (3, 4) 195 | } 196 | 197 | 198 | ``` -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | // ConverterFunc is called to override default conversions 11 | type ConverterFunc func(source interface{}, targetType reflect.Type) (interface{}, error) 12 | 13 | // ConverterTo interface allows you to define how your type should convert to others 14 | type ConverterTo interface { 15 | ConvertTo(targetType reflect.Type) (interface{}, error) 16 | } 17 | 18 | // ConverterEngine keeps conversion configurations 19 | type ConverterEngine struct { 20 | sourceConverters map[reflect.Type][]ConverterFunc 21 | targetConverters map[reflect.Type][]ConverterFunc 22 | interfaceConverters map[reflect.Type][]ConverterFunc 23 | } 24 | 25 | // Default is a default conversion engine 26 | var Default = New() 27 | 28 | // ErrExpectedPointer is returned when the function expects a pointer parameter 29 | var ErrExpectedPointer = errors.New("Expected pointer") 30 | 31 | // ErrIncompatibleType is returned when it is impossible to convert a type to another 32 | var ErrIncompatibleType = errors.New("Incompatible types") 33 | 34 | // ErrNoConversionAvailable is returned by any ConverterFunc when it does not know how to convert the passed values 35 | var ErrNoConversionAvailable = errors.New("No conversion available") 36 | 37 | // New instantiates a new Converter Engine 38 | func New() *ConverterEngine { 39 | return &ConverterEngine{ 40 | sourceConverters: make(map[reflect.Type][]ConverterFunc), 41 | targetConverters: make(map[reflect.Type][]ConverterFunc), 42 | interfaceConverters: make(map[reflect.Type][]ConverterFunc), 43 | } 44 | } 45 | 46 | // AddSourceConverter adds a source conversion function to the engine that knows how to convert the source type to some targets 47 | func (ce *ConverterEngine) AddSourceConverter(sourceType reflect.Type, f ConverterFunc) { 48 | cf := ce.sourceConverters[sourceType] 49 | cf = append(cf, f) 50 | ce.sourceConverters[sourceType] = cf 51 | } 52 | 53 | // AddTargetConverter adds a target conversion function to the engine that knows how to convert the target type from some sources 54 | func (ce *ConverterEngine) AddTargetConverter(targetType reflect.Type, f ConverterFunc) { 55 | cf := ce.targetConverters[targetType] 56 | cf = append(cf, f) 57 | ce.targetConverters[targetType] = cf 58 | } 59 | 60 | // AddInterfaceConverter adds a converion function for types that match the given interface (experimental) 61 | func (ce *ConverterEngine) AddInterfaceConverter(interfaceType reflect.Type, f ConverterFunc) { 62 | if interfaceType.Kind() != reflect.Interface { 63 | panic("type must be an interface") 64 | } 65 | cf := ce.interfaceConverters[interfaceType] 66 | cf = append(cf, f) 67 | ce.interfaceConverters[interfaceType] = cf 68 | } 69 | 70 | // convertMap attempts to convert the source map to another type of map 71 | func (ce *ConverterEngine) convertMap(source interface{}, targetType reflect.Type) (interface{}, error) { 72 | S := reflect.ValueOf(source) 73 | T := reflect.MakeMap(targetType) 74 | 75 | targetElementType := targetType.Elem() 76 | keyType := targetType.Key() 77 | 78 | for i := S.MapRange(); i.Next(); { 79 | value, err := ce.Convert(i.Value().Interface(), targetElementType) 80 | if err != nil { 81 | return nil, err 82 | } 83 | key, err := ce.Convert(i.Key().Interface(), keyType) 84 | if err != nil { 85 | return nil, err 86 | } 87 | T.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value)) 88 | } 89 | return T.Interface(), nil 90 | } 91 | 92 | // convertSlice attempts to convert a slice to another type of slice 93 | func (ce *ConverterEngine) convertSlice(source interface{}, targetType reflect.Type) (interface{}, error) { 94 | S := reflect.ValueOf(source) 95 | T := reflect.MakeSlice(targetType, 0, S.Len()) 96 | targetElementType := targetType.Elem() 97 | 98 | for i := 0; i < S.Len(); i++ { 99 | item, err := ce.Convert(S.Index(i).Interface(), targetElementType) 100 | if err != nil { 101 | return nil, err 102 | } 103 | T = reflect.Append(T, reflect.ValueOf(item)) 104 | } 105 | return T.Interface(), nil 106 | } 107 | 108 | // kind2Exact converts a type of the same kind 109 | func kind2Exact(source interface{}, targetType reflect.Type) interface{} { 110 | return reflect.ValueOf(source).Convert(targetType).Interface() 111 | } 112 | 113 | // Convert attempts to convert the source value to the given target type 114 | // if it does not fail, the returned value is guaranteed to be of the target type 115 | func (ce *ConverterEngine) Convert(source interface{}, targetType reflect.Type) (interface{}, error) { 116 | sourceType := reflect.TypeOf(source) 117 | if sourceType == targetType { 118 | return source, nil // no conversion necessary 119 | } 120 | 121 | // check if there are any custom source converters 122 | converters := ce.sourceConverters[reflect.TypeOf(source)] 123 | for _, converter := range converters { 124 | result, err := converter(source, targetType) 125 | if err == nil { 126 | return ce.Convert(result, targetType) 127 | } 128 | if err != ErrNoConversionAvailable { 129 | return nil, err 130 | } 131 | } 132 | 133 | // check if the source type implements ConverterTo 134 | converter, ok := source.(ConverterTo) 135 | if ok { 136 | result, err := converter.ConvertTo(targetType) 137 | if err == nil { 138 | return ce.Convert(result, targetType) 139 | } 140 | if err != ErrNoConversionAvailable { 141 | return nil, err 142 | } 143 | } 144 | 145 | // check if there are any custom target converters 146 | converters = ce.targetConverters[targetType] 147 | for _, converter := range converters { 148 | result, err := converter(source, targetType) 149 | if err == nil { 150 | return ce.Convert(result, targetType) 151 | } 152 | if err != ErrNoConversionAvailable { 153 | return nil, err 154 | } 155 | } 156 | 157 | // check for interface-based converter (experimental) 158 | for itype, converters := range ce.interfaceConverters { 159 | for _, converter := range converters { 160 | if sourceType.Implements(itype) { 161 | result, err := converter(source, targetType) 162 | if err == nil { 163 | return ce.Convert(result, targetType) 164 | } 165 | if err != ErrNoConversionAvailable { 166 | return nil, err 167 | } 168 | } 169 | } 170 | } 171 | 172 | S := reflect.ValueOf(source) 173 | 174 | // Conversion to string 175 | if targetType.Kind() == reflect.String { 176 | stringer, ok := source.(fmt.Stringer) // if target implements Stringer, use it. 177 | if ok { 178 | return kind2Exact(stringer.String(), targetType), nil 179 | } 180 | // Convert to string typical value types 181 | switch sourceType.Kind() { 182 | case reflect.Bool: 183 | return kind2Exact(strconv.FormatBool(S.Bool()), targetType), nil 184 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 185 | return kind2Exact(strconv.FormatInt(S.Int(), 10), targetType), nil 186 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 187 | return kind2Exact(strconv.FormatUint(S.Uint(), 10), targetType), nil 188 | case reflect.Float32, reflect.Float64: 189 | return kind2Exact(strconv.FormatFloat(S.Float(), 'g', 6, int(sourceType.Size())*8), targetType), nil 190 | } 191 | 192 | } 193 | 194 | if sourceType.Kind() == reflect.String { 195 | // Attempt to parse typical value types from the string 196 | switch targetType.Kind() { 197 | case reflect.Bool: 198 | b, err := strconv.ParseBool(S.String()) 199 | if err != nil { 200 | return nil, err 201 | } 202 | return kind2Exact(b, targetType), nil 203 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 204 | i, err := strconv.ParseInt(S.String(), 10, int(targetType.Size())*8) 205 | if err != nil { 206 | return nil, err 207 | } 208 | return kind2Exact(i, targetType), nil 209 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 210 | i, err := strconv.ParseUint(S.String(), 10, int(targetType.Size())*8) 211 | if err != nil { 212 | return nil, err 213 | } 214 | return kind2Exact(i, targetType), nil 215 | case reflect.Float32, reflect.Float64: 216 | f, err := strconv.ParseFloat(S.String(), int(targetType.Size())*8) 217 | if err != nil { 218 | return nil, err 219 | } 220 | return kind2Exact(f, targetType), nil 221 | } 222 | } 223 | 224 | // slice conversion 225 | if sourceType.Kind() == reflect.Slice && targetType.Kind() == reflect.Slice { 226 | return ce.convertSlice(source, targetType) 227 | } 228 | 229 | // map conversion 230 | if sourceType.Kind() == reflect.Map && targetType.Kind() == reflect.Map { 231 | return ce.convertMap(source, targetType) 232 | } 233 | 234 | // reflection-based conversion 235 | if reflect.TypeOf(source).ConvertibleTo(targetType) { 236 | return S.Convert(targetType).Interface(), nil 237 | } 238 | 239 | // no luck 240 | return nil, ErrIncompatibleType 241 | } 242 | 243 | // Set sets the given target pointer to sourcevalue, performing 244 | // any type conversion necessary 245 | func (ce *ConverterEngine) Set(target, source interface{}) error { 246 | T := reflect.ValueOf(target) 247 | if T.Kind() != reflect.Ptr { 248 | return ErrExpectedPointer 249 | } 250 | T = T.Elem() 251 | 252 | converted, err := ce.Convert(source, T.Type()) 253 | if err != nil { 254 | return err 255 | } 256 | T.Set(reflect.ValueOf(converted)) 257 | return nil 258 | } 259 | 260 | // Convert attempts to convert the source value to the given target type using the default engine 261 | // if it does not fail, the returned value is guaranteed to be of the target type 262 | func Convert(source interface{}, targetType reflect.Type) (interface{}, error) { 263 | return Default.Convert(source, targetType) 264 | } 265 | 266 | // Set sets the given target pointer to source value using the default engine 267 | // performing any type conversion necessary 268 | func Set(target, source interface{}) error { 269 | return Default.Set(target, source) 270 | } 271 | -------------------------------------------------------------------------------- /convert_test.go: -------------------------------------------------------------------------------- 1 | package elastic_test 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/epiclabs-io/elastic" 12 | 13 | "github.com/epiclabs-io/ut" 14 | ) 15 | 16 | var ErrAny = errors.New("Any error") 17 | 18 | type StringAlias string 19 | type FloatAlias float64 20 | type IntAlias int 21 | 22 | type TestStruct struct { 23 | X int 24 | Y int 25 | } 26 | 27 | func (ts *TestStruct) String() string { 28 | return fmt.Sprintf("(%d, %d)", ts.X, ts.Y) 29 | } 30 | 31 | // implement the converter interface 32 | func (ts *TestStruct) ConvertTo(targetType reflect.Type) (interface{}, error) { 33 | if targetType.Kind() == reflect.Float64 { 34 | return math.Sqrt(float64(ts.X*ts.X) + float64(ts.Y*ts.Y)), nil 35 | } 36 | return nil, elastic.ErrNoConversionAvailable 37 | } 38 | 39 | type ConversionTest struct { 40 | source interface{} 41 | expectedResult interface{} 42 | expectedError error 43 | } 44 | 45 | var testData = []ConversionTest{ 46 | {true, true, nil}, 47 | {"hello", "hello", nil}, // check no conversion when same types 48 | {int8(1), int8(1), nil}, 49 | {int16(2), int16(2), nil}, 50 | {int32(3), int32(3), nil}, 51 | {int64(4), int64(4), nil}, 52 | {int8(-1), int8(-1), nil}, 53 | {int16(-2), int16(-2), nil}, 54 | {int32(-3), int32(-3), nil}, 55 | {int64(-4), int64(-4), nil}, 56 | {uint8(5), uint8(5), nil}, 57 | {uint16(6), uint16(6), nil}, 58 | {uint32(7), uint32(7), nil}, 59 | {uint64(8), uint64(8), nil}, 60 | {float32(9.2), float32(9.2), nil}, 61 | {float64(194.2), float64(194.2), nil}, 62 | {"1", int8(1), nil}, 63 | {"2", int16(2), nil}, 64 | {"3", int32(3), nil}, 65 | {"4", int64(4), nil}, 66 | {"-1", int8(-1), nil}, 67 | {"-2", int16(-2), nil}, 68 | {"-3", int32(-3), nil}, 69 | {"-4", int64(-4), nil}, 70 | {"5", uint8(5), nil}, 71 | {"6", uint16(6), nil}, 72 | {"7", uint32(7), nil}, 73 | {"8", uint64(8), nil}, 74 | {"9.2", float32(9.2), nil}, 75 | {"19.3", float64(19.3), nil}, 76 | {"-9.2", float32(-9.2), nil}, 77 | {"-19.3", float64(-19.3), nil}, 78 | {int8(1), "1", nil}, 79 | {int16(2), "2", nil}, 80 | {int32(3), "3", nil}, 81 | {int64(4), "4", nil}, 82 | {int8(-1), "-1", nil}, 83 | {int16(-2), "-2", nil}, 84 | {int32(-3), "-3", nil}, 85 | {int64(-4), "-4", nil}, 86 | {uint8(5), "5", nil}, 87 | {uint16(6), "6", nil}, 88 | {uint32(7), "7", nil}, 89 | {uint64(8), "8", nil}, 90 | {float32(9.2), "9.2", nil}, 91 | {float64(19.3), "19.3", nil}, 92 | {float32(-9.2), "-9.2", nil}, 93 | {float64(-19.3), "-19.3", nil}, 94 | {int(-1), uint(0xffffffffffffffff), nil}, 95 | {"true", true, nil}, 96 | {"false", false, nil}, 97 | {true, "true", nil}, 98 | {false, "false", nil}, 99 | {5, float32(5), nil}, 100 | {[]interface{}{1, 2, 3, 4}, []int{1, 2, 3, 4}, nil}, 101 | {[]interface{}{"1", "2", "3", "-4"}, []int{1, 2, 3, -4}, nil}, 102 | {[]interface{}{"1.1", "2.2", "3.3", "-4.4"}, []float32{1.1, 2.2, 3.3, -4.4}, nil}, 103 | {[]interface{}{"1.1", "2.2", "3.3", "-4.4"}, []float64{1.1, 2.2, 3.3, -4.4}, nil}, 104 | {map[string]interface{}{ 105 | "uno": 1, 106 | "dos": 2, 107 | "tres": 3, 108 | }, map[string]int{ 109 | "uno": 1, 110 | "dos": 2, 111 | "tres": 3, 112 | }, nil}, 113 | {map[string]interface{}{ 114 | "1": "uno", 115 | "2": "dos", 116 | "3": "tres", 117 | }, map[int]string{ 118 | 1: "uno", 119 | 2: "dos", 120 | 3: "tres", 121 | }, nil}, 122 | {[]byte{65, 66, 67, 0}, "ABC\x00", nil}, 123 | {"ABC\x00", []byte{65, 66, 67, 0}, nil}, 124 | {"hola", StringAlias("hola"), nil}, // test conversion between alias 125 | {StringAlias("hola"), "hola", nil}, // test conversion between alias 126 | {"XYZ", int8(1), ErrAny}, // test an unparseable string returns an error 127 | {"XYZ", int16(2), ErrAny}, 128 | {"XYZ", int32(3), ErrAny}, 129 | {"XYZ", int64(4), ErrAny}, 130 | {"XYZ", int8(-1), ErrAny}, 131 | {"XYZ", int16(-2), ErrAny}, 132 | {"XYZ", int32(-3), ErrAny}, 133 | {"XYZ", int64(-4), ErrAny}, 134 | {"XYZ", uint8(5), ErrAny}, 135 | {"XYZ", uint16(6), ErrAny}, 136 | {"XYZ", uint32(7), ErrAny}, 137 | {"XYZ", uint64(8), ErrAny}, 138 | {"XYZ", float32(9.2), ErrAny}, 139 | {"XYZ", float64(19.3), ErrAny}, 140 | {"XYZ", float32(-9.2), ErrAny}, 141 | {"XYZ", float64(-19.3), ErrAny}, 142 | {true, 7, ErrAny}, 143 | {ConversionTest{}, 4, elastic.ErrIncompatibleType}, 144 | {&TestStruct{X: 5, Y: 7}, "(5, 7)", nil}, // test fmt.Stringer 145 | {&TestStruct{X: 5, Y: 7}, float64(8.602325267042627), nil}, // Test Converter implementation 146 | {&TestStruct{X: 5, Y: 7}, FloatAlias(8.602325267042627), nil}, // Test Converter implementation 147 | {&TestStruct{X: 5, Y: 7}, StringAlias("(5, 7)"), nil}, // test fmt.Stringer implementation to an alias type 148 | {[]byte{0, 0, 0, 1, 0, 0, 0, 2}, TestStruct{X: 1, Y: 2}, nil}, // Test Target converter 149 | {&TestStruct{X: 5, Y: 7}, 99, elastic.ErrIncompatibleType}, 150 | {FloatAlias(2.2), int(2), nil}, // test Source converter 151 | {FloatAlias(2.7), int(3), nil}, // test Source converter 152 | {FloatAlias(2.7), IntAlias(3), nil}, // test Source converter 153 | {float32(5.5), float64(5.5), nil}, // test upgrade/downgrade 154 | {float64(5.5), float32(5.5), nil}, // test upgrade/downgrade 155 | } 156 | 157 | func TestConvert(tx *testing.T) { 158 | t := ut.BeginTest(tx, false) // set to true to generate test results 159 | defer t.FinishTest() 160 | 161 | // The following adds a custom converter that rounds floats when converting them to integers 162 | elastic.Default.AddSourceConverter(reflect.TypeOf(FloatAlias(0)), func(source interface{}, targetType reflect.Type) (interface{}, error) { 163 | if targetType.Kind() == reflect.Int { 164 | f := source.(FloatAlias) 165 | d := f - FloatAlias(int(f)) 166 | if d > 0.5 { 167 | return int(f) + 1, nil 168 | } 169 | return int(f), nil 170 | } 171 | return nil, elastic.ErrNoConversionAvailable 172 | }) 173 | 174 | // The following adds a custom target converter that unpacks a TestStruct out of a byte array 175 | elastic.Default.AddTargetConverter(reflect.TypeOf(TestStruct{}), func(source interface{}, targetType reflect.Type) (interface{}, error) { 176 | switch source.(type) { 177 | case []byte: 178 | b := source.([]byte) 179 | if len(b) == 8 { 180 | return TestStruct{ 181 | X: int(binary.BigEndian.Uint32(b)), 182 | Y: int(binary.BigEndian.Uint32(b[4:])), 183 | }, nil 184 | } 185 | } 186 | return nil, elastic.ErrNoConversionAvailable 187 | }) 188 | 189 | // run all tests 190 | // each conversion test has a source value that will be converted to the type 191 | // of the expected result. Then the result is compared to the expected result. 192 | for _, ct := range testData { 193 | t.StartSubTest("Conversion of '%v' (%s) to %s", ct.source, reflect.TypeOf(ct.source), reflect.TypeOf(ct.expectedResult).String()) 194 | r, err := elastic.Convert(ct.source, reflect.TypeOf(ct.expectedResult)) 195 | if ct.expectedError == nil { 196 | t.Ok(err) // verify no error 197 | t.Equals(ct.expectedResult, r) // compare values to see if conversion was correct 198 | } else { 199 | if ct.expectedError == ErrAny { 200 | t.MustFail(err, "Conversion should have failed") 201 | } else { 202 | t.MustFailWith(err, ct.expectedError) 203 | } 204 | } 205 | 206 | // run the same test but using `elastic.Set` instead 207 | target := reflect.New(reflect.TypeOf(ct.expectedResult)) 208 | err = elastic.Set(target.Interface(), ct.source) 209 | if ct.expectedError == nil { 210 | t.Ok(err) 211 | t.Equals(ct.expectedResult, target.Elem().Interface()) 212 | } else { 213 | if ct.expectedError == ErrAny { 214 | t.MustFail(err, "Conversion should have failed") 215 | } else { 216 | t.MustFailWith(err, ct.expectedError) 217 | } 218 | } 219 | } 220 | 221 | // Test `Set` fails when the first parameter is not a pointer 222 | var x int 223 | err := elastic.Set(x, 4) 224 | t.MustFailWith(err, elastic.ErrExpectedPointer) 225 | 226 | } 227 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "reflect" 8 | 9 | "github.com/epiclabs-io/elastic" 10 | ) 11 | 12 | // Vector is a sample structure that represents a vector 13 | type Vector struct { 14 | X float64 15 | Y float64 16 | } 17 | 18 | func main() { 19 | 20 | var f interface{} = float64(5.5) 21 | var i int 22 | 23 | // convert value types 24 | // note that using convert wouldn't make sense if you are certain 25 | // f is a float64 at compile time. 26 | err := elastic.Set(&i, f) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | fmt.Println(i) 32 | 33 | var ints []int 34 | err = elastic.Set(&ints, []interface{}{1, 2, 3, "4", float64(5), 6}) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | fmt.Println(ints) 40 | 41 | someMap := map[string]interface{}{ 42 | "1": "uno", 43 | "2": "dos", 44 | "3": "tres", 45 | } 46 | 47 | intmap := make(map[int]string) 48 | err = elastic.Set(&intmap, someMap) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | fmt.Println(intmap) 53 | 54 | // Add a custom converter to convert Vector to float64 55 | // (Calculates the modulus of the vector) 56 | elastic.Default.AddSourceConverter(reflect.TypeOf(Vector{}), func(source interface{}, targetType reflect.Type) (interface{}, error) { 57 | vector := source.(Vector) 58 | switch targetType.Kind() { 59 | case reflect.Float64, reflect.Int: 60 | return math.Sqrt(float64(vector.X*vector.X) + float64(vector.Y*vector.Y)), nil 61 | case reflect.String: 62 | return fmt.Sprintf("(%g, %g)", vector.X, vector.Y), nil 63 | } 64 | return nil, elastic.ErrNoConversionAvailable 65 | 66 | }) 67 | 68 | // Add a custom converter to convert a string to vector 69 | elastic.Default.AddSourceConverter(reflect.TypeOf(string("")), func(source interface{}, targetType reflect.Type) (interface{}, error) { 70 | switch targetType { 71 | case reflect.TypeOf(Vector{}): 72 | var v Vector 73 | _, err := fmt.Sscanf(source.(string), "(%g, %g)", &v.X, &v.Y) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return v, nil 78 | } 79 | return nil, elastic.ErrNoConversionAvailable 80 | 81 | }) 82 | 83 | v := Vector{ 84 | X: 3.0, 85 | Y: 4.0, 86 | } 87 | 88 | f, err = elastic.Convert(v, reflect.TypeOf(float64(0))) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | fmt.Println(f) // prints 5 93 | 94 | var n int 95 | elastic.Set(&n, v) 96 | fmt.Println(n) // prints 5 97 | 98 | var s string 99 | elastic.Set(&s, v) 100 | fmt.Println(s) // prints (3, 4) 101 | 102 | elastic.Set(&v, "(2, 8)") 103 | fmt.Println(v) // prints {2, 8} 104 | 105 | } 106 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/epiclabs-io/elastic 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/epiclabs-io/diff3 v0.0.0-20181217103619-05282cece609 // indirect 7 | github.com/epiclabs-io/ut v0.0.0-20190416122157-8da7fe4b4947 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/epiclabs-io/convert v0.0.0-20200225195501-b49147ddae2f h1:o/dJYDO8UPUy095NfFob+V5czl5RD7M6njx72/Irsew= 2 | github.com/epiclabs-io/diff3 v0.0.0-20181217103619-05282cece609 h1:KHcpmcC/8cnCDXDm6SaCTajWF/vyUbBE1ovA27xYYEY= 3 | github.com/epiclabs-io/diff3 v0.0.0-20181217103619-05282cece609/go.mod h1:tM499ZoH5jQRF3wlMnl59SJQwVYXIBdJRZa/K71p0IM= 4 | github.com/epiclabs-io/ut v0.0.0-20190416122157-8da7fe4b4947 h1:5jyZq+mwwE90FnIyzAorlWF0Nrg8AB48KsDxofSAyBw= 5 | github.com/epiclabs-io/ut v0.0.0-20190416122157-8da7fe4b4947/go.mod h1:Sm6PW7b/nLOHEn3XxuUOXFYA4xFkLUnyAWUOcTGcRZ4= 6 | --------------------------------------------------------------------------------