Rewrite generator and update for Lemmy 0.18.3
This commit is contained in:
192
cmd/gen/extractor/extractor.go
Normal file
192
cmd/gen/extractor/extractor.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package extractor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
Name string
|
||||
Summary string
|
||||
Method string
|
||||
Path string
|
||||
ParamsName string
|
||||
ParamsID int64
|
||||
ReturnName string
|
||||
ReturnID int64
|
||||
}
|
||||
|
||||
type Struct struct {
|
||||
Name string
|
||||
Fields []Field
|
||||
UnionNames []string
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Name string
|
||||
IsArray bool
|
||||
IsOptional bool
|
||||
Type string
|
||||
}
|
||||
|
||||
type Extractor struct {
|
||||
root gjson.Result
|
||||
}
|
||||
|
||||
func New(path string) (*Extractor, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Extractor{gjson.ParseBytes(data)}, nil
|
||||
}
|
||||
|
||||
func (e *Extractor) Routes() []Route {
|
||||
var out []Route
|
||||
routes := e.root.Get("children.#.children.#(kind==2048)#|@flatten")
|
||||
for _, route := range routes.Array() {
|
||||
name := route.Get("name").String()
|
||||
signature := route.Get(`signatures.0`)
|
||||
|
||||
httpInfo := signature.Get(`comment.summary.#(kind=="code").text`).String()
|
||||
if !strings.HasPrefix(httpInfo, "`HTTP") {
|
||||
continue
|
||||
}
|
||||
method, path := parseHTTPInfo(httpInfo)
|
||||
|
||||
summary := strings.TrimSpace(signature.Get(`comment.summary.#(kind=="text").text`).String())
|
||||
if summary == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
paramsID := signature.Get("parameters.0.type.target").Int()
|
||||
paramsName := signature.Get("parameters.0.type.name").String()
|
||||
returnID := signature.Get("type.typeArguments.0.target").Int()
|
||||
returnName := signature.Get("type.typeArguments.0.name").String()
|
||||
|
||||
out = append(out, Route{
|
||||
Name: name,
|
||||
Summary: summary,
|
||||
Method: method,
|
||||
Path: path,
|
||||
ParamsName: paramsName,
|
||||
ParamsID: paramsID,
|
||||
ReturnName: returnName,
|
||||
ReturnID: returnID,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *Extractor) Structs(routes []Route) []Struct {
|
||||
var ids []int64
|
||||
for _, route := range routes {
|
||||
ids = append(ids, route.ParamsID)
|
||||
if route.ReturnID != 0 {
|
||||
ids = append(ids, route.ReturnID)
|
||||
}
|
||||
}
|
||||
|
||||
structs := map[int64]Struct{}
|
||||
e.getStructs(ids, structs)
|
||||
return getKeys(structs)
|
||||
}
|
||||
|
||||
func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
||||
for _, id := range ids {
|
||||
if _, ok := structs[id]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
jstruct := e.root.Get(fmt.Sprintf("children.#(id==%d)", id))
|
||||
if !jstruct.Exists() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := jstruct.Get("name").String()
|
||||
|
||||
if jstruct.Get("type.type").String() == "union" {
|
||||
structs[id] = Struct{
|
||||
Name: name,
|
||||
UnionNames: e.unionNames(jstruct),
|
||||
}
|
||||
} else {
|
||||
fields, newIDs := e.fields(jstruct)
|
||||
|
||||
structs[id] = Struct{
|
||||
Name: name,
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
e.getStructs(newIDs, structs)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Extractor) unionNames(jstruct gjson.Result) []string {
|
||||
jnames := jstruct.Get("type.types").Array()
|
||||
out := make([]string, len(jnames))
|
||||
for i, name := range jnames {
|
||||
out[i] = name.Get("value").String()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
||||
var fields []Field
|
||||
var ids []int64
|
||||
jfields := jstruct.Get("children").Array()
|
||||
for _, jfield := range jfields {
|
||||
var field Field
|
||||
|
||||
field.Name = jfield.Get("name").String()
|
||||
field.IsOptional = jfield.Get("flags.isOptional").Bool()
|
||||
|
||||
if jfield.Get("type.type").String() == "array" {
|
||||
field.IsArray = true
|
||||
field.Type = jfield.Get("type.elementType.name").String()
|
||||
|
||||
switch jfield.Get("type.elementType.type").String() {
|
||||
case "reference":
|
||||
ids = append(ids, jfield.Get("type.elementType.target").Int())
|
||||
case "union":
|
||||
field.Type = "string"
|
||||
}
|
||||
} else {
|
||||
field.Type = jfield.Get("type.name").String()
|
||||
|
||||
switch jfield.Get("type.type").String() {
|
||||
case "reference":
|
||||
ids = append(ids, jfield.Get("type.target").Int())
|
||||
case "union":
|
||||
field.Type = "string"
|
||||
}
|
||||
}
|
||||
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return fields, ids
|
||||
}
|
||||
|
||||
func parseHTTPInfo(httpInfo string) (method, path string) {
|
||||
httpInfo = strings.Trim(httpInfo, "`")
|
||||
method, path, _ = strings.Cut(httpInfo, " ")
|
||||
method = strings.TrimPrefix(method, "HTTP.")
|
||||
method = strings.ToUpper(method)
|
||||
return method, path
|
||||
}
|
||||
|
||||
func getKeys(m map[int64]Struct) []Struct {
|
||||
out := make([]Struct, len(m))
|
||||
i := 0
|
||||
for _, s := range m {
|
||||
out[i] = s
|
||||
i++
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"go.elara.ws/go-lemmy/cmd/gen/parser"
|
||||
"go.elara.ws/go-lemmy/cmd/gen/extractor"
|
||||
)
|
||||
|
||||
type RoutesGenerator struct {
|
||||
@@ -17,23 +17,30 @@ func NewRoutes(w io.Writer, pkgName string) *RoutesGenerator {
|
||||
return &RoutesGenerator{w, pkgName}
|
||||
}
|
||||
|
||||
func (r *RoutesGenerator) Generate(routes []parser.Route, impls map[string]string) error {
|
||||
func (r *RoutesGenerator) Generate(routes []extractor.Route) error {
|
||||
f := jen.NewFile(r.PkgName)
|
||||
f.HeaderComment("Code generated by go.elara.ws/go-lemmy/cmd/gen (routes generator). DO NOT EDIT.")
|
||||
|
||||
for _, r := range routes {
|
||||
resStruct := impls[r.Struct]
|
||||
|
||||
f.Comment(r.Summary)
|
||||
f.Func().Params(
|
||||
jen.Id("c").Id("*Client"),
|
||||
).Id(transformName(r.Struct)).Params(
|
||||
).Id(transformName(r.Name)).Params(
|
||||
jen.Id("ctx").Qual("context", "Context"),
|
||||
jen.Id("data").Qual("go.elara.ws/go-lemmy/types", r.Struct),
|
||||
).Params(
|
||||
jen.Op("*").Qual("go.elara.ws/go-lemmy/types", resStruct),
|
||||
jen.Error(),
|
||||
).BlockFunc(func(g *jen.Group) {
|
||||
g.Id("resData").Op(":=").Op("&").Qual("go.elara.ws/go-lemmy/types", resStruct).Block()
|
||||
jen.Id("data").Qual("go.elara.ws/go-lemmy/types", r.ParamsName),
|
||||
).ParamsFunc(func(g *jen.Group) {
|
||||
if r.ReturnName != "" {
|
||||
g.Op("*").Qual("go.elara.ws/go-lemmy/types", r.ReturnName)
|
||||
}
|
||||
g.Error()
|
||||
}).BlockFunc(func(g *jen.Group) {
|
||||
returnName := r.ReturnName
|
||||
if returnName == "" {
|
||||
returnName = "EmptyResponse"
|
||||
}
|
||||
|
||||
g.Id("resData").Op(":=").Op("&").Qual("go.elara.ws/go-lemmy/types", returnName).Block()
|
||||
|
||||
var funcName string
|
||||
switch r.Method {
|
||||
@@ -46,16 +53,28 @@ func (r *RoutesGenerator) Generate(routes []parser.Route, impls map[string]strin
|
||||
g.List(jen.Id("res"), jen.Err()).Op(":=").Id("c").Dot(funcName).Params(
|
||||
jen.Id("ctx"), jen.Lit(r.Method), jen.Lit(r.Path), jen.Id("data"), jen.Op("&").Id("resData"),
|
||||
)
|
||||
g.If(jen.Err().Op("!=").Nil()).Block(
|
||||
jen.Return(jen.Nil(), jen.Err()),
|
||||
)
|
||||
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(g *jen.Group) {
|
||||
if returnName == "EmptyResponse" {
|
||||
g.Return(jen.Err())
|
||||
} else {
|
||||
g.Return(jen.Nil(), jen.Err())
|
||||
}
|
||||
})
|
||||
|
||||
g.Err().Op("=").Id("resError").Params(jen.Id("res"), jen.Id("resData").Dot("LemmyResponse"))
|
||||
g.If(jen.Err().Op("!=").Nil()).Block(
|
||||
jen.Return(jen.Nil(), jen.Err()),
|
||||
)
|
||||
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(g *jen.Group) {
|
||||
if returnName == "EmptyResponse" {
|
||||
g.Return(jen.Err())
|
||||
} else {
|
||||
g.Return(jen.Nil(), jen.Err())
|
||||
}
|
||||
})
|
||||
|
||||
g.Return(jen.Id("resData"), jen.Nil())
|
||||
if returnName == "EmptyResponse" {
|
||||
g.Return(jen.Nil())
|
||||
} else {
|
||||
g.Return(jen.Id("resData"), jen.Nil())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,6 +82,7 @@ func (r *RoutesGenerator) Generate(routes []parser.Route, impls map[string]strin
|
||||
}
|
||||
|
||||
func transformName(s string) string {
|
||||
s = strings.ToUpper(s[:1]) + s[1:]
|
||||
s = strings.TrimPrefix(s, "Get")
|
||||
s = strings.TrimPrefix(s, "List")
|
||||
return s
|
||||
|
||||
@@ -3,9 +3,10 @@ package generator
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"go.elara.ws/go-lemmy/cmd/gen/parser"
|
||||
"go.elara.ws/go-lemmy/cmd/gen/extractor"
|
||||
)
|
||||
|
||||
type StructGenerator struct {
|
||||
@@ -17,36 +18,88 @@ func NewStruct(w io.Writer, pkgName string) *StructGenerator {
|
||||
return &StructGenerator{w, pkgName}
|
||||
}
|
||||
|
||||
func (s *StructGenerator) Generate(items []parser.Item) error {
|
||||
func (s *StructGenerator) Generate(items []extractor.Struct) error {
|
||||
f := jen.NewFile(s.PkgName)
|
||||
f.HeaderComment("Code generated by go.elara.ws/go-lemmy/cmd/gen (struct generator). DO NOT EDIT.")
|
||||
|
||||
|
||||
for _, item := range items {
|
||||
if item.Struct != nil {
|
||||
st := item.Struct
|
||||
f.Type().Id(st.Name).StructFunc(func(g *jen.Group) {
|
||||
for _, field := range st.Fields {
|
||||
g.Id(field.Name).Id(field.Type).Tag(map[string]string{
|
||||
"json": field.OrigName,
|
||||
"url": field.OrigName + ",omitempty",
|
||||
if len(item.UnionNames) > 0 {
|
||||
f.Type().Id(item.Name).String()
|
||||
|
||||
f.Const().DefsFunc(func(g *jen.Group) {
|
||||
for _, member := range item.UnionNames {
|
||||
constName := strings.Replace(item.Name+string(member), " ", "", -1)
|
||||
g.Id(constName).Id(item.Name).Op("=").Lit(string(member))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
f.Type().Id(item.Name).StructFunc(func(g *jen.Group) {
|
||||
for _, field := range item.Fields {
|
||||
g.Id(transformFieldName(field.Name)).Id(getType(field)).Tag(map[string]string{
|
||||
"json": field.Name,
|
||||
"url": field.Name + ",omitempty",
|
||||
})
|
||||
}
|
||||
|
||||
if strings.HasSuffix(st.Name, "Response") {
|
||||
if strings.HasSuffix(item.Name, "Response") {
|
||||
g.Id("LemmyResponse")
|
||||
}
|
||||
})
|
||||
} else if item.Enum != nil {
|
||||
e := item.Enum
|
||||
f.Type().Id(e.Name).String()
|
||||
|
||||
f.Const().DefsFunc(func(g *jen.Group) {
|
||||
for _, member := range e.Members {
|
||||
g.Id(e.Name + string(member)).Id(e.Name).Op("=").Lit(string(member))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return f.Render(s.w)
|
||||
}
|
||||
|
||||
func getType(f extractor.Field) string {
|
||||
t := transformType(f.Type)
|
||||
if f.IsArray {
|
||||
t = "[]" + t
|
||||
}
|
||||
if f.IsOptional {
|
||||
t = "Optional[" + t + "]"
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func transformType(s string) string {
|
||||
switch s {
|
||||
case "number":
|
||||
return "float64"
|
||||
case "boolean":
|
||||
return "bool"
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func transformFieldName(s string) string {
|
||||
s = snakeToCamel(s)
|
||||
s = strings.NewReplacer(
|
||||
"Id", "ID",
|
||||
"Url", "URL",
|
||||
"Nsfw", "NSFW",
|
||||
"Jwt", "JWT",
|
||||
"Crud", "CRUD",
|
||||
).Replace(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func snakeToCamel(s string) string {
|
||||
sb := &strings.Builder{}
|
||||
capitalizeNext := true
|
||||
for _, char := range s {
|
||||
if char == '_' {
|
||||
capitalizeNext = true
|
||||
continue
|
||||
}
|
||||
|
||||
if capitalizeNext {
|
||||
sb.WriteRune(unicode.ToUpper(char))
|
||||
capitalizeNext = false
|
||||
} else {
|
||||
sb.WriteRune(char)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
162
cmd/gen/main.go
162
cmd/gen/main.go
@@ -2,14 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.elara.ws/go-lemmy/cmd/gen/extractor"
|
||||
"go.elara.ws/go-lemmy/cmd/gen/generator"
|
||||
"go.elara.ws/go-lemmy/cmd/gen/parser"
|
||||
"go.elara.ws/logger"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
@@ -18,145 +15,33 @@ func init() {
|
||||
log.Logger = logger.NewPretty(os.Stderr)
|
||||
}
|
||||
|
||||
var implDirs = [...]string{
|
||||
"crates/api_crud/src",
|
||||
"crates/apub/src/api",
|
||||
"crates/api/src",
|
||||
}
|
||||
|
||||
var structDirs = [...]string{
|
||||
"crates/api_common",
|
||||
"crates/db_schema/src/source",
|
||||
"crates/db_views_actor/src/structs.rs",
|
||||
"crates/db_views/src/structs.rs",
|
||||
"crates/db_views_moderator/src/structs.rs",
|
||||
"crates/db_schema/src/aggregates/structs.rs",
|
||||
"crates/db_schema/src/lib.rs",
|
||||
"crates/websocket/src/lib.rs",
|
||||
}
|
||||
|
||||
const routesFile = "src/api_routes_http.rs"
|
||||
|
||||
func main() {
|
||||
lemmyDir := flag.String("lemmy-dir", "lemmy", "Path to Lemmy repository")
|
||||
jsonFile := flag.String("json-file", "lemmy.json", "Path to the JSON file generated from lemmy's docs")
|
||||
outDir := flag.String("out-dir", "out", "Directory to write output in")
|
||||
flag.Parse()
|
||||
|
||||
baseStructDir := filepath.Join(*outDir, "types")
|
||||
sp := parser.NewStruct(nil)
|
||||
sp.Skip = []string{"LemmyContext", "Recipient", "WsMessage", "Connect", "SessionInfo"}
|
||||
for _, structDir := range structDirs {
|
||||
dir := filepath.Join(*lemmyDir, structDir)
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(path) != ".rs" {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := d.Name()
|
||||
if name == "context.rs" ||
|
||||
name == "local_user_language.rs" ||
|
||||
name == "chat_server.rs" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fl, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fl.Close()
|
||||
|
||||
sp.Reset(fl)
|
||||
fileStructs, err := sp.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nameNoExt := strings.TrimSuffix(d.Name(), ".rs")
|
||||
goFilePath := filepath.Join(baseStructDir, nameNoExt+".gen.go")
|
||||
|
||||
i := 1
|
||||
_, err = os.Stat(goFilePath)
|
||||
for err == nil {
|
||||
goFilePath = filepath.Join(baseStructDir, nameNoExt+"."+strconv.Itoa(i)+".gen.go")
|
||||
_, err = os.Stat(goFilePath)
|
||||
i++
|
||||
}
|
||||
|
||||
outFl, err := os.Create(goFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFl.Close()
|
||||
|
||||
_, err = outFl.WriteString("// Source: " + path + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return generator.NewStruct(outFl, "types").Generate(fileStructs)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("Error walking directory while parsing structs").Err(err).Str("dir", dir).Send()
|
||||
}
|
||||
}
|
||||
|
||||
ip := parser.NewImpl(nil)
|
||||
impls := map[string]string{}
|
||||
for _, implDir := range implDirs {
|
||||
dir := filepath.Join(*lemmyDir, implDir)
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(path) != ".rs" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fl, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fl.Close()
|
||||
|
||||
ip.Reset(fl)
|
||||
implMap, err := ip.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for reqStruct, resStruct := range implMap {
|
||||
impls[reqStruct] = resStruct
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("Error walking directory while parsing impls").Err(err).Str("dir", dir).Send()
|
||||
}
|
||||
}
|
||||
|
||||
routesPath := filepath.Join(*lemmyDir, routesFile)
|
||||
rf, err := os.Open(routesPath)
|
||||
e, err := extractor.New(*jsonFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening routes file").Err(err).Send()
|
||||
log.Fatal("Error creating extractor").Err(err).Send()
|
||||
}
|
||||
defer rf.Close()
|
||||
|
||||
rp := parser.NewRoutes(rf)
|
||||
routes, err := rp.Parse()
|
||||
routes := e.Routes()
|
||||
structs := e.Structs(routes)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(*outDir, "types"), 0o755)
|
||||
if err != nil {
|
||||
log.Fatal("Error parsing routes file").Err(err).Send()
|
||||
log.Fatal("Error creating types directory").Err(err).Send()
|
||||
}
|
||||
|
||||
otf, err := os.Create(filepath.Join(*outDir, "types/types.gen.go"))
|
||||
if err != nil {
|
||||
log.Fatal("Error creating types output file").Err(err).Send()
|
||||
}
|
||||
defer otf.Close()
|
||||
|
||||
err = generator.NewStruct(otf, "types").Generate(structs)
|
||||
if err != nil {
|
||||
log.Fatal("Error generating output routes file").Err(err).Send()
|
||||
}
|
||||
|
||||
orf, err := os.Create(filepath.Join(*outDir, "routes.gen.go"))
|
||||
@@ -165,12 +50,7 @@ func main() {
|
||||
}
|
||||
defer orf.Close()
|
||||
|
||||
_, err = orf.WriteString("// Source: " + routesPath + "\n")
|
||||
if err != nil {
|
||||
log.Fatal("Error writing source string to routes file").Err(err).Send()
|
||||
}
|
||||
|
||||
err = generator.NewRoutes(orf, "lemmy").Generate(routes, impls)
|
||||
err = generator.NewRoutes(orf, "lemmy").Generate(routes)
|
||||
if err != nil {
|
||||
log.Fatal("Error generating output routes file").Err(err).Send()
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
implRegex = regexp.MustCompile(`impl Perform.* for (.+) {`)
|
||||
respTypeRegex = regexp.MustCompile(`type Response = (.+);`)
|
||||
)
|
||||
|
||||
var ErrNoType = errors.New("type line not found")
|
||||
|
||||
type ImplParser struct {
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
func NewImpl(r io.Reader) *ImplParser {
|
||||
return &ImplParser{
|
||||
r: bufio.NewReader(r),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ImplParser) Parse() (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
for {
|
||||
line, err := i.r.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if implRegex.MatchString(line) {
|
||||
im := implRegex.FindStringSubmatch(line)
|
||||
|
||||
line, err := i.r.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !respTypeRegex.MatchString(line) {
|
||||
return nil, ErrNoType
|
||||
}
|
||||
|
||||
rtm := respTypeRegex.FindStringSubmatch(line)
|
||||
|
||||
out[im[1]] = rtm[1]
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (i *ImplParser) Reset(r io.Reader) {
|
||||
i.r.Reset(r)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
scopeRegex = regexp.MustCompile(`web::(?:scope|resource)\("(.*)"\)\n`)
|
||||
routeRegex = regexp.MustCompile(`\.route\(\n?\s*(?:"(.*)",[ \n])?\s*web::(.+)\(\)\.to\(route_.*::<(.+)>`)
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
Path string
|
||||
Method string
|
||||
Struct string
|
||||
}
|
||||
|
||||
type RoutesParser struct {
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
func NewRoutes(r io.Reader) *RoutesParser {
|
||||
return &RoutesParser{
|
||||
r: bufio.NewReader(r),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoutesParser) Parse() ([]Route, error) {
|
||||
var out []Route
|
||||
for {
|
||||
line, err := r.r.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if scopeRegex.MatchString(line) {
|
||||
scopePath := scopeRegex.FindStringSubmatch(line)[1]
|
||||
if scopePath == "/api/v3" {
|
||||
continue
|
||||
}
|
||||
|
||||
routes, err := r.parseRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range routes {
|
||||
path, err := url.JoinPath(scopePath, routes[i].Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes[i].Path = path
|
||||
}
|
||||
|
||||
out = append(out, routes...)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *RoutesParser) parseRoutes() ([]Route, error) {
|
||||
var out []Route
|
||||
for {
|
||||
line, err := r.r.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
if strings.TrimSpace(line)[:1] == ")" {
|
||||
return out, nil
|
||||
} else {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(line) == ".route(" {
|
||||
lines, err := r.readLines(3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
line += lines
|
||||
}
|
||||
|
||||
if strings.TrimSpace(line)[:1] == ")" {
|
||||
return out, nil
|
||||
} else if strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
} else if !routeRegex.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
sm := routeRegex.FindStringSubmatch(line)
|
||||
out = append(out, Route{
|
||||
Path: sm[1],
|
||||
Method: strings.ToUpper(sm[2]),
|
||||
Struct: sm[3],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoutesParser) readLines(n int) (string, error) {
|
||||
out := ""
|
||||
for i := 0; i < n; i++ {
|
||||
line, err := r.r.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out += line
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *RoutesParser) Reset(rd io.Reader) {
|
||||
r.r.Reset(rd)
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
structRegex = regexp.MustCompile(`pub struct (.+) \{`)
|
||||
fieldRegex = regexp.MustCompile(`(?U) {1,1}([^ ]+): (.+),`)
|
||||
|
||||
enumRegex = regexp.MustCompile(`pub enum (.+) \{`)
|
||||
memberRegex = regexp.MustCompile(` ([^ #]+),\n`)
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Struct *Struct
|
||||
Enum *Enum
|
||||
}
|
||||
|
||||
type Struct struct {
|
||||
Name string
|
||||
Fields []Field
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
OrigName string
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
type Enum struct {
|
||||
Name string
|
||||
Members []Member
|
||||
}
|
||||
|
||||
type Member string
|
||||
|
||||
type StructParser struct {
|
||||
r *bufio.Reader
|
||||
Skip []string
|
||||
TransformName func(string) string
|
||||
TransformType func(string) string
|
||||
}
|
||||
|
||||
func NewStruct(r io.Reader) *StructParser {
|
||||
return &StructParser{
|
||||
r: bufio.NewReader(r),
|
||||
TransformName: TransformNameGo,
|
||||
TransformType: TransformTypeGo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StructParser) Parse() ([]Item, error) {
|
||||
var out []Item
|
||||
for {
|
||||
line, err := s.r.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if structRegex.MatchString(line) {
|
||||
structName := structRegex.FindStringSubmatch(line)[1]
|
||||
if slices.Contains(s.Skip, structName) {
|
||||
continue
|
||||
}
|
||||
structName = s.TransformName(structName)
|
||||
|
||||
// If the line ends with "}", this is a struct with no fields
|
||||
if strings.HasSuffix(line, "}\n") {
|
||||
out = append(out, Item{
|
||||
Struct: &Struct{
|
||||
Name: structRegex.FindStringSubmatch(line)[1],
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
fields, err := s.parseStructFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out = append(out, Item{
|
||||
Struct: &Struct{
|
||||
Name: structName,
|
||||
Fields: fields,
|
||||
},
|
||||
})
|
||||
} else if enumRegex.MatchString(line) {
|
||||
enumName := enumRegex.FindStringSubmatch(line)[1]
|
||||
if slices.Contains(s.Skip, enumName) {
|
||||
continue
|
||||
}
|
||||
enumName = s.TransformName(enumName)
|
||||
|
||||
members, err := s.parseEnumMemebers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out = append(out, Item{
|
||||
Enum: &Enum{
|
||||
Name: enumName,
|
||||
Members: members,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *StructParser) parseStructFields() ([]Field, error) {
|
||||
encountered := map[string]struct{}{}
|
||||
var out []Field
|
||||
for {
|
||||
line, err := s.r.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
if strings.HasPrefix(line, "}") {
|
||||
return out, nil
|
||||
} else {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "}") {
|
||||
return out, nil
|
||||
} else if strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
} else if !fieldRegex.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
sm := fieldRegex.FindStringSubmatch(line)
|
||||
if sm[1] == "Example" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := encountered[sm[1]]; ok {
|
||||
continue
|
||||
} else {
|
||||
encountered[sm[1]] = struct{}{}
|
||||
}
|
||||
|
||||
out = append(out, Field{
|
||||
OrigName: sm[1],
|
||||
Name: s.TransformName(sm[1]),
|
||||
Type: s.TransformType(sm[2]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StructParser) parseEnumMemebers() ([]Member, error) {
|
||||
var out []Member
|
||||
for {
|
||||
line, err := s.r.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
if strings.HasPrefix(line, "}") {
|
||||
return out, nil
|
||||
} else {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "}") {
|
||||
return out, nil
|
||||
} else if strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
} else if !memberRegex.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
sm := memberRegex.FindStringSubmatch(line)
|
||||
|
||||
out = append(out, Member(sm[1]))
|
||||
}
|
||||
}
|
||||
|
||||
// TransformTypeGo transforms Rust types to Go
|
||||
//
|
||||
// Example: TransformTypeGo("Option<Vec<i64>>") // returns "Optional[[]int64]"
|
||||
func TransformTypeGo(t string) string {
|
||||
prefix := ""
|
||||
suffix := ""
|
||||
|
||||
for strings.HasPrefix(t, "Option<") {
|
||||
t = strings.TrimPrefix(strings.TrimSuffix(t, ">"), "Option<")
|
||||
prefix += "Optional["
|
||||
suffix += "]"
|
||||
}
|
||||
|
||||
for strings.HasPrefix(t, "Vec<") {
|
||||
t = strings.TrimPrefix(strings.TrimSuffix(t, ">"), "Vec<")
|
||||
prefix += "[]"
|
||||
}
|
||||
|
||||
for strings.HasPrefix(t, "Sensitive<") {
|
||||
t = strings.TrimPrefix(strings.TrimSuffix(t, ">"), "Sensitive<")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(t, "Id") {
|
||||
t = "int"
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "String", "Url", "DbUrl", "Ltree":
|
||||
t = "string"
|
||||
case "usize":
|
||||
t = "uint"
|
||||
case "i64":
|
||||
t = "int64"
|
||||
case "i32":
|
||||
t = "int32"
|
||||
case "i16":
|
||||
t = "int16"
|
||||
case "i8":
|
||||
t = "int8"
|
||||
case "chrono::NaiveDateTime":
|
||||
return "LemmyTime"
|
||||
case "Value":
|
||||
return "any"
|
||||
}
|
||||
|
||||
return prefix + t + suffix
|
||||
}
|
||||
|
||||
// TransformNameGo transforms conventional Rust naming to
|
||||
// conventional Go naming.
|
||||
//
|
||||
// Example: TransformNameGo("post_id") // returns "PostID"
|
||||
func TransformNameGo(s string) string {
|
||||
out := ""
|
||||
|
||||
s = strings.ReplaceAll(s, "Crud", "CRUD")
|
||||
|
||||
splitName := strings.Split(s, "_")
|
||||
for _, segment := range splitName {
|
||||
switch segment {
|
||||
case "id":
|
||||
out += "ID"
|
||||
case "url":
|
||||
out += "URL"
|
||||
case "nsfw":
|
||||
out += "NSFW"
|
||||
case "jwt":
|
||||
out += "JWT"
|
||||
case "crud":
|
||||
out += "CRUD"
|
||||
default:
|
||||
if len(segment) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
out += strings.ToUpper(segment[:1]) + segment[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *StructParser) Reset(r io.Reader) {
|
||||
s.r.Reset(r)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package parser
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTransformNameGo(t *testing.T) {
|
||||
type testcase struct {
|
||||
name string
|
||||
expect string
|
||||
}
|
||||
|
||||
cases := []testcase{
|
||||
{"post_id", "PostID"},
|
||||
{"nsfw", "NSFW"},
|
||||
{"test_url", "TestURL"},
|
||||
{"some_complex_name_with_id_and_nsfw_and_url", "SomeComplexNameWithIDAndNSFWAndURL"},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
got := TransformNameGo(testcase.name)
|
||||
if got != testcase.expect {
|
||||
t.Errorf("Expected %s, got %s", testcase.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformTypeGo(t *testing.T) {
|
||||
type testcase struct {
|
||||
typeName string
|
||||
expect string
|
||||
}
|
||||
|
||||
cases := []testcase{
|
||||
{"i16", "int16"},
|
||||
{"Option<Vec<i64>>", "Optional[[]int64]"},
|
||||
{"Url", "string"},
|
||||
{"Sensitive<String>", "string"},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
t.Run(testcase.typeName, func(t *testing.T) {
|
||||
got := TransformTypeGo(testcase.typeName)
|
||||
if got != testcase.expect {
|
||||
t.Errorf("Expected %s, got %s", testcase.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user