Browse Source

Added current state of POC Astruct (Auto Struct)

Auto Structures currently only supports AssumeStruct == false!
Auto Structures don't quite make use of structures which end up being
produced.
Auto Structures currently support dynamic any or interface{} types, this
should change to either any or error out.
Apollo 1 year ago
parent
commit
dc6b2355ba
7 changed files with 495 additions and 1 deletions
  1. 22 1
      README.md
  2. 366 0
      astrut.go
  3. 41 0
      example/main.go
  4. 15 0
      example/normal.json
  5. 44 0
      example/test.json
  6. 5 0
      go.mod
  7. 2 0
      go.sum

+ 22 - 1
README.md

@@ -1,3 +1,24 @@
 # astruct
 
-Auto structures in Go
+Auto structures in Go
+
+## BUGS
+
+* Currently we assume if 1 type is able to be int64 then we use int64, but it might not be able to (it might actually be float64)
+* Need to fix it so if AssumeStruct is true then we make structures (always), else we only make a structure if the types vary
+* Fix it so if we make a structure (because AssumeStruct is true) then anywhere we'd have the same field use that structure instead
+* Fix numeric structure names when there is no prefix
+* Verify this is valid go code
+
+## To Remove
+
+* UseInterface (because we already expect go1.20.3, rather what we built with) use `any` rather than `interface{}` adaptable
+
+## Missing Features
+
+* Build a better WriteFile where it generates each structure then when we do a final WriteFile we merge all that together (basically optimize the WriteFile process so it's cleaner and less huge)
+
+## Gothonic
+
+* Work on breaking down the many complex and huge methods/functions and get a series of smaller methods/functions built
+* Use `any` instead of `interface{}`, and drop old versions of Go.

+ 366 - 0
astrut.go

@@ -0,0 +1,366 @@
+package astruct
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"reflect"
+	"strings"
+
+	"golang.org/x/exp/maps"
+	"golang.org/x/exp/slices"
+)
+
+type Map map[string]reflect.Kind
+
+// The main structure to use with astruct
+//
+// This structure contains options to configure behavior and methods in which to parse (JSON) and output (structs in source code)
+type Astruct struct {
+	// Options
+
+	Use32Bit         bool   // Defaults to false, using 64-bit int and 64-bit float, when true, uses 32-bit variants of int and float
+	UseAny           bool   // Defaults to true, when false the key word any will be replaced with interface{} (unless UseInterface is false, in which error is thrown)
+	UseInterface     bool   // Defaults to true, when astruct isn't able to determine a type (being an array with multiple types) it will fallback to interface{} (unless UseAny is true in which any is used)
+	VerboseLogging   bool   // Dump verbose info into logs (User must setup log first!)
+	AssumeStruct     bool   // Even if a type could be a map it will make a structure for it instead, default false (instead it will try to use a map unless varying types detected)
+	PrefixStructName string // If not an empty "" string, Structures will be prefixed (for cases where you're going to have a single file produced with various structures from astruct, helps prevent name collisions, which would normally error out)
+
+	// In-Memory Data (This data is gathered to form final products)
+	// Almost all of these will be key being field name for in a structure (if one needs to be built) and value of reflect.Kind to determine:
+	// 1. If the given thing was seen before (as with the case of say an []struct) or if that isn't possible as it's something else (in which a structure might not be possible)
+	// 2. If a map is actually better represented as a structure (In the case of AssumeStruct the map won't be an option instead a map[string]struct would be used, or even []struct)
+
+	MapLikes   map[string]Map            // Holds map-like structures which might be better defined as structures if they have varying types (else it will retain a single type being a map)
+	ArrayLikes map[string][]reflect.Kind // Holds array-like structures (this includes slices) which might be able to be defined as arrays (but if varying types are encountered then it might end up as []any or []interface{})
+	Parent     any                       // Position just above the Current position (for when we're done parsing this "depth", or if this is nil to indicate we're done)
+	Depth      uint                      // Tracks how many nested structures (map, array/slice) deep we are
+}
+
+// Initializes to default options (this also resets the In-Memory Data to an initialized state)
+func (A *Astruct) Defaults() {
+	A.Use32Bit = false
+	A.UseAny = true
+	A.UseInterface = true
+	A.VerboseLogging = false
+	A.AssumeStruct = false
+	A.PrefixStructName = ""
+	A.Init()
+}
+
+// This resets the In-Memory Data to an initialized state
+func (A *Astruct) Init() {
+	A.MapLikes = make(map[string]Map)
+	A.ArrayLikes = make(map[string][]reflect.Kind)
+	A.Parent = nil
+	A.Depth = 0
+}
+
+// Creates a new Astruct, if given a Prefix for generated structs (only the first one) will be used
+func NewAstruct(PrefixStructName ...string) *Astruct {
+	A := &Astruct{}
+	A.Defaults()
+	if len(PrefixStructName) != 0 {
+		A.PrefixStructName = CamelCase(PrefixStructName[0])
+	}
+	return A
+}
+
+func (A *Astruct) ReadFile(filename string) error {
+	data, err := os.ReadFile(filename)
+	if err != nil {
+		return fmt.Errorf("astruct.Astruct.ReadFile(filename='%s') On os.ReadFile > %w", filename, err)
+	}
+	var root map[string]any
+	err = json.Unmarshal(data, &root)
+	if err != nil {
+		return fmt.Errorf("astruct.Astruct.ReadFile(filename='%s') On json.Unmarshal > %w", filename, err)
+	}
+	// Begin parsing...
+	A.parse(root, "")
+	return nil
+}
+
+func (A *Astruct) parse(at any, name string) error {
+	switch reflect.TypeOf(at).Kind() {
+	case reflect.Map:
+		m := at.(map[string]any)
+		var last_type reflect.Kind = 0
+		var same_type bool = true
+		for k, v := range m {
+			fmt.Printf(strings.Repeat("  ", int(A.Depth))+"'%s' = %s\n", k, reflect.TypeOf(v).Kind().String())
+			if same_type {
+				if last_type == 0 {
+					last_type = reflect.TypeOf(v).Kind()
+				} else if reflect.TypeOf(v).Kind() != last_type {
+					same_type = false
+				}
+			}
+			switch reflect.TypeOf(v).Kind() {
+			case reflect.Map:
+				A.Depth += 1
+				err := A.parse(v, k)
+				if err != nil {
+					return err
+				}
+				A.Depth -= 1
+			case reflect.Array, reflect.Slice:
+				A.Depth += 1
+				err := A.parse(v, k)
+				if err != nil {
+					return err
+				}
+				A.Depth -= 1
+			}
+		}
+		A.MapLikes[name] = make(Map)
+		if !same_type {
+			for k, v := range m {
+				if reflect.TypeOf(v).Kind() == reflect.Float32 || reflect.TypeOf(v).Kind() == reflect.Float64 {
+					f := v.(float64)
+					if IsInt(f) {
+						A.MapLikes[name][k] = reflect.Int64
+					} else {
+						A.MapLikes[name][k] = reflect.Float64
+					}
+				} else {
+					A.MapLikes[name][k] = reflect.TypeOf(v).Kind()
+				}
+			}
+		} else {
+			A.MapLikes[name]["all"] = reflect.TypeOf(m[maps.Keys(m)[0]]).Kind()
+		}
+	case reflect.Array, reflect.Slice:
+		a := at.([]any)
+		var last_type reflect.Kind = 0
+		var same_type bool = true
+		for i, v := range a {
+			fmt.Printf(strings.Repeat("  ", int(A.Depth))+"%d = %s\n", i, reflect.TypeOf(v).Kind().String())
+			if same_type {
+				if last_type == 0 {
+					last_type = reflect.TypeOf(v).Kind()
+				} else if reflect.TypeOf(v).Kind() != last_type {
+					same_type = false
+				}
+			}
+			switch reflect.TypeOf(v).Kind() {
+			case reflect.Map:
+				A.Depth += 1
+				err := A.parse(v, fmt.Sprint(i))
+				if err != nil {
+					return err
+				}
+				A.Depth -= 1
+			case reflect.Array, reflect.Slice:
+				A.Depth += 1
+				err := A.parse(v, fmt.Sprint(i))
+				if err != nil {
+					return err
+				}
+				A.Depth -= 1
+			}
+		}
+		A.ArrayLikes[name] = []reflect.Kind{}
+		if !same_type {
+			for _, v := range a {
+				if reflect.TypeOf(v).Kind() == reflect.Float32 || reflect.TypeOf(v).Kind() == reflect.Float64 {
+					f := v.(float64)
+					if IsInt(f) {
+						A.ArrayLikes[name] = append(A.ArrayLikes[name], reflect.Int64)
+					} else {
+						A.ArrayLikes[name] = append(A.ArrayLikes[name], reflect.Float64)
+					}
+				} else {
+					A.ArrayLikes[name] = append(A.ArrayLikes[name], reflect.TypeOf(v).Kind())
+				}
+			}
+		} else {
+			v := a[0]
+			if reflect.TypeOf(v).Kind() == reflect.Float32 || reflect.TypeOf(v).Kind() == reflect.Float64 {
+				f := v.(float64)
+				if IsInt(f) {
+					A.ArrayLikes[name] = append(A.ArrayLikes[name], reflect.Int64)
+				} else {
+					A.ArrayLikes[name] = append(A.ArrayLikes[name], reflect.Float64)
+				}
+			} else {
+				A.ArrayLikes[name] = append(A.ArrayLikes[name], reflect.TypeOf(v).Kind())
+			}
+		}
+	default:
+		fmt.Printf(strings.Repeat("  ", int(A.Depth))+"%s\n", reflect.TypeOf(at).Kind().String())
+	}
+	return nil
+}
+
+// Checks if the given JSON Number (represented as float64) can be a int64 instead
+func IsInt(v float64) bool {
+	str := fmt.Sprintf("%f", v)
+	fmt.Printf("'%s' = ", str)
+	var hit_dot bool = false
+	for _, r := range str {
+		if r == '.' {
+			hit_dot = true
+			continue
+		}
+		if hit_dot {
+			if r != '0' {
+				fmt.Println("false")
+				return false
+			}
+		}
+	}
+	fmt.Println("true")
+	return true
+}
+
+// Checks if the given data (either map or array/slice) contains the same types
+//
+// Used to identify if a map or array/slice could be used (unless AssumeStruct is true in which a struct will always be made)
+func SameType(data any) bool {
+	if reflect.TypeOf(data) == reflect.TypeOf(Map{}) {
+		m := data.(Map)
+		var last_type reflect.Kind = 0
+		for _, v := range m {
+			if last_type == 0 {
+				last_type = v
+			} else if v != last_type {
+				return false
+			}
+		}
+		return true
+	}
+	switch reflect.TypeOf(data).Kind() {
+	case reflect.Map:
+		m := data.(map[string]any)
+		var last_type reflect.Kind = 0
+		for _, v := range m {
+			if last_type == 0 {
+				last_type = reflect.TypeOf(v).Kind()
+			} else if reflect.TypeOf(v).Kind() != last_type {
+				return false
+			}
+		}
+		return true
+	case reflect.Array, reflect.Slice:
+		a := data.([]any)
+		var last_type reflect.Kind = 0
+		for _, v := range a {
+			if last_type == 0 {
+				last_type = reflect.TypeOf(v).Kind()
+			} else if reflect.TypeOf(v).Kind() != last_type {
+				return false
+			}
+		}
+		return true
+	}
+	return false
+}
+
+func CamelCase(field string) string {
+	if len(field) == 0 {
+		return ""
+	}
+	var result string
+	if !strings.Contains(field, " ") || !strings.Contains(field, "-") {
+		result += strings.ToUpper(string(field[0]))
+		result += field[1:]
+		return result
+	}
+	result = strings.ToTitle(field)
+	result = strings.ReplaceAll(result, " ", "")
+	result = strings.ReplaceAll(result, "-", "")
+	return result
+}
+
+func (A *Astruct) WriteFile(filename, packageName string, permissions os.FileMode) error {
+	if packageName == "" {
+		return fmt.Errorf("astruct.Astruct.WriteFile(filename='%s', packageName='%s', permissions=%d) > %s", filename, packageName, permissions, "Missing 'packageName', this must not be empty!")
+	}
+	var successful bool = true
+	fh, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, permissions)
+	if err != nil {
+		return fmt.Errorf("astruct.Astruct.WriteFile(filename='%s', packageName='%s', permissions=%d) On os.OpenFile > %w", filename, packageName, permissions, err)
+	}
+	defer func() {
+		fh.Close()
+		if !successful {
+			err = os.Remove(filename)
+			if err != nil {
+				log.Panicf("astruct.Astruct.WriteFile(filename='%s', packageName='%s', permissions=%d) On os.Remove > %v", filename, packageName, permissions, err)
+			}
+		}
+	}()
+	fh.WriteString(fmt.Sprintf("package %s\n\n", packageName))
+	//var slices_to_structs []string = []string{}
+	var seen []string = []string{}
+	for name, m := range A.MapLikes {
+		if name == "all" { // Ignore the root node for now
+			continue
+		}
+		if slices.Contains(seen, name) {
+			continue
+		}
+		if !A.AssumeStruct || !SameType(m) {
+			fh.WriteString(fmt.Sprintf("type %s%s struct {\n", A.PrefixStructName, CamelCase(name)))
+			for k, v := range m {
+				if slices.Contains(maps.Keys(A.MapLikes), k) {
+					if SameType(A.MapLikes[k]) {
+						fh.WriteString(fmt.Sprintf("\t%s map[string]%s\n", CamelCase(k), A.MapLikes[k]["all"].String()))
+						seen = append(seen, k)
+					} else {
+						fh.WriteString(fmt.Sprintf("\t%s %s%s\n", CamelCase(k), A.PrefixStructName, CamelCase(k)))
+					}
+				} else {
+					if slices.Contains(maps.Keys(A.ArrayLikes), k) {
+						a := A.ArrayLikes[k]
+						if len(a) == 1 {
+							if a[0] == reflect.Float64 && A.Use32Bit {
+								fh.WriteString(fmt.Sprintf("\t%s []float32\n", CamelCase(k)))
+								continue
+							} else if a[0] == reflect.Int64 && A.Use32Bit {
+								fh.WriteString(fmt.Sprintf("\t%s []int32\n", CamelCase(k)))
+								continue
+							} else if a[0] == reflect.Map && A.AssumeStruct {
+								fh.WriteString(fmt.Sprintf("\t%s []%s\n", CamelCase(k), CamelCase(k)))
+								continue
+							} else if a[0] == reflect.Map && !A.AssumeStruct {
+								fh.WriteString(fmt.Sprintf("\t%s []%s%d\n", CamelCase(k), CamelCase(k), 0))
+								continue
+							}
+							fh.WriteString(fmt.Sprintf("\t%s []%s\n", CamelCase(k), a[0]))
+						} else {
+							//fh.WriteString(fmt.Sprintf("\t%s []%s\n", CamelCase(k), CamelCase(k)))
+							//slices_to_structs = append(slices_to_structs, k)
+							if A.UseAny && A.UseInterface {
+								fh.WriteString(fmt.Sprintf("\t%s []any\n", CamelCase(k)))
+							} else if !A.UseAny && A.UseInterface {
+								fh.WriteString(fmt.Sprintf("\t%s []interface{}\n", CamelCase(k)))
+							} else {
+								return fmt.Errorf("astruct.Astruct.WriteFile(filename='%s', packageName='%s', permissions=%d) > Field '%s' is a slice, but it contains varying types (!UseAny, !UseInterface)", filename, packageName, permissions, k)
+							}
+						}
+					} else {
+						if v == reflect.Float64 && A.Use32Bit {
+							fh.WriteString(fmt.Sprintf("\t%s float32\n", CamelCase(k)))
+							continue
+						} else if v == reflect.Int64 && A.Use32Bit {
+							fh.WriteString(fmt.Sprintf("\t%s int32\n", CamelCase(k)))
+							continue
+						}
+						fh.WriteString(fmt.Sprintf("\t%s %s\n", CamelCase(k), v.String()))
+					}
+				}
+			}
+			fh.WriteString("}\n\n")
+		}
+	}
+	/*
+		for _, name := range slices_to_structs {
+			fh.WriteString(fmt.Sprintf("type %s%s struct {\n", A.PrefixStructName, name))
+			for _,
+		}
+	*/
+	return nil
+}

