|
@@ -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
|
|
|
+}
|