diff --git a/cmd/RolesApiConverter/main.go b/cmd/RolesApiConverter/main.go index 0967fbd..f45c722 100644 --- a/cmd/RolesApiConverter/main.go +++ b/cmd/RolesApiConverter/main.go @@ -82,17 +82,18 @@ func main() { parts := sliceutils.Filter(strings.Split(line, " "), func(t string) bool { return t != "" }) nameTypeMap[parts[0]] = parts[1] } - pkgString, _, _ := strings.Cut(string(data), "\n") outBuilder := strings.Builder{} outBuilder.WriteString(`// Code generated by cmd/RolesApiConverter DO NOT EDIT. // If you need to refresh the content, run go generate again `) - outBuilder.WriteString(pkgString + "\n\n") + outBuilder.WriteString("package server\n\n") - outBuilder.WriteString("func convertStorageRoleToApiRole(r storage.Role) linstromRole {\n") + outBuilder.WriteString("import \"gitlab.com/mstarongitlab/linstrom/storage\"\n") + + outBuilder.WriteString("func convertRoleStorageToLinstrom(r storage.Role) linstromRole {\n") outBuilder.WriteString("return linstromRole{") - outBuilder.WriteString("Id:r.ID,CreatedAt:r.CreatedAt,UpdatedAt:&r.UpdatedAt,Name:r.Name,") + outBuilder.WriteString("Id:r.ID,CreatedAt:r.CreatedAt,UpdatedAt:r.UpdatedAt,Name:r.Name,") outBuilder.WriteString("Priority:r.Priority,IsUserRole:r.IsUserRole,IsBuiltIn:r.IsBuiltIn,") for k := range nameTypeMap { outBuilder.WriteString(fmt.Sprintf("%s:r.%s,", k, k)) diff --git a/cmd/RolesApiTypeGenerator/main.go b/cmd/RolesApiTypeGenerator/main.go new file mode 100644 index 0000000..4a64600 --- /dev/null +++ b/cmd/RolesApiTypeGenerator/main.go @@ -0,0 +1,136 @@ +/* +Generator for the API type definition on the go server side +*/ +package main + +import ( + "flag" + "fmt" + "io" + "os" + "regexp" + "strings" + "unicode" + + "gitlab.com/mstarongitlab/goutils/sliceutils" +) + +var findRoleStructRegex = regexp.MustCompile(`type Role struct \{([\s\S]+)\}\n\n/\*`) + +var ( + flagInputFile = flag.String("input", "", "Specify the input file. If empty, read from stdin") + flagOutputFile = flag.String( + "output", + "", + "Specify the output file. If empty, writes to stdout", + ) +) + +func main() { + flag.Parse() + var input io.Reader + var output io.Writer + + if *flagInputFile == "" { + input = os.Stdin + } else { + file, err := os.Open(*flagInputFile) + if err != nil { + panic(err) + } + defer file.Close() + input = file + } + if *flagOutputFile == "" { + output = os.Stdout + } else { + file, err := os.Create(*flagOutputFile) + if err != nil { + panic(err) + } + defer file.Close() + output = file + } + + data, err := io.ReadAll(input) + if err != nil { + panic(err) + } + if !findRoleStructRegex.Match(data) { + panic("Input doesn't contain role struct") + } + content := findRoleStructRegex.FindStringSubmatch(string(data))[1] + lines := strings.Split(content, "\n") + lines = sliceutils.Map(lines, func(t string) string { return strings.TrimSpace(t) }) + importantLines := sliceutils.Filter(lines, func(t string) bool { + if strings.HasPrefix(t, "//") { + return false + } + if strings.Contains(t, "gorm.Model") { + return false + } + data := sliceutils.Filter(strings.Split(t, " "), func(t string) bool { return t != "" }) + if len(data) < 2 { + return false + } + if !strings.HasPrefix(data[1], "*") && !strings.HasPrefix(data[1], "[]") { + return false + } + return true + }) + nameTypeMap := map[string]string{} + for _, line := range importantLines { + parts := sliceutils.Filter(strings.Split(line, " "), func(t string) bool { return t != "" }) + nameTypeMap[parts[0]] = parts[1] + } + + outBuilder := strings.Builder{} + outBuilder.WriteString(`// Code generated by cmd/RolesApiTypeGenerator DO NOT EDIT. +// If you need to refresh the content, run go generate again +`) + outBuilder.WriteString("package server\n\n") + outBuilder.WriteString("import \"time\"\n") + + outBuilder.WriteString("type linstromRole struct {\n") + outBuilder.WriteString(" Id uint `jsonapi:\"primary,roles\"`\n") + outBuilder.WriteString(fNT("CreatedAt", "time.Time")) + outBuilder.WriteString(fNT("UpdatedAt", "time.Time")) + outBuilder.WriteString(fNT("Name", "string")) + outBuilder.WriteString(fNT("Priority", "uint32")) + outBuilder.WriteString(fNT("IsUserRole", "bool")) + outBuilder.WriteString(fNT("IsBuiltIn", "bool")) + + for n, t := range nameTypeMap { + outBuilder.WriteString(fNT(n, t)) + } + + outBuilder.WriteString("}") + fmt.Fprint(output, outBuilder.String()) +} + +func convertGoNameToJsonApiName(name string) string { + buffer := []rune{} + isFirst := true + for _, char := range name { + if unicode.IsUpper(char) { + if isFirst { + buffer = append(buffer, unicode.ToLower(char)) + isFirst = false + } else { + buffer = append(buffer, '-', unicode.ToLower(char)) + } + } else { + buffer = append(buffer, char) + } + } + return string(buffer) +} + +func fNT(name, valType string) string { + return fmt.Sprintf( + " %s %s `jsonapi:\"attr,%s\"`\n", + name, + valType, + convertGoNameToJsonApiName(name), + ) +} diff --git a/cmd/RolesFrontendGenerator/main.go b/cmd/RolesFrontendGenerator/main.go new file mode 100644 index 0000000..83138b6 --- /dev/null +++ b/cmd/RolesFrontendGenerator/main.go @@ -0,0 +1,174 @@ +/* +Tool for generating the frontend definition of the role model +It generates a typescript file containing the full role, but without functions +*/ +package main + +import ( + "flag" + "fmt" + "io" + "os" + "regexp" + "strings" + "unicode" + + "gitlab.com/mstarongitlab/goutils/sliceutils" +) + +var findRoleStructRegex = regexp.MustCompile(`type Role struct \{([\s\S]+)\}\n\n/\*`) + +var ( + flagInputFile = flag.String("input", "", "Specify the input file. If empty, read from stdin") + flagOutputFile = flag.String( + "output", + "", + "Specify the output file. If empty, writes to stdout", + ) +) + +func main() { + flag.Parse() + var input io.Reader + var output io.Writer + + if *flagInputFile == "" { + input = os.Stdin + } else { + file, err := os.Open(*flagInputFile) + if err != nil { + panic(err) + } + defer file.Close() + input = file + } + if *flagOutputFile == "" { + output = os.Stdout + } else { + file, err := os.Create(*flagOutputFile) + if err != nil { + panic(err) + } + defer file.Close() + output = file + } + + data, err := io.ReadAll(input) + if err != nil { + panic(err) + } + if !findRoleStructRegex.Match(data) { + panic("Input doesn't contain role struct") + } + content := findRoleStructRegex.FindStringSubmatch(string(data))[1] + lines := strings.Split(content, "\n") + lines = sliceutils.Map(lines, func(t string) string { return strings.TrimSpace(t) }) + importantLines := sliceutils.Filter(lines, func(t string) bool { + if strings.HasPrefix(t, "//") { + return false + } + if strings.Contains(t, "gorm.Model") { + return false + } + data := sliceutils.Filter(strings.Split(t, " "), func(t string) bool { return t != "" }) + if len(data) < 2 { + return false + } + if !strings.HasPrefix(data[1], "*") && !strings.HasPrefix(data[1], "[]") { + return false + } + return true + }) + nameTypeMap := map[string]string{} + for _, line := range importantLines { + parts := sliceutils.Filter(strings.Split(line, " "), func(t string) bool { return t != "" }) + nameTypeMap[parts[0]] = parts[1] + } + + outBuilder := strings.Builder{} + outBuilder.WriteString(`// Code generated by cmd/RolesApiTypeGenerator DO NOT EDIT. +// If you need to refresh the content, run go generate again +`) + outBuilder.WriteString("import Model, { attr } from '@ember-data/model';\n\n") + + outBuilder.WriteString("export default class RoleModel extends Model {\n") + outBuilder.WriteString(` @attr() declare createdAt: Date + @attr() declare updatedAt: Date + @attr() declare name: string + @attr() declare priority: number + @attr() declare isUserRole: boolean + @attr() declare isBuiltIn: boolean + +`) + + for n, t := range nameTypeMap { + outBuilder.WriteString(fNT(n, t)) + } + + outBuilder.WriteString("}") + fmt.Fprint(output, outBuilder.String()) +} + +func convertGoTypeToTS(t string) string { + switch t { + case "string", "*string": + return "string" + case "time.Time": + return "Date" + case "uint", + "uint8", + "uint16", + "uint32", + "uint64", + "int", + "int8", + "int16", + "int32", + "int64", + "float32", + "float64", + "byte", + "*uint", + "*uint8", + "*uint16", + "*uint32", + "*uint64", + "*int", + "*int8", + "*int16", + "*int32", + "*int64", + "*float32", + "*float64", + "*byte": + return "number" + case "bool", "*bool": + return "boolean" + case "[]string": + return "Array" + default: + panic(fmt.Sprintf("Unknown type: %s", t)) + } +} + +func convertGoNameToTS(name string) string { + buffer := []rune{} + isFirst := true + for _, char := range name { + if isFirst { + buffer = append(buffer, unicode.ToLower(char)) + isFirst = false + } else { + buffer = append(buffer, char) + } + } + return string(buffer) +} + +func fNT(name, valType string) string { + return fmt.Sprintf( + " @attr() declare %s?: %s\n", + convertGoNameToTS(name), + convertGoTypeToTS(valType), + ) +} diff --git a/storage/roles.go b/storage/roles.go index 92b446f..f9ea761 100644 --- a/storage/roles.go +++ b/storage/roles.go @@ -11,6 +11,17 @@ import ( //go:generate ./RolesGenerator -input=roles.go -output=rolesUtil_generated.go //go:generate rm RolesGenerator +//go:generate go build -o ApiGenerator ../cmd/RolesApiTypeGenerator/main.go +//go:generate ./ApiGenerator -input=roles.go -output=../server/apiLinstromTypes_generated.go +//go:generate rm ApiGenerator + +//go:generate go build -o HelperGenerator ../cmd/RolesApiConverter/main.go +//go:generate ./HelperGenerator -input=roles.go -output=../server/apiLinstromTypeHelpers_generated.go +//go:generate rm HelperGenerator + +//go:generate go build -o FrontendGenerator ../cmd/RolesFrontendGenerator/main.go +//go:generate ./FrontendGenerator -input=roles.go -output=../frontend-reactive/app/models/role.ts + // A role is, in concept, similar to how Discord handles roles // Some permission can be either disallowed (&false), don't care (nil) or allowed (&true) // Don't care just says to use the value from the next lower role where it is set