+ 41 - 0
example/main.go

@@ -0,0 +1,41 @@
+package main
+
+import (
+	"fmt"
+
+	"git.red-green.com/david/astruct"
+)
+
+func main() {
+	var a *astruct.Astruct = astruct.NewAstruct("Weather")
+	var err error = a.ReadFile("test.json")
+	if err != nil {
+		fmt.Println("Err:", err)
+		return
+	}
+	fmt.Println("MapLikes:")
+	for name, m := range a.MapLikes {
+		if name == "" {
+			continue
+		}
+		fmt.Printf("%s\n", name)
+		for k, v := range m {
+			fmt.Printf("  '%s' = %s\n", k, v.String())
+		}
+	}
+	fmt.Println("ArrayLikes:")
+	for name, a := range a.ArrayLikes {
+		if name == "" {
+			continue
+		}
+		fmt.Printf("%s\n", name)
+		for i, v := range a {
+			fmt.Printf("  %d = %s\n", i, v.String())
+		}
+	}
+	err = a.WriteFile("output.txt", "test", 0666)
+	if err != nil {
+		fmt.Println("Err:", err)
+		return
+	}
+}

+ 15 - 0
example/normal.json

@@ -0,0 +1,15 @@
+{
+  "test": {
+    "3": "k4",
+    "gravity": 9.81,
+    "nest": {
+      "1": [
+        1,
+        2,
+        3
+      ],
+      "ok": true
+    },
+    "number": 42
+  }
+}

