github.com/simpleiot/simpleiot@v0.18.3/data/encode.go (about) 1 package data 2 3 import ( 4 "fmt" 5 "reflect" 6 "strconv" 7 "strings" 8 ) 9 10 // maxSafeInteger is the largest integer value that can be stored in a float64 11 // (i.e. Point value) without losing precision. 12 const maxSafeInteger = 1<<53 - 1 13 14 // maxStructureSize is the largest array / map / struct that will be converted 15 // to an array of Points 16 const maxStructureSize = 1000 17 18 func pointFromPrimitive(pointType string, v reflect.Value) (Point, error) { 19 p := Point{Type: pointType} 20 k := v.Kind() 21 22 if k == reflect.Pointer { 23 if v.IsNil() { 24 p.Tombstone = 1 25 return p, nil 26 } 27 v = v.Elem() 28 k = v.Kind() 29 } 30 switch k { 31 case reflect.Bool: 32 p.Value = BoolToFloat(v.Bool()) 33 case reflect.Int, 34 reflect.Int8, 35 reflect.Int16, 36 reflect.Int32, 37 reflect.Int64: 38 39 val := v.Int() 40 if val > maxSafeInteger || val < -maxSafeInteger { 41 return p, fmt.Errorf("float64 overflow for value: %v", val) 42 } 43 p.Value = float64(val) 44 case reflect.Uint, 45 reflect.Uint8, 46 reflect.Uint16, 47 reflect.Uint32, 48 reflect.Uint64: 49 50 val := v.Uint() 51 if val > maxSafeInteger { 52 return p, fmt.Errorf("float64 overflow for value: %v", val) 53 } 54 p.Value = float64(val) 55 case reflect.Float32, reflect.Float64: 56 p.Value = v.Float() 57 case reflect.String: 58 p.Text = v.String() 59 default: 60 return p, fmt.Errorf("unsupported type: %v", k) 61 } 62 return p, nil 63 } 64 func appendPointsFromValue( 65 points []Point, 66 pointType string, 67 v reflect.Value, 68 ) ([]Point, error) { 69 t := v.Type() 70 k := t.Kind() 71 switch k { 72 case reflect.Array, reflect.Slice: 73 // Points support arrays / slices of supported primitives 74 if v.Len() > maxStructureSize { 75 return points, fmt.Errorf( 76 "%v length of %v exceeds maximum of %v", 77 k, v.Len(), maxStructureSize, 78 ) 79 } 80 for i := 0; i < v.Len(); i++ { 81 p, err := pointFromPrimitive(pointType, v.Index(i)) 82 if err != nil { 83 return points, fmt.Errorf("%v of %w", k, err) 84 } 85 p.Key = strconv.Itoa(i) 86 points = append(points, p) 87 } 88 case reflect.Map: 89 // Points support maps with string keys 90 if keyK := t.Key().Kind(); keyK != reflect.String { 91 return points, fmt.Errorf("unsupported type: map keyed by %v", keyK) 92 } 93 if v.Len() > maxStructureSize { 94 return points, fmt.Errorf( 95 "%v length of %v exceeds maximum of %v", 96 k, v.Len(), maxStructureSize, 97 ) 98 } 99 iter := v.MapRange() 100 for iter.Next() { 101 mKey, mVal := iter.Key(), iter.Value() 102 p, err := pointFromPrimitive(pointType, mVal) 103 if err != nil { 104 return points, fmt.Errorf("map contains %w", err) 105 } 106 p.Key = mKey.String() 107 points = append(points, p) 108 } 109 case reflect.Struct: 110 // Points support "flat" structs, and they are treated like maps 111 // Key name is taken from struct "point" tag or from the field name 112 numField := t.NumField() 113 if numField > maxStructureSize { 114 return points, fmt.Errorf( 115 "%v size of %v exceeds maximum of %v", 116 k, numField, maxStructureSize, 117 ) 118 } 119 for i := 0; i < numField; i++ { 120 sf := t.Field(i) 121 key := sf.Tag.Get("point") 122 if key == "" { 123 key = sf.Tag.Get("edgepoint") 124 } 125 if key == "" { 126 key = ToCamelCase(sf.Name) 127 } 128 p, err := pointFromPrimitive(pointType, v.Field(i)) 129 if err != nil { 130 return points, fmt.Errorf("struct contains %w", err) 131 } 132 p.Key = key 133 points = append(points, p) 134 } 135 case reflect.Pointer: 136 // We support pointers to primitives and structs 137 // If the pointer is nil, all generated points will have a tombstone set 138 if !v.IsNil() { 139 return appendPointsFromValue(points, pointType, v.Elem()) 140 } 141 switch k := t.Elem().Kind(); k { 142 case reflect.Struct: 143 // Generate a tombstone point for all struct fields 144 numField := t.Elem().NumField() 145 if numField > maxStructureSize { 146 return points, fmt.Errorf( 147 "%v size of %v exceeds maximum of %v", 148 k, numField, maxStructureSize, 149 ) 150 } 151 for i := 0; i < numField; i++ { 152 sf := t.Elem().Field(i) 153 key := sf.Tag.Get("point") 154 if key == "" { 155 key = sf.Tag.Get("edgepoint") 156 } 157 if key == "" { 158 key = ToCamelCase(sf.Name) 159 } 160 p := Point{ 161 Type: pointType, 162 Key: key, 163 Tombstone: 1, 164 } 165 points = append(points, p) 166 } 167 case reflect.Bool, 168 reflect.Int, 169 reflect.Int8, 170 reflect.Int16, 171 reflect.Int32, 172 reflect.Int64, 173 reflect.Uint, 174 reflect.Uint8, 175 reflect.Uint16, 176 reflect.Uint32, 177 reflect.Uint64, 178 reflect.Float32, 179 reflect.Float64, 180 reflect.String: 181 points = append(points, Point{Type: pointType, Tombstone: 1}) 182 default: 183 return points, fmt.Errorf("unsupported pointer type: %v", k) 184 } 185 default: 186 p, err := pointFromPrimitive(pointType, v) 187 if err != nil { 188 return points, err 189 } 190 points = append(points, p) 191 } 192 return points, nil 193 } 194 195 // ToCamelCase naively converts a string to camelCase. This function does 196 // not consider common initialisms. 197 func ToCamelCase(s string) string { 198 // Find first lowercase letter 199 lowerIndex := strings.IndexFunc(s, func(c rune) bool { 200 return 'a' <= c && c <= 'z' 201 }) 202 if lowerIndex < 0 { 203 // ALLUPPERCASE 204 s = strings.ToLower(s) 205 } else if lowerIndex == 1 { 206 // FirstLetterUppercase 207 s = strings.ToLower(s[0:lowerIndex]) + s[lowerIndex:] 208 } else if lowerIndex > 1 { 209 // MANYLettersUppercase 210 s = strings.ToLower(s[0:lowerIndex-1]) + s[lowerIndex-1:] 211 } 212 return s 213 } 214 215 // Encode is used to convert a user struct to 216 // a node. in must be a struct type that contains 217 // node, point, and edgepoint tags as shown below. 218 // It is recommended that id and parent node tags 219 // always be included. 220 // 221 // type exType struct { 222 // ID string `node:"id"` 223 // Parent string `node:"parent"` 224 // Description string `point:"description"` 225 // Count int `point:"count"` 226 // Role string `edgepoint:"role"` 227 // Tombstone bool `edgepoint:"tombstone"` 228 // } 229 func Encode(in any) (NodeEdge, error) { 230 inV, inT, inK := reflectValue(in) 231 nodeType := ToCamelCase(inT.Name()) 232 ret := NodeEdge{Type: nodeType} 233 234 if inK != reflect.Struct { 235 return ret, fmt.Errorf("error decoding to %v; must be a struct", inK) 236 } 237 238 var err error 239 for i := 0; i < inT.NumField(); i++ { 240 sf := inT.Field(i) 241 if pt := sf.Tag.Get("point"); pt != "" { 242 ret.Points, err = appendPointsFromValue( 243 ret.Points, pt, inV.Field(i), 244 ) 245 if err != nil { 246 return ret, err 247 } 248 } else if et := sf.Tag.Get("edgepoint"); et != "" { 249 ret.EdgePoints, err = appendPointsFromValue( 250 ret.EdgePoints, et, inV.Field(i), 251 ) 252 if err != nil { 253 return ret, err 254 } 255 } else if nt := sf.Tag.Get("node"); nt != "" && 256 sf.Type.Kind() == reflect.String { 257 258 if nt == "id" { 259 ret.ID = inV.Field(i).String() 260 } else if nt == "parent" { 261 ret.Parent = inV.Field(i).String() 262 } 263 } 264 } 265 266 return ret, nil 267 } 268 269 // DiffPoints compares a before and after struct and generates the set of Points 270 // that represent their differences. 271 func DiffPoints[T any](before, after T) (Points, error) { 272 bV, t, k := reflectValue(before) 273 aV, _, _ := reflectValue(after) 274 275 // Check to ensure this is a struct 276 if k != reflect.Struct { 277 return nil, fmt.Errorf("error decoding to %v; must be a struct", k) 278 } 279 280 points := Points{} 281 for i, numFields := 0, t.NumField(); i < numFields; i++ { 282 // Determine point type from struct tag 283 structTag := t.Field(i).Tag 284 pointType := structTag.Get("point") 285 if pointType == "" { 286 continue 287 } 288 289 bFieldV := bV.Field(i) 290 aFieldV := aV.Field(i) 291 292 // Handle special case of pointer to a struct 293 if bFieldV.Kind() == reflect.Pointer && 294 bFieldV.Type().Elem().Kind() == reflect.Struct { 295 // If new pointer is nil, set all fields to tombstone, else 296 // proceed 297 if bFieldV.IsNil() && aFieldV.IsNil() { 298 // do nothing 299 continue 300 } else if aFieldV.IsNil() { 301 // Generate a tombstone point for all struct fields 302 t := bFieldV.Type().Elem() 303 numField := t.NumField() 304 if numField > maxStructureSize { 305 return points, fmt.Errorf( 306 "%v size of %v exceeds maximum of %v", 307 k, numField, maxStructureSize, 308 ) 309 } 310 for i := 0; i < numField; i++ { 311 sf := t.Field(i) 312 key := sf.Tag.Get("point") 313 if key == "" { 314 key = ToCamelCase(sf.Name) 315 } 316 p := Point{ 317 Type: pointType, 318 Key: key, 319 Tombstone: 1, 320 } 321 points.Add(p) 322 } 323 continue 324 } else if bFieldV.IsNil() { 325 var err error 326 points, err = appendPointsFromValue( 327 points, 328 pointType, 329 aFieldV.Elem(), 330 ) 331 if err != nil { 332 return points, err 333 } 334 continue 335 } 336 337 bFieldV = bFieldV.Elem() 338 aFieldV = aFieldV.Elem() 339 } 340 341 switch bFieldV.Kind() { 342 case reflect.Array, reflect.Slice: 343 if aFieldV.Len() > maxStructureSize { 344 return points, fmt.Errorf( 345 "%v length of %v exceeds maximum of %v", 346 k, aFieldV.Len(), maxStructureSize, 347 ) 348 } 349 i, aFieldLen, bFieldLen := 0, aFieldV.Len(), bFieldV.Len() 350 for ; i < aFieldLen; i++ { 351 if i >= bFieldLen || !aFieldV.Index(i).Equal(bFieldV.Index(i)) { 352 // Add / update point 353 p, err := pointFromPrimitive(pointType, aFieldV.Index(i)) 354 if err != nil { 355 return points, fmt.Errorf("%v of %w", k, err) 356 } 357 p.Key = strconv.Itoa(i) 358 points.Add(p) 359 } 360 } 361 for i = bFieldLen - 1; i >= aFieldLen; i-- { 362 // Create tombstone point 363 points.Add(Point{ 364 Type: pointType, 365 Key: strconv.Itoa(i), 366 Tombstone: 1, 367 }) 368 } 369 case reflect.Map: 370 // Points support maps with string keys 371 if keyK := bFieldV.Type().Key().Kind(); keyK != reflect.String { 372 return points, fmt.Errorf("unsupported type: map keyed by %v", keyK) 373 } 374 if aFieldV.Len() > maxStructureSize { 375 return points, fmt.Errorf( 376 "%v length of %v exceeds maximum of %v", 377 k, aFieldV.Len(), maxStructureSize, 378 ) 379 } 380 // Populate keysToDelete with all keys from `before` map 381 keysToDelete := make(map[string]bool) 382 iter := bFieldV.MapRange() 383 for iter.Next() { 384 keysToDelete[iter.Key().String()] = true 385 } 386 // Now iterate over `after` map 387 iter = aFieldV.MapRange() 388 for iter.Next() { 389 mKey, mVal := iter.Key(), iter.Value() 390 if !mVal.Equal(bFieldV.MapIndex(mKey)) { 391 // Add / update key 392 p, err := pointFromPrimitive(pointType, mVal) 393 if err != nil { 394 return points, fmt.Errorf("map contains %w", err) 395 } 396 p.Key = mKey.String() 397 points.Add(p) 398 } 399 delete(keysToDelete, mKey.String()) 400 } 401 for key := range keysToDelete { 402 // Create tombstone point 403 points.Add(Point{ 404 Type: pointType, 405 Key: key, 406 Tombstone: 1, 407 }) 408 } 409 case reflect.Struct: 410 // Points support "flat" structs, and they are treated like maps 411 // Key name is taken from struct "point" tag or from the field name 412 t := bFieldV.Type() 413 numField := t.NumField() 414 if numField > maxStructureSize { 415 return points, fmt.Errorf( 416 "%v size of %v exceeds maximum of %v", 417 k, numField, maxStructureSize, 418 ) 419 } 420 for i := 0; i < numField; i++ { 421 sf := t.Field(i) 422 key := sf.Tag.Get("point") 423 if key == "" { 424 key = ToCamelCase(sf.Name) 425 } 426 if !bFieldV.Field(i).Equal(aFieldV.Field(i)) { 427 // Update key 428 p, err := pointFromPrimitive(pointType, aFieldV.Field(i)) 429 if err != nil { 430 return points, fmt.Errorf("struct contains %w", err) 431 } 432 p.Key = key 433 points.Add(p) 434 } 435 } 436 default: 437 if !bFieldV.Equal(aFieldV) { 438 // Update point 439 p, err := pointFromPrimitive(pointType, aFieldV) 440 if err != nil { 441 return points, err 442 } 443 points.Add(p) 444 } 445 } 446 } 447 return points, nil 448 }