diff --git a/examples/simple/golang/go.mod b/examples/simple/golang/go.mod new file mode 100644 index 0000000..a74d83b --- /dev/null +++ b/examples/simple/golang/go.mod @@ -0,0 +1,3 @@ +module efprojects.com/kitten-ipc/example/simple + +go 1.25.1 diff --git a/examples/simple/golang/main.go b/examples/simple/golang/main.go new file mode 100644 index 0000000..bbecfc8 --- /dev/null +++ b/examples/simple/golang/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "path" + + kittenipc "efprojects.com/kitten-ipc" +) + +// kittenipc:api +type IpcApi struct { +} + +func (api IpcApi) Div(a int, b int) (int, error) { + if b == 0 { + return 0, fmt.Errorf("zero division") + } + return a / b, nil +} + +func main() { + cwd, err := os.Getwd() + if err != nil { + log.Panic(err) + } + + api := IpcApi{} + + cmdStr := fmt.Sprintf("node %s", path.Join(cwd, "..", "ts/index.js")) + cmd := exec.Command(cmdStr) + kit := kittenipc.New(cmd, &api) + + if err := kit.Start(); err != nil { + log.Panic(err) + } + + if err := kit.Wait(); err != nil { + log.Panic(err) + } +} diff --git a/examples/simple/ts/index.ts b/examples/simple/ts/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/examples/simple/ts/package.json b/examples/simple/ts/package.json new file mode 100644 index 0000000..5b78c30 --- /dev/null +++ b/examples/simple/ts/package.json @@ -0,0 +1,12 @@ +{ + "name": "kitten-example-simple", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" + }, + "author": "", + "license": "ISC", + "description": "" +} diff --git a/examples/simple/ts/tsconfig.json b/examples/simple/ts/tsconfig.json new file mode 100644 index 0000000..cec4a3a --- /dev/null +++ b/examples/simple/ts/tsconfig.json @@ -0,0 +1,44 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + // "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": [], + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + } +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..f92105c --- /dev/null +++ b/go.work @@ -0,0 +1,7 @@ +go 1.25.1 + +use ( + golang + kitcom + examples/simple/golang +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..b25a0dd --- /dev/null +++ b/go.work.sum @@ -0,0 +1,14 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= diff --git a/golang/go.mod b/golang/go.mod new file mode 100644 index 0000000..2f20896 --- /dev/null +++ b/golang/go.mod @@ -0,0 +1,3 @@ +module efprojects.com/kitten-ipc + +go 1.25.1 diff --git a/golang/lib.go b/golang/lib.go new file mode 100644 index 0000000..50f216f --- /dev/null +++ b/golang/lib.go @@ -0,0 +1,12 @@ +package kittenipc + +import ( + "os/exec" +) + +type KittenIPC struct { +} + +func New(cmd *exec.Cmd, api any) *KittenIPC { + +} diff --git a/kitcom/go.mod b/kitcom/go.mod new file mode 100644 index 0000000..e3d6964 --- /dev/null +++ b/kitcom/go.mod @@ -0,0 +1,5 @@ +module efprojects.com/kitten-ipc/kitcom + +go 1.25.1 + +require golang.org/x/tools v0.38.0 // indirect diff --git a/kitcom/go.sum b/kitcom/go.sum new file mode 100644 index 0000000..85d99f4 --- /dev/null +++ b/kitcom/go.sum @@ -0,0 +1,2 @@ +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= diff --git a/kitcom/gogen.go b/kitcom/gogen.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/kitcom/gogen.go @@ -0,0 +1 @@ +package main diff --git a/kitcom/goparser.go b/kitcom/goparser.go new file mode 100644 index 0000000..25c1526 --- /dev/null +++ b/kitcom/goparser.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "regexp" +) + +var decorComment = regexp.MustCompile(`^//\s?kittenipc:api$`) + +type apiStruct struct { + pkgName string + name string + methods []*ast.FuncDecl +} + +type GoApiParser struct { + apiStructs []*apiStruct +} + +func (g *GoApiParser) Parse(sourceFile string) (Api, error) { + + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, sourceFile, nil, parser.ParseComments|parser.SkipObjectResolution) + if err != nil { + return Api{}, fmt.Errorf("parse file: %w", err) + } + + pkgName := astFile.Name.Name + + for _, decl := range astFile.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + + if genDecl.Doc == nil { + continue + } + + // use only last comment. https://tip.golang.org/doc/comment#syntax + lastComment := genDecl.Doc.List[len(genDecl.Doc.List)-1] + if !decorComment.MatchString(lastComment.Text) { + continue + } + + typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec) + if !ok { + continue + } + + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + _ = structType + + g.apiStructs = append(g.apiStructs, &apiStruct{ + name: typeSpec.Name.Name, + pkgName: pkgName, + }) + } + + if len(g.apiStructs) == 0 { + // todo support arbitrary order of input files + return Api{}, fmt.Errorf("no api struct found") + } + + for _, decl := range astFile.Decls { + funcDecl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + + if !funcDecl.Name.IsExported() { + continue + } + + if funcDecl.Recv == nil { + continue + } + + reciever := funcDecl.Recv.List[0] + recvType := reciever.Type + + star, isPointer := recvType.(*ast.StarExpr) + if isPointer { + recvType = star.X + } + + recvIdent, ok := recvType.(*ast.Ident) + if !ok { + continue + } + + for _, apiStrct := range g.apiStructs { + if recvIdent.Name == apiStrct.name && pkgName == apiStrct.pkgName { + apiStrct.methods = append(apiStrct.methods, funcDecl) + } + } + } + + return Api{}, nil +} diff --git a/kitcom/habr.go b/kitcom/habr.go new file mode 100644 index 0000000..b935ef3 --- /dev/null +++ b/kitcom/habr.go @@ -0,0 +1,241 @@ +//go:build exclude + +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "log" + "os" + "strings" + "text/template" + + "golang.org/x/tools/go/ast/inspector" +) + +// Шаблон, на основе которого будем генерировать +// .EntityName, .PrimaryType — параметры, +// в которые будут установлены данные, добытые из AST-модели +var repositoryTemplate = template.Must(template.New("").Parse(` +package main + +import ( + "github.com/jinzhu/gorm" +) + +type {{ .EntityName }}Repository struct { + db *gorm.DB +} + +func New{{ .EntityName }}Repository(db *gorm.DB) {{ .EntityName }}Repository { + return {{ .EntityName }}Repository{ db: db} +} + +func (r {{ .EntityName }}Repository) Get({{ .PrimaryName }} {{ .PrimaryType}}) (*{{ .EntityName }}, error) { + entity := new({{ .EntityName }}) + err := r.db.Limit(1).Where("{{ .PrimarySQLName }} = ?", {{ .PrimaryName }}).Find(entity).Error() + return entity, err +} + + +func (r {{ .EntityName }}Repository) Create(entity *{{ .EntityName }}) error { + return r.db.Create(entity).Error +} + +func (r {{ .EntityName }}Repository) Update(entity *{{ .EntityName }}) error { + return r.db.Model(entity).Update.Error +} + +func (r {{ .EntityName }}Repository) Update(entity *{{ .EntityName }}) error { + return r.db.Model(entity).Update.Error +} + +func (r {{ .EntityName }}Repository) Delete(entity *{{ .EntityName }}) error { + return r.db.Delete.Error +} +`)) + +// Агрегатор данных для установки параметров в шаблоне +type repositoryGenerator struct { + typeSpec *ast.TypeSpec + structType *ast.StructType +} + +// Просто helper-функция для печати замысловатого ast.Expr в обычный string +func expr2string(expr ast.Expr) string { + var buf bytes.Buffer + err := printer.Fprint(&buf, token.NewFileSet(), expr) + if err != nil { + log.Fatalf("error print expression to string: #{err}") + } + return buf.String() +} + +// Helper для извлечения поля структуры, +// которое станет первичным ключом в таблице DB +// Поиск поля ведётся по тегам +// Ищем то, что мы пометили gorm:"primary_key" +func (r repositoryGenerator) primaryField() (*ast.Field, error) { + for _, field := range r.structType.Fields.List { + if !strings.Contains(field.Tag.Value, "primary") { + continue + } + return field, nil + } + return nil, fmt.Errorf("has no primary field") +} + +// Собственно, генератор +// оформлен методом структуры repositoryGenerator, +// так что параметры передавать не нужно: +// они уже аккумулированы в ресивере метода r repositoryGenerator +// Передаём ссылку на ast.File, +// в котором и окажутся плоды трудов +func (r repositoryGenerator) Generate(outFile *ast.File) error { + //Находим первичный ключ + primary, err := r.primaryField() + if err != nil { + return err + } + //Аллокация и установка параметров для template + params := struct { + EntityName string + PrimaryName string + PrimarySQLName string + PrimaryType string + }{ + //Параметры извлекаем из ресивера метода + EntityName: r.typeSpec.Name.Name, + PrimaryName: primary.Names[0].Name, + PrimarySQLName: primary.Names[0].Name, + PrimaryType: expr2string(primary.Type), + } + //Аллокация буфера, + //куда будем заливать выполненный шаблон + var buf bytes.Buffer + //Процессинг шаблона с подготовленными параметрами + //в подготовленный буфер + err = repositoryTemplate.Execute(&buf, params) + if err != nil { + return fmt.Errorf("execute template: %v", err) + } + //Теперь сделаем парсинг обработанного шаблона, + //который уже стал валидным кодом Go, + //в дерево разбора, + //получаем AST этого кода + templateAst, err := parser.ParseFile( + token.NewFileSet(), + //Источник для парсинга лежит не в файле, + "", + //а в буфере + buf.Bytes(), + //mode парсинга, нас интересуют в основном комментарии + parser.ParseComments, + ) + if err != nil { + return fmt.Errorf("parse template: %v", err) + } + //Добавляем декларации из полученного дерева + //в результирующий outFile *ast.File, + //переданный нам аргументом + for _, decl := range templateAst.Decls { + outFile.Decls = append(outFile.Decls, decl) + } + return nil +} + +func main() { + //Цель генерации передаётся переменной окружения + path := os.Getenv("GOFILE") + if path == "" { + log.Fatal("GOFILE must be set") + } + //Разбираем целевой файл в AST + astInFile, err := parser.ParseFile( + token.NewFileSet(), + path, + nil, + //Нас интересуют комментарии + parser.ParseComments, + ) + if err != nil { + log.Fatalf("parse file: %v", err) + } + //Для выбора интересных нам деклараций + //используем Inspector из golang.org/x/tools/go/ast/inspector + i := inspector.New([]*ast.File{astInFile}) + //Подготовим фильтр для этого инспектора + iFilter := []ast.Node{ + //Нас интересуют декларации + &ast.GenDecl{}, + } + //Выделяем список заданий генерации + var genTasks []repositoryGenerator + //Запускаем инспектор с подготовленным фильтром + //и литералом фильтрующей функции + i.Nodes(iFilter, func(node ast.Node, push bool) (proceed bool) { + genDecl := node.(*ast.GenDecl) + //Код без комментариев не нужен, + if genDecl.Doc == nil { + return false + } + //интересуют спецификации типов, + typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec) + if !ok { + return false + } + //а конкретно структуры + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + return false + } + //Из оставшегося + for _, comment := range genDecl.Doc.List { + switch comment.Text { + //выделяем структуры, помеченные комментарием repogen:entity, + case "//repogen:entity": + //и добавляем в список заданий генерации + genTasks = append(genTasks, repositoryGenerator{ + typeSpec: typeSpec, + structType: structType, + }) + } + } + return false + }) + //Аллокация результирующего дерева разбора + astOutFile := &ast.File{ + Name: astInFile.Name, + } + //Запускаем список заданий генерации + for _, task := range genTasks { + //Для каждого задания вызываем написанный нами генератор + //как метод этого задания + //Сгенерированные декларации помещаются в результирующее дерево разбора + err = task.Generate(astOutFile) + if err != nil { + log.Fatalf("generate: %v", err) + } + } + //Подготовим файл конечного результата всей работы, + //назовем его созвучно файлу модели, добавим только суффикс _gen + outFile, err := os.Create(strings.TrimSuffix(path, ".go") + "_gen.go") + if err != nil { + log.Fatalf("create file: %v", err) + } + //Не забываем прибраться + defer outFile.Close() + //Печатаем результирующий AST в результирующий файл исходного кода + //«Печатаем» не следует понимать буквально, + //дерево разбора нельзя просто переписать в файл исходного кода, + //это совершенно разные форматы + //Мы здесь воспользуемся специализированным принтером из пакета ast/printer + err = printer.Fprint(outFile, token.NewFileSet(), astOutFile) + if err != nil { + log.Fatalf("print file: %v", err) + } +} diff --git a/kitcom/main.go b/kitcom/main.go new file mode 100644 index 0000000..cba3344 --- /dev/null +++ b/kitcom/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path" +) + +type ProgLang string + +const ( + Golang ProgLang = "Golang" + TypeScript = "Typescript" +) + +var ( + Src string + Dest string +) + +func parseFlags() { + flag.StringVar(&Src, "src", "", "Source file") + flag.StringVar(&Dest, "dest", "", "Dest file") + flag.Parse() +} + +type Api struct { +} + +type ApiParser interface { + Parse(sourceFile string) (Api, error) +} + +func main() { + // todo support go:generate + //goFile := os.Getenv("GOFILE") + //if goFile == "" { + // log.Panic("GOFILE must be set") + //} + + parseFlags() + if Src == "" || Dest == "" { + log.Panic("source and destination must be set") + } + + if err := checkIsFile(Src); err != nil { + log.Panic(err) + } + + apiParser, err := apiParserByExt(Src) + if err != nil { + log.Panic(err) + } + + _, err = apiParser.Parse(Src) + if err != nil { + log.Panic(err) + } +} + +func checkIsFile(src string) error { + info, err := os.Stat(src) + if err != nil { + return fmt.Errorf("stat file: %w", err) + } + if info.IsDir() { + return fmt.Errorf("%s is a directory; directories are not supported yet", src) + } + return nil +} + +func apiParserByExt(src string) (ApiParser, error) { + switch path.Ext(src) { + case ".go": + return &GoApiParser{}, nil + case ".ts": + return &TypescriptApiParser{}, nil + case ".js": + return nil, fmt.Errorf("vanilla javascript is not supported and never will be") + case "": + return nil, fmt.Errorf("could not find file extension for %s", src) + default: + return nil, fmt.Errorf("unsupported file extension: %s", path.Ext(src)) + } +} diff --git a/kitcom/tsgen.go b/kitcom/tsgen.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/kitcom/tsgen.go @@ -0,0 +1 @@ +package main diff --git a/kitcom/tsparser.go b/kitcom/tsparser.go new file mode 100644 index 0000000..bd15b84 --- /dev/null +++ b/kitcom/tsparser.go @@ -0,0 +1,9 @@ +package main + +type TypescriptApiParser struct { +} + +func (t *TypescriptApiParser) Parse(sourceFile string) (Api, error) { + //TODO implement me + panic("implement me") +} diff --git a/ts/lib.ts b/ts/lib.ts new file mode 100644 index 0000000..e69de29 diff --git a/ts/package.json b/ts/package.json new file mode 100644 index 0000000..21a6081 --- /dev/null +++ b/ts/package.json @@ -0,0 +1,12 @@ +{ + "name": "kitten-ipc", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" + }, + "author": "", + "license": "ISC", + "description": "" +} diff --git a/ts/tsconfig.json b/ts/tsconfig.json new file mode 100644 index 0000000..cec4a3a --- /dev/null +++ b/ts/tsconfig.json @@ -0,0 +1,44 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + // "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": [], + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + } +}