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 true the key word any (which is an alias to interface{}) will be used, when false an error will be thrown (this only applies to arrays/slices who have varying types) VerboseLogging bool // Dump verbose info into logs (User must setup log first!) AssumeStruct bool // Defaults to true, when true instead of a map of a basic type a structure will be made (even if the type matches), while false a map could be used in places where the type matches 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) SuffixForArrayLikes string // If not empty "" string, and it's an array this suffix is added (default's to "Item") // 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.VerboseLogging = false A.AssumeStruct = true A.PrefixStructName = "" A.SuffixForArrayLikes = "Item" 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 { if A.VerboseLogging { log.Printf(strings.Repeat(" ", int(A.Depth))+"'%s' = %s", 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 { // We'll use a empty string "", to indicate this map is same type A.MapLikes[name][""] = reflect.TypeOf(m[maps.Keys(m)[0]]).Kind() 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() } } } case reflect.Array, reflect.Slice: a := at.([]any) var last_type reflect.Kind = 0 var same_type bool = true for i, v := range a { if A.VerboseLogging { log.Printf(strings.Repeat(" ", int(A.Depth))+"%d = %s", 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, A.SuffixForArrayLikes) if err != nil { return err } A.Depth -= 1 case reflect.Array, reflect.Slice: A.Depth += 1 err := A.parse(v, A.SuffixForArrayLikes) 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: if A.VerboseLogging { log.Printf(strings.Repeat(" ", int(A.Depth))+"%s", 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) var hit_dot bool = false for _, r := range str { if r == '.' { hit_dot = true continue } if hit_dot { if r != '0' { return false } } } 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 == "" { // Ignore the root node for now continue }*/ if slices.Contains(seen, name) { continue } _, sameType := m[""] // Check if the empty string exists in this map (if it does it contains same typed data) if !A.AssumeStruct || !sameType { 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) { _, sameType2 := A.MapLikes[k][""] if sameType2 && !A.AssumeStruct { fh.WriteString(fmt.Sprintf("\t%s map[string]%s\n", CamelCase(k), A.MapLikes[k][""].String())) seen = append(seen, k) } else if !sameType2 || A.AssumeStruct && v == reflect.Map { fh.WriteString(fmt.Sprintf("\t%s %s%s\n", CamelCase(k), A.PrefixStructName, CamelCase(k))) } else if !sameType2 || A.AssumeStruct && v != reflect.Map { fh.WriteString(fmt.Sprintf("\t%s %s\n", CamelCase(k), v.String())) } } 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 { fh.WriteString(fmt.Sprintf("\t%s []%s%s\n", CamelCase(k), CamelCase(k), A.SuffixForArrayLikes)) 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 { fh.WriteString(fmt.Sprintf("\t%s []any\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") } else if A.AssumeStruct && sameType { fh.WriteString(fmt.Sprintf("type %s%s struct {\n", A.PrefixStructName, CamelCase(name))) t := m[""] var kind reflect.Kind if t == reflect.Float64 && A.Use32Bit { kind = reflect.Float32 } else if t == reflect.Int64 && A.Use32Bit { kind = reflect.Int32 } else { kind = t } for k := range m { if k == "" { continue } fh.WriteString(fmt.Sprintf("\t%s %s\n", CamelCase(k), kind)) } 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 }