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  }