+ 44 - 0
example/test.json

@@ -0,0 +1,44 @@
+{
+    "coord": {
+      "lon": -82.7193,
+      "lat": 28.2442
+    },
+    "weather": [
+      {
+        "id": 800,
+        "main": "Clear",
+        "description": "clear sky",
+        "icon": "01d"
+      }
+    ],
+    "base": "stations",
+    "main": {
+      "temp": 79.43,
+      "feels_like": 79.43,
+      "temp_min": 75.94,
+      "temp_max": 83.14,
+      "pressure": 1021,
+      "humidity": 55
+    },
+    "visibility": 10000,
+    "wind": {
+      "speed": 7,
+      "deg": 98,
+      "gust": 11.99
+    },
+    "clouds": {
+      "all": 0
+    },
+    "dt": 1682005069,
+    "sys": {
+      "type": 2,
+      "id": 2012526,
+      "country": "US",
+      "sunrise": 1681988440,
+      "sunset": 1682035114
+    },
+    "timezone": -14400,
+    "id": 4165869,
+    "name": "New Port Richey",
+    "cod": 200
+}

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module git.red-green.com/david/astruct
+
+go 1.20
+
+require golang.org/x/exp v0.0.0-20230420155640-133eef4313cb // indirect

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+golang.org/x/exp v0.0.0-20230420155640-133eef4313cb h1:rhjz/8Mbfa8xROFiH+MQphmAmgqRM0bOMnytznhWEXk=
+golang.org/x/exp v0.0.0-20230420155640-133eef4313cb/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=