Compare commits

...

80 Commits

Author SHA1 Message Date
Pasha
123e0daa7d - 2024-11-15 14:21:43 +03:00
Pasha
b4575f53e8 generate contruct receiver 2024-11-15 14:15:48 +03:00
Pasha
525994f956 finish 2024-11-15 12:42:16 +03:00
Pasha
c0e91951c2 third try write to sqlc queries 2024-11-15 12:40:14 +03:00
Pasha
40df103023 third try write to sqlc queries 2024-11-15 12:37:59 +03:00
Pasha
ae8e0898cc third try write to sqlc queries 2024-11-15 12:36:24 +03:00
Pasha
0567abe346 third try write to sqlc queries 2024-11-15 12:07:05 +03:00
Pasha
f012a6b031 third try write to sqlc queries 2024-11-15 12:05:57 +03:00
Pasha
aead19ae97 third try write to sqlc queries 2024-11-15 12:04:15 +03:00
Pasha
9c66efbada third try write to sqlc queries 2024-11-15 11:59:22 +03:00
Pasha
f686525eac third try write to sqlc queries 2024-11-15 11:47:34 +03:00
Pasha
c34dc73294 third try write to sqlc queries 2024-11-15 11:45:45 +03:00
Pasha
a35df3ecc6 third try write to sqlc queries 2024-11-15 11:44:07 +03:00
Pasha
7164b56099 third try write to sqlc queries 2024-11-15 11:42:43 +03:00
Pasha
86e7319452 third try write to sqlc queries 2024-11-15 11:42:11 +03:00
Pasha
7f0edaa9f6 third try write to sqlc queries 2024-11-15 11:41:03 +03:00
Pasha
7a46b40c3f third try write to sqlc queries 2024-11-15 11:38:50 +03:00
Pasha
47d8ed5e43 third try write to sqlc queries 2024-11-15 11:38:00 +03:00
Pasha
fe5c898aeb third try write to sqlc queries 2024-11-15 11:36:39 +03:00
Pasha
0da3c8004e third try write to sqlc queries 2024-11-15 11:34:36 +03:00
Pasha
1e2c045bcd third try write to sqlc queries 2024-11-15 11:31:32 +03:00
Pasha
abd73327ad second try write to sqlc queries 2024-11-15 11:24:28 +03:00
Pasha
08e5db900c first try write to sqlc queries 2024-11-15 11:11:22 +03:00
Pasha
524cc86ea9 - 2024-11-14 16:46:11 +03:00
Pasha
3ea9617ab4 added genarate db sql files 2024-11-14 16:05:42 +03:00
Pasha
90dff5e262 added genarate db sql files 2024-11-14 16:04:56 +03:00
Pasha
46b3111e53 fix cycle imports 2024-11-14 14:57:20 +03:00
Pasha
100696f35c update main bluprint logic 2024-11-14 14:47:46 +03:00
Pasha
45bf4f6bb5 - 2024-11-14 12:21:08 +03:00
Pasha
ca3f7ef292 added trash gen pagination queries 2024-11-13 16:19:18 +03:00
Pasha
5386195bda added templates for pagination 2024-11-13 12:48:17 +03:00
Pasha
5ec42b681a added templates for pagination 2024-11-13 12:46:16 +03:00
9226ec1e5e some update validation 2024-03-03 23:12:16 +03:00
cef7a97724 update readme 2024-03-01 18:22:42 +03:00
bb4adf590b update examples 2024-03-01 18:15:54 +03:00
c0b1412fdf check 2024-03-01 17:59:22 +03:00
3eaefe7d82 check 2024-03-01 17:54:11 +03:00
41daab9a94 check 2024-03-01 17:48:35 +03:00
0dce9424e5 check 2024-03-01 17:39:52 +03:00
11f34fa4b3 check 2024-03-01 17:04:41 +03:00
5de742ed05 check 2024-03-01 16:52:11 +03:00
638c427939 check 2024-03-01 16:46:14 +03:00
c3a0b6df56 check 2024-03-01 16:43:17 +03:00
ecd2fa15f8 check 2024-03-01 16:38:27 +03:00
2fee8935be check 2024-03-01 16:28:20 +03:00
38dfd11f85 check 2024-03-01 15:54:15 +03:00
d96eff32db check 2024-03-01 15:45:36 +03:00
82a891787a check 2024-03-01 15:35:32 +03:00
ff60de838b check 2024-03-01 15:13:51 +03:00
84a7f64231 check 2024-03-01 14:53:49 +03:00
a66b186b05 check 2024-03-01 14:46:17 +03:00
220dd43a4f . 2024-02-29 22:39:53 +03:00
79695ceac0 implement 2024-02-29 22:33:56 +03:00
47cfd6c008 implement 2024-02-29 22:21:30 +03:00
c49ffb1d51 implement 2024-02-29 22:16:30 +03:00
c08258f612 implement 2024-02-29 22:13:16 +03:00
6d93f92fe2 implement 2024-02-29 21:29:18 +03:00
aa5df3fadc implement 2024-02-29 21:24:59 +03:00
ee7fa89369 implement 2024-02-29 21:16:39 +03:00
24e798578e add gen server files 2024-02-29 21:04:29 +03:00
3d4901e746 update module name 2024-02-29 16:41:07 +03:00
da46f5a267 update gen 2024-02-29 16:31:00 +03:00
039ed007e4 add all gen slices 2024-02-29 01:18:04 +03:00
7ceae9b8dc add full base logic for generated validation 2024-02-27 21:42:45 +03:00
e19edb346b gen validate for controllers 2024-02-27 17:16:38 +03:00
ae4e40fc6b update 2024-02-27 13:50:46 +03:00
be0cd54df8 update modules: model kroki 2024-02-27 13:43:02 +03:00
a9d511e5c6 some update 2024-02-26 20:52:13 +03:00
70afb2781c update 2024-02-26 19:39:26 +03:00
1a73a25af5 fix err 2024-02-26 18:30:44 +03:00
4e609e9126 add init routes controllers 2024-02-26 18:26:29 +03:00
47b62b5e07 add gen controllers 2024-02-26 17:42:10 +03:00
8f9030f943 rename controller files 2024-02-26 12:51:52 +03:00
91264c021a check error 2024-02-26 02:42:13 +03:00
17bc98db5e some update gen logiq req resp files 2024-02-26 02:36:33 +03:00
c35fb1b761 some changes 2024-02-26 01:00:22 +03:00
Danil Solovyov
663ac8a298 Changes:
- logger.Declaration добавлена переменная varName
- env.Declaration переменная v переименована в varName
- utils добавлена функция для получения строки ошибки для вывода в сгенерированный файл
2024-01-15 19:46:27 +05:00
Danil Solovyov
e64e32874f Changes:
- теперь imports модулей не обернуты в двойные кавычки
- добавлены переменные в шаблонизаторе ProjectRoot и ProjectModule
- пример поправлен в соответствии с gitlab спецификацией
2024-01-15 18:12:56 +05:00
Danil
293b0b783a Update README.md 2024-01-15 10:50:05 +00:00
Danil Solovyov
fee55ca007 Added:
- Модуль openapi
- Пакет utils

Features:
- Генерация диаграммы базы данных
- Парсинг моделей

Changes:
- Изменена последовательность работы модулей и шаблонизатора
- Из шаблонизатора убран вызов команд и перенесен в creator
- Изменен вывод сообщений логгера на консольный
2024-01-15 03:44:50 +05:00
37 changed files with 3169 additions and 86 deletions

@ -62,13 +62,17 @@ blueprint
### Настройки
`path` - альтернативное расположение папки.\
`vars` - хеш-таблица значений, доступных в шаблонах. Пример в шаблоне: `{{.Vars.SomeValue}}`
`vars` - хеш-таблица значений, доступных в шаблонах. Пример в шаблоне: `{{.Vars.SomeValue}}`\
Для `vars` есть предопределенные значения:
- `ProjectRoot` - Корневая папка проекта. Абсолютный путь вызова приложения проекта.
- `ProjectModule` - Название модуля проекта.
## Модули их настройки и функции
Если модуль не будет указан в файле настроек, то он не будет вызван и не сможет использоваться в шаблонизаторе.\
Доступные модули:
- logger
- env
- openapi
## logger
Модуль логгера для шаблонизатора
@ -77,9 +81,9 @@ blueprint
`name` - Название логгера. Допустимые значения: `zap`
### Функции модуля для шаблонизатора
`logger.Import()` - возвращает строку для импорта выбранного логгера. Например: `{{.Modules.logger.Import}}` для zap вернет `"go.uber.org/zap"`.\
`logger.ImportCore()` - возвращает строку для импорта ядра выбранного логгера. Например: `{{.Modules.logger.ImportCore}}` для zap вернет `"go.uber.org/zap/core"`.\
`logger.Declaration()` - возвращает декларирование выбранного логгера. Например: `{{.Modules.logger.Declaration}}` для zap вернет:
`logger.Import()` - возвращает строку для импорта выбранного логгера. Например: `{{.Modules.logger.Import}}` для zap вернет `go.uber.org/zap`.\
`logger.ImportCore()` - возвращает строку для импорта ядра выбранного логгера. Например: `{{.Modules.logger.ImportCore}}` для zap вернет `go.uber.org/zap/core`.\
`logger.Declaration(varName string)` - возвращает декларирование выбранного логгера. Например: `{{.Modules.logger.Declaration "logger"}}` для zap вернет:
```golang
cfgLogger := zap.NewDevelopmentConfig()
@ -104,7 +108,7 @@ logger, err := cfgLogger.Build()
- `name` - имя переменной окружения, в структуре именуется в CamelCase. **Обязательное значение.**
- `type` - тип переменной для структуры. **Обязательное значение.**
- `default` - стандартное значение.
- `required` - является ли значение обязательным. По умолчанию: false
- `required` - является ли значение обязательным. **По умолчанию: false**
Пример:
```yaml
@ -116,8 +120,8 @@ vars:
```
### Функции модуля для шаблонизатора
`env.Import()` - возвращает строку для импорта env. `{{.Modules.env.Import}}` вернет `"github.com/caarlos0/env/v8"`\
`env.Declaration(v string)` - возвращает декларирование переменной с заполненной структурой переменных окружения. `{{.Modules.env.Declaration config}}` вернет
`env.Import()` - возвращает строку для импорта env. `{{.Modules.env.Import}}` вернет `github.com/caarlos0/env/v8`\
`env.Declaration(varName string)` - возвращает декларирование переменной с заполненной структурой переменных окружения. `{{.Modules.env.Declaration "config"}}` вернет
```golang
err = env.Parse(config)
if err != nil {
@ -131,3 +135,68 @@ type Config struct {
AppName string `env:"APP_NAME,required" envDefault:"Some app name"`
}
```
## openapi
Модуль для генерации файлов Golang моделей и файла `database.puml` - диаграммы базы данных. Читает файл Openapi 3 спецификации из места вызова проекта.\
Рекомендуемое имя файла спецификации `Openapi.yaml`.\
Допустимые имена файла: `Openapi.yaml`, `Openapi.yml`, `openapi.yaml`, `penapi.yml`.\
Также для генерации слоев контроллера, сервиса, репозитория и сервера, инициализация всех слоев выводиться в app.\
Для добавления этого функционала стоит просто указать путь до желаемой директории, имя пакета будет имя директории.\
```yaml
openapi:
model_save_path: ./internal/models
controller_save_path: ./internal/controllers
service_save_path: ./internal/service
repository_save_path: ./internal/repository
server_save_path: ./internal/server/http
```
### Настройки
`full_path` - Полный путь к файлу OpenAPI. **По умолчанию: ./**\
`model_save_path` - Путь к папке сохранения моделей. **По умолчанию: ./models**\
`kroki_save_path` - Путь к папке сохранения диаграммы базы данных. **По умолчанию: ./**\
`db` - Название базы данных. Пока только добавляет теги к модели. **По умолчанию: nil**\
Допустимые значения:
- mongo
- sql
### Функции модуля для шаблонизатора
`openapi.KrokiPUML()` - возвращает диаграмму базы данных в формате PUML. Например `{{.Modules.openapi.KrokiPUML}}` вернет:
```plantuml
@startuml Database
map Category {
_id => **string** //primary_key//
name => **string**
products => **[]Product**
}
map Color {
r => **integer**
a => **integer**
b => **integer**
g => **integer**
}
map Product {
_id => **string** //primary_key//
color => **color**
name => **string**
status => **integer** //enum//
tags => **[]string**
}
map status {
0 => in_stock
1 => in reserve
2 => out-of-stock
}
Category::products -> Product
Product::color -> Color
Product::status -> status
@enduml
```

@ -2,10 +2,14 @@ package blueprint
import (
"fmt"
"github.com/rs/zerolog/log"
"os"
"os/exec"
"path/filepath"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/env"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/logger"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/layers"
)
type BlueprintCreator struct {
@ -17,7 +21,7 @@ type BlueprintCreator struct {
type BlueprintModule interface {
Name() string
SetSettings(settings any) error
Execute() error
Execute() (*layers.AllData, error)
}
func NewBlueprintCreator(blueprintPath, projectName string) (*BlueprintCreator, error) {
@ -34,6 +38,7 @@ func NewBlueprintCreator(blueprintPath, projectName string) (*BlueprintCreator,
blueprintModules := []BlueprintModule{
logger.NewLoggerModule(),
env.NewEnvModule(),
openapi.NewOpenAPIModule(),
}
templateModules := make(map[string]BlueprintModule, 0)
@ -63,6 +68,8 @@ func NewBlueprintCreator(blueprintPath, projectName string) (*BlueprintCreator,
template.SetVar("ProjectName", file.ProjectName)
template.SetVar("Author", file.Author)
template.SetVar("Description", file.Description)
template.SetVar("ProjectRoot", absExecPath)
template.SetVar("ProjectModule", filepath.Base(absExecPath))
return &BlueprintCreator{
template: template,
@ -72,15 +79,52 @@ func NewBlueprintCreator(blueprintPath, projectName string) (*BlueprintCreator,
}
func (r *BlueprintCreator) Create() error {
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
cmd := exec.Command("go", "mod", "init")
log.Info().Msg("execCommands: go mod init")
if err := cmd.Run(); err != nil {
return fmt.Errorf("execCommands: go mod init. %v", err)
}
}
// execute modules
var layersData *layers.AllData
for _, module := range r.modules {
data, err := module.Execute()
if err != nil {
return fmt.Errorf("module execute (%v). %v", module.Name(), err)
}
if data != nil {
layersData = data
}
}
r.template.settings.LayersData = layersData
// execute template
if err := r.template.Execute(); err != nil {
return fmt.Errorf("template execute. %v", err)
}
for _, module := range r.modules {
if err := module.Execute(); err != nil {
return fmt.Errorf("module execute (%v). %v", module.Name(), err)
}
err := layers.InstallGoimports()
if err != nil {
return fmt.Errorf("failed install go imports. %v", err)
}
err = layers.RunGoimports()
if err != nil {
return fmt.Errorf("failed linter go imports. %v", err)
}
// go mod tidy
cmd := exec.Command("go", "mod", "tidy")
log.Info().Msg("execCommands: go mod tidy")
if err := cmd.Run(); err != nil {
return fmt.Errorf("execCommands: go mod tidy. %v", err)
}
return nil
}

@ -3,11 +3,11 @@ package env
import (
"fmt"
"os/exec"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/layers"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
)
type EnvModule struct {
@ -36,13 +36,11 @@ func (r Var) code() string {
def = fmt.Sprintf(` envDefault:"%v"`, r.Default)
}
caser := cases.Title(language.English)
for _, item := range strings.Split(r.Name, "_") {
fieldName += caser.String(item)
fieldName += utils.TextToTitle(item)
}
return fmt.Sprintf(" %v %v `env:\"%v%v\"%v`", fieldName, r.Type, r.Name, req, def)
return fmt.Sprintf("\t%v %v `env:\"%v%v\"%v`", fieldName, r.Type, r.Name, req, def)
}
func NewEnvModule() *EnvModule {
@ -77,26 +75,25 @@ func (r *EnvModule) SetSettings(settings any) error {
return nil
}
func (r *EnvModule) Execute() error {
value := strings.Replace(r.Import(), `"`, "", -1)
cmd := exec.Command("go", "get", value)
func (r *EnvModule) Execute() (*layers.AllData, error) {
cmd := exec.Command("go", "get", r.Import())
return cmd.Run()
return nil, cmd.Run()
}
func (r *EnvModule) Import() string {
return `"github.com/caarlos0/env/v8"`
return "github.com/caarlos0/env/v8"
}
func (r *EnvModule) Declaration(v string) string {
if v == "" {
return "//FIXME TEMPLATE ERROR: env.Declaration required with non-empty argument"
func (r *EnvModule) Declaration(varName string) string {
if varName == "" {
return utils.TemplateError("env.Declaration required with non-empty argument")
}
return fmt.Sprintf(`err = env.Parse(%v)
if err != nil {
panic(err)
}`, v)
}`, varName)
}
func (r *EnvModule) Struct() string {

@ -3,9 +3,10 @@ package logger
import (
"fmt"
"os/exec"
"strings"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/layers"
"gopkg.in/yaml.v3"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
)
type LoggerModule struct {
@ -57,17 +58,16 @@ func (r *LoggerModule) SetSettings(settings any) error {
return nil
}
func (r *LoggerModule) Execute() error {
value := strings.Replace(r.Import(), `"`, "", -1)
cmd := exec.Command("go", "get", value)
func (r *LoggerModule) Execute() (*layers.AllData, error) {
cmd := exec.Command("go", "get", r.Import())
return cmd.Run()
return nil, cmd.Run()
}
type Logger interface {
Import() string
ImportCore() string
Declaration() string
Declaration(varName string) string
Type() string
Message(messageType string, message string, fields ...any) string
}
@ -80,8 +80,11 @@ func (r *LoggerModule) ImportCore() string {
return r.logger.ImportCore()
}
func (r *LoggerModule) Init() string {
return r.logger.Declaration()
func (r *LoggerModule) Declaration(varName string) string {
if varName == "" {
return utils.TemplateError("logger.Declaration required with non-empty argument")
}
return r.logger.Declaration(varName)
}
func (r *LoggerModule) Type() string {

@ -3,8 +3,7 @@ package logger
import (
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
)
type ZapLogger struct {
@ -15,22 +14,22 @@ func NewZapLogger() *ZapLogger {
}
func (r *ZapLogger) Import() string {
return `"go.uber.org/zap"`
return "go.uber.org/zap"
}
func (r *ZapLogger) ImportCore() string {
return `"go.uber.org/zap/zapcore"`
return "go.uber.org/zap/zapcore"
}
func (r *ZapLogger) Declaration() string {
return `cfgLogger := zap.NewDevelopmentConfig()
func (r *ZapLogger) Declaration(varName string) string {
return fmt.Sprintf(`cfgLogger := zap.NewDevelopmentConfig()
cfgLogger.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
cfgLogger.EncoderConfig.ConsoleSeparator = " "
logger, err := cfgLogger.Build()
%v, err := cfgLogger.Build()
if err != nil {
panic(err)
}`
}`, varName)
}
func (r *ZapLogger) Type() string {
@ -40,15 +39,14 @@ func (r *ZapLogger) Type() string {
func (r *ZapLogger) Message(messageType, message string, fields ...any) string {
var fieldsToString string
caser := cases.Title(language.English)
messageType = caser.String(messageType)
messageType = utils.TextToTitle(messageType)
if !r.isAllowedMessageType(messageType) {
return fmt.Sprintf(`//FIXME TEMPLATE ERROR: logger.Message messageType bad type. Allowed types: %v`, r.allowedMessageTypes())
return utils.TemplateError(fmt.Sprintf(`logger.Message messageType bad type. Allowed types: %v`, r.allowedMessageTypes()))
}
if len(fields)%2 != 0 {
return `//FIXME TEMPLATE ERROR: logger.Message need even number of fields. Example: logger.Message <messageType> <some msg> <fieldName> <fieldVal>...`
return utils.TemplateError("logger.Message need even number of fields. Example: logger.Message <messageType> <some msg> <fieldName> <fieldVal>...")
}
for i := 0; i < len(fields); i += 2 {

@ -0,0 +1,27 @@
package db
import "penahub.gitlab.yandexcloud.net/pena-services/blueprint/types/dbtypes"
type Mongo struct {
dbType dbtypes.DBType
}
func NewMongo(dbType dbtypes.DBType) *Mongo {
return &Mongo{dbType: dbType}
}
func (r *Mongo) Tag() string {
return "bson"
}
func (r *Mongo) GenerateSettings() error {
return nil
}
func (r *Mongo) SetTemplate(methodName string) string {
return "\n"
}
func (r *Mongo) DBType() dbtypes.DBType {
return r.dbType
}

@ -0,0 +1,51 @@
package db
import (
"fmt"
"os"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/types/dbtypes"
"strings"
)
type SQL struct {
dbType dbtypes.DBType
queriesTemplates []string
}
func NewSQL(dbType dbtypes.DBType) *SQL {
return &SQL{dbType: dbType, queriesTemplates: make([]string, 0)}
}
func (r *SQL) Tag() string {
return "sql"
}
func (r *SQL) GenerateSettings() error {
if err := os.WriteFile("sqlc.yaml", []byte(dbtypes.SqlcYamlTemplate), 0644); err != nil {
return fmt.Errorf("failed creating sqlc.yaml: %w", err)
}
dirPath := dbtypes.DirSqlcPath
if err := os.MkdirAll(dirPath, 0755); err != nil {
return fmt.Errorf("failed create dir sqlc/db_query: %w", err)
}
if err := os.WriteFile(dbtypes.PathToQueriesFile, []byte(strings.Join(r.queriesTemplates, "/n")), 0644); err != nil {
return fmt.Errorf("failed create file - queries.sql: %w", err)
}
return nil
}
func (r *SQL) SetTemplate(methodName string) string {
if strings.Contains(strings.ToLower(methodName), "getlist") {
r.queriesTemplates = append(r.queriesTemplates, fmt.Sprintf(dbtypes.PaginationSqlTemplate, methodName))
return ""
} else {
return ""
}
}
func (r *SQL) DBType() dbtypes.DBType {
return r.dbType
}

@ -0,0 +1,177 @@
package kroki
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/getkin/kin-openapi/openapi3"
"github.com/rs/zerolog/log"
)
var krokiTempate = `@startuml Database
{{range .Tables -}}
{{.String}}
{{end -}}
{{range .Enums -}}
{{.String}}
{{end -}}
{{range .Refs -}}
{{.String}}
{{end}}
@enduml`
type Kroki struct {
Tables []Table
Enums []Enum
Refs []Ref
}
type Ref struct {
ChildName string
ChildField string
ParentName string
ParentField string
}
func (r Ref) String() string {
parentLink := r.ParentName
if r.ParentField != "" {
parentLink += fmt.Sprintf("::%v", r.ParentField)
}
return fmt.Sprintf("%v::%v -> %v", r.ChildName, r.ChildField, parentLink)
}
type Enum struct {
Name string
Items []any
}
func (r Enum) String() string {
result := fmt.Sprintf("map %v {\n", r.Name)
for iterator, item := range r.Items {
result += fmt.Sprintf("\t%v => %v\n", iterator, item)
}
result += "}\n" //nolint
return result
}
const (
NotePrimaryKey = "primary_key"
NeedParentField = "need_parent_field"
)
func NewKroki() *Kroki {
return &Kroki{
Tables: make([]Table, 0),
Refs: make([]Ref, 0),
}
}
func (r *Kroki) AddTables(tables ...Table) {
r.Tables = append(r.Tables, tables...)
}
func (r *Kroki) AddRefs(refs ...Ref) {
r.Refs = append(r.Refs, refs...)
}
func (r *Kroki) AddEnums(enums ...Enum) {
r.Enums = append(r.Enums, enums...)
}
func (r *Kroki) FillFromOpenapi(openapi *openapi3.T) {
for name, schema := range openapi.Components.Schemas {
if strings.HasSuffix(strings.ToUpper(name), "REQ") || strings.HasSuffix(strings.ToUpper(name), "RESP") {
continue
}
krokiTable := NewKrokiTable(name)
krokiTable.FillFromSchema(schema.Value)
r.AddTables(*krokiTable)
}
}
func (r *Kroki) Generate() (string, error) {
// Fill References from Tables
for _, table := range r.Tables {
r.AddRefs(table.Refs...)
r.AddEnums(table.Enums...)
}
// Check Refs for fill
for _, ref := range r.Refs {
if ref.ParentField == NeedParentField {
for _, table := range r.Tables {
if table.Name == ref.ParentName {
for _, field := range table.Fields {
if field.Note == NotePrimaryKey {
ref.ParentField = field.Name
}
}
}
}
}
}
t, err := template.New("kroki").Parse(krokiTempate)
if err != nil {
return "", fmt.Errorf("Kroki.Parse: %v", err)
}
buffer := bytes.NewBuffer(nil)
if err = t.Execute(buffer, r); err != nil {
return "", fmt.Errorf("Kroki.Execute: %v", err)
}
return buffer.String(), nil
}
func (r *Kroki) SavePUMLFile(savePath string) error {
if savePath == "" {
return fmt.Errorf("Kroki: path cannot be empty")
}
if err := os.MkdirAll(savePath, 0750); err != nil {
return fmt.Errorf("Kroki.MkDirAll: %v", err)
}
absPath, err := filepath.Abs(savePath)
if err != nil {
return fmt.Errorf("Kroki.LogPath: %v", err)
}
filePath := filepath.Join(absPath, "database.puml")
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("Kroki.CreateFile: %v", err)
}
defer func() {
if err = file.Close(); err != nil {
log.Err(fmt.Errorf("Kroki.CloseFile: %v", err)).Str("filepath", filePath).Msg("cannot close file")
}
}()
text, err := r.Generate()
if err != nil {
return fmt.Errorf("Kroki.Generate: %v", err)
}
_, err = file.WriteString(text)
if err != nil {
return fmt.Errorf("Kroki.WriteToFile: %v", err)
}
log.Info().Str("file", filePath).Msg("kroki: file generated")
return nil
}

@ -0,0 +1,128 @@
package kroki
import (
"fmt"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
)
type Table struct {
Name string
Fields []KrokiTableField
Refs []Ref
Enums []Enum
}
type KrokiTableField struct {
Name string
Type string
Note string
}
func NewKrokiTable(name string) *Table {
return &Table{
Name: name,
Fields: make([]KrokiTableField, 0),
Refs: make([]Ref, 0),
Enums: make([]Enum, 0),
}
}
func (r *Table) AddFields(fields ...KrokiTableField) {
r.Fields = append(r.Fields, fields...)
}
func (r *Table) FillFromSchema(schema *openapi3.Schema) {
iterator := 0
for propertyName, property := range schema.Properties {
field := KrokiTableField{
Name: propertyName,
Type: property.Value.Type,
Note: property.Value.Format,
}
if len(property.Value.Enum) > 0 {
field.Type = "integer"
field.Note = "enum"
r.Enums = append(r.Enums, Enum{
Name: propertyName,
Items: property.Value.Enum,
})
r.Refs = append(r.Refs, Ref{
ChildName: r.Name,
ChildField: propertyName,
ParentName: propertyName,
})
}
if property.Value.Type == openapi3.TypeObject {
field.Type = propertyName
if property.Ref != "" {
ref := Ref{
ChildName: r.Name,
ChildField: propertyName,
ParentName: utils.TextGetLastElement(property.Ref, "/"),
}
r.Refs = append(r.Refs, ref)
}
}
if property.Value.Type == openapi3.TypeArray {
field.Type = fmt.Sprintf("[]%v", property.Value.Items.Value.Type)
if property.Value.Items.Ref != "" {
field.Type = "[]" + utils.TextGetLastElement(property.Value.Items.Ref, "/")
ref := Ref{
ChildName: r.Name,
ChildField: propertyName,
ParentName: utils.TextGetLastElement(property.Value.Items.Ref, "/"),
}
r.Refs = append(r.Refs, ref)
}
}
if strings.Contains(propertyName, "_id") && !utils.IsPrimaryKey(propertyName) {
ref := Ref{
ChildName: r.Name,
ChildField: propertyName,
ParentName: strings.TrimSuffix(propertyName, "_id"),
ParentField: NeedParentField,
}
r.Refs = append(r.Refs, ref)
}
if utils.IsPrimaryKey(propertyName) {
field.Note = NotePrimaryKey
if iterator != 0 {
r.AddFields(r.Fields[0])
r.Fields[0] = field
iterator++
continue
}
}
r.AddFields(field)
iterator++
}
}
func (r Table) String() string {
result := fmt.Sprintf("map %v {\n", r.Name)
for _, field := range r.Fields {
result += fmt.Sprintf("\t%v => **%v**", field.Name, field.Type)
if field.Note != "" {
result += fmt.Sprintf(" //%v//", field.Note)
}
result += "\n"
}
result += "}\n"
return result
}

@ -0,0 +1,188 @@
package kroki
import (
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/suite"
)
type KrokiTestSuite struct {
doc *openapi3.T
kroki *Kroki
suite.Suite
}
const testFile = "../openapi_test.yaml"
func (r *KrokiTestSuite) SetupSuite() {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(testFile)
r.NoErrorf(err, "cannot load openapi file: %v")
err = doc.Validate(loader.Context)
r.NoErrorf(err, "cannot validate openapi: %s")
r.doc = doc
r.kroki = NewKroki()
}
func (r *KrokiTestSuite) TearDownSuite() {}
func TestKrokiTableTest(t *testing.T) {
suite.Run(t, new(KrokiTestSuite))
}
func (r *KrokiTestSuite) TestFillFromSchema() {
type args struct {
modelName string
schema *openapi3.Schema
}
tableColor := Table{
Name: "Color",
Fields: []KrokiTableField{
{
Name: "r",
Type: "integer",
},
{
Name: "g",
Type: "integer",
},
{
Name: "b",
Type: "integer",
},
{
Name: "a",
Type: "integer",
},
},
Refs: make([]Ref, 0),
}
tableProduct := Table{
Name: "Product",
Fields: []KrokiTableField{
{
Name: "_id",
Type: "string",
Note: NotePrimaryKey,
},
{
Name: "name",
Type: "string",
},
{
Name: "color",
Type: "color",
},
{
Name: "tags",
Type: "[]string",
},
{
Name: "status",
Type: "integer",
Note: "enum",
},
},
Refs: []Ref{
{
ChildName: "Product",
ChildField: "color",
ParentName: "Color",
ParentField: "",
},
{
ChildName: "Product",
ChildField: "status",
ParentName: "status",
},
},
}
tableCategory := Table{
Name: "Category",
Fields: []KrokiTableField{
{
Name: "_id",
Type: "string",
Note: NotePrimaryKey,
},
{
Name: "name",
Type: "string",
},
{
Name: "products",
Type: "[]Product",
},
},
Refs: []Ref{
{
ChildName: "Category",
ChildField: "products",
ParentName: "Product",
},
},
}
tests := []struct {
name string
got *Table
args args
want Table
}{
{
name: "Color",
got: NewKrokiTable("Color"),
args: args{
modelName: "Color",
schema: r.doc.Components.Schemas["Color"].Value,
},
want: tableColor,
},
{
name: "Product",
got: NewKrokiTable("Product"),
args: args{
modelName: "Product",
schema: r.doc.Components.Schemas["Product"].Value,
},
want: tableProduct,
},
{
name: "Category",
got: NewKrokiTable("Category"),
args: args{
modelName: "Category",
schema: r.doc.Components.Schemas["Category"].Value,
},
want: tableCategory,
},
}
for _, tt := range tests {
r.Run(tt.name, func() {
tt.got.FillFromSchema(tt.args.schema)
r.Equal(tt.want.Name, tt.got.Name)
r.ElementsMatch(tt.want.Fields, tt.got.Fields)
r.ElementsMatch(tt.want.Refs, tt.got.Refs)
r.kroki.AddTables(*tt.got)
})
}
}
func (r *KrokiTestSuite) TestGenerate() {
}
func (r *KrokiTestSuite) TestSavePUMLFile() {
if err := r.kroki.SavePUMLFile("./model_test"); err != nil {
r.NoError(err)
}
}

@ -0,0 +1,115 @@
package layers
var httpMethods = map[string]string{
"GET": "Get",
"HEAD": "Head",
"POST": "Post",
"PUT": "Put",
"PATCH": "Patch",
"DELETE": "Delete",
"CONNECT": "Connect",
"OPTIONS": "Options",
"TRACE": "Trace",
}
const controllerTemplate = `
package {{ .PackageName }}
import (
"github.com/gofiber/fiber/v2"
)
type {{ .ControllerName }}Controller struct {
{{ .ControllerName }}Service *{{ .PackageService }}.{{ .ControllerName }}Service
}
func New{{ .ControllerName }}Controller(service *{{ .PackageService }}.{{ .ControllerName }}Service) *{{ .ControllerName }}Controller {
return &{{ .ControllerName }}Controller{
{{ .ControllerName }}Service: service,
}
}
func (c *{{ .ControllerName }}Controller) Register(router fiber.Router) {
{{ range .Endpoints }}
router.{{ execute "method" .MethodName .EndpointData }}("{{ execute "endpoint" .MethodName .EndpointData }}", c.{{ .MethodName }})
{{ end }}
}
func (c *{{ .ControllerName }}Controller) Name() string {
return ""
}
{{ range .Endpoints }}
func (c *{{ $.ControllerName }}Controller) {{ .MethodName }}(ctx *fiber.Ctx) error {
// Обработчик для метода {{ .MethodName }}
{{ $modelName := execute "model" .MethodName .EndpointData }}
{{ $requestType := execute "requestType" .MethodName .EndpointData }}
{{ $response := execute "response" .MethodName .EndpointData }}
{{ $responseType := execute "responseType" .MethodName .EndpointData }}
{{ if $modelName }}
var request {{ $requestType }}{{ $.ModelPackageName }}.{{ $modelName }}
if err := ctx.BodyParser(&request); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
{{ with $validationData := executeValidation .MethodName .EndpointData $.ValidationCode }}
{{ $validationData }}
{{ end }}
{{ end }}
{{ if eq $response "" }}
{{ if eq $modelName "" }}
err := c.{{ $.ControllerName }}Service.{{ .MethodName }}(ctx.Context())
{{ else }}
err := c.{{ $.ControllerName }}Service.{{ .MethodName }}(ctx.Context(), &request)
{{ end }}
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
{{ else }}
{{ if eq $modelName "" }}
response, err := c.{{ $.ControllerName }}Service.{{ .MethodName }}(ctx.Context())
{{ else }}
response, err := c.{{ $.ControllerName }}Service.{{ .MethodName }}(ctx.Context(), &request)
{{ end }}
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.Status(fiber.StatusOK).JSON(response)
{{ end }}
}
{{ end }}
`
type ControllerTemplateData struct {
PackageName string
ModelPackageName string
ControllerName string
Endpoints []Endpoint
ValidationCode map[string]string
PackageService string
}
type Endpoint struct {
MethodName string
EndpointData map[string]EndpointData
}
type EndpointData struct {
Response string
ResponseType string
ModelName string
ModelType string
HttpMethod string
Endpoint string
}
type ResponseData struct {
Response string
ResponseType string
}
type RequestData struct {
Request string
RequestType string
}

@ -0,0 +1,323 @@
package layers
import (
"fmt"
"github.com/getkin/kin-openapi/openapi3"
"os/exec"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/types/dbtypes"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
"strings"
)
func generateValidationCode(endpoints []Endpoint, openapi *openapi3.T) (map[string]string, map[string]RequestData, map[string]ResponseData) {
validationCodes := make(map[string]string)
respMap := make(map[string]ResponseData)
reqMap := make(map[string]RequestData)
for _, endpoint := range endpoints {
response, responseType, err := getResponseSchema(endpoint.EndpointData[endpoint.MethodName].Endpoint, endpoint.EndpointData[endpoint.MethodName].HttpMethod, openapi)
request, requesttype, err := getRequestSchema(endpoint.EndpointData[endpoint.MethodName].Endpoint, endpoint.EndpointData[endpoint.MethodName].HttpMethod, openapi)
respMap[endpoint.MethodName] = ResponseData{Response: response, ResponseType: responseType}
reqMap[endpoint.MethodName] = RequestData{Request: request, RequestType: requesttype}
if err != nil {
fmt.Println("Failed to find RequestSchema or ResponseSchema:", err)
continue
}
schema, found := openapi.Components.Schemas[request]
if !found {
fmt.Println("Schema not found in components:", request)
continue
}
var validationCode strings.Builder
for propName, prop := range schema.Value.Properties {
propName = utils.TextToCamelCase(propName)
switch prop.Value.Type {
case "string":
if prop.Value.Enum != nil {
enumList := make([]string, len(prop.Value.Enum))
for i, v := range prop.Value.Enum {
enumList[i] = v.(string)
}
validationCode.WriteString("statusValue := request." + propName + ".String()\n")
validationCode.WriteString("if statusValue == \"Unknown\" {\n")
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"%s must be one of %s\")\n", propName, strings.Join(enumList, ", ")))
validationCode.WriteString("}\n")
}
if prop.Value.MaxLength != nil {
validationCode.WriteString(fmt.Sprintf("if len(request.%s) > %d {\n", propName, int(*prop.Value.MaxLength)))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Maximum length exceeded for %s\")\n", propName))
validationCode.WriteString("}\n")
}
if prop.Value.MinLength != 0 {
validationCode.WriteString(fmt.Sprintf("if len(request.%s) < %d {\n", propName, int(prop.Value.MinLength)))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Minimum length not met for %s\")\n", propName))
validationCode.WriteString("}\n")
}
case "integer":
if prop.Value.Max != nil {
validationCode.WriteString(fmt.Sprintf("if request.%s > %d {\n", propName, int(*prop.Value.Max)))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Value for %s must be less than or equal to %d\")\n", propName, int(*prop.Value.Max)))
validationCode.WriteString("}\n")
}
if prop.Value.Min != nil {
validationCode.WriteString(fmt.Sprintf("if request.%s < %d {\n", propName, int(*prop.Value.Min)))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Value for %s must be greater than or equal to %d\")\n", propName, int(*prop.Value.Min)))
validationCode.WriteString("}\n")
}
case "boolean":
if prop.Value.Default != nil {
defaultBool := prop.Value.Default.(bool)
validationCode.WriteString(fmt.Sprintf("if request.%s != %t {\n", propName, defaultBool))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"%s must be %t\")\n", propName, defaultBool))
validationCode.WriteString("}\n")
}
case "array":
if prop.Value.MinItems != 0 && prop.Value.MaxItems != nil {
validationCode.WriteString(fmt.Sprintf("if len(request.%s) < %d || len(request.%s) > %d {\n", propName, prop.Value.MinItems, propName, *prop.Value.MaxItems))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Array %s must contain between %d and %d items\")\n", propName, prop.Value.MinItems, *prop.Value.MaxItems))
validationCode.WriteString("}\n")
} else if prop.Value.MinItems != 0 {
validationCode.WriteString(fmt.Sprintf("if len(request.%s) < %d {\n", propName, prop.Value.MinItems))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Array %s must contain at least %d items\")\n", propName, prop.Value.MinItems))
validationCode.WriteString("}\n")
} else if prop.Value.MaxItems != nil {
validationCode.WriteString(fmt.Sprintf("if len(request.%s) > %d {\n", propName, *prop.Value.MaxItems))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Array %s must contain at most %d items\")\n", propName, *prop.Value.MaxItems))
validationCode.WriteString("}\n")
}
//if prop.Value.UniqueItems {
// validationCode.WriteString(fmt.Sprintf("if !чтото.isUnique(request.%s) {\n", propName))
// validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"%s must contain unique items\")\n", propName))
// validationCode.WriteString("}\n")
//}
case "object":
for innerPropName, innerProp := range prop.Value.Properties {
innerPropName = utils.TextToCamelCase(innerPropName)
switch innerProp.Value.Type {
case "string":
if innerProp.Value.MaxLength != nil {
validationCode.WriteString(fmt.Sprintf("if len(request.%s.%s) > %d {\n", propName, innerPropName, int(*innerProp.Value.MaxLength)))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Maximum length exceeded for %s\")\n", innerPropName))
validationCode.WriteString("}\n")
}
if innerProp.Value.MinLength != 0 {
validationCode.WriteString(fmt.Sprintf("if len(request.%s.%s) < %d {\n", propName, innerPropName, int(innerProp.Value.MinLength)))
validationCode.WriteString(fmt.Sprintf(" return fiber.NewError(fiber.StatusBadRequest, \"Minimum length not met for %s\")\n", innerPropName))
validationCode.WriteString("}\n")
}
}
}
}
}
if validationCode.String() != "" {
key := fmt.Sprintf("%s_%s", endpoint.EndpointData[endpoint.MethodName].HttpMethod, endpoint.EndpointData[endpoint.MethodName].Endpoint)
validationCodes[key] = validationCode.String()
}
}
return validationCodes, reqMap, respMap
}
func getRequestSchema(endpoint, httpMethod string, openapi *openapi3.T) (string, string, error) {
pathItem, ok := openapi.Paths.Map()[endpoint]
if !ok {
return "", "", fmt.Errorf("endpoint not found")
}
operation, ok := pathItem.Operations()[strings.ToUpper(httpMethod)]
if !ok {
return "", "", fmt.Errorf("operation not found for method %s", httpMethod)
}
requestBody := operation.RequestBody
if requestBody != nil && requestBody.Value != nil && requestBody.Value.Content != nil {
for _, contentType := range requestBody.Value.Content {
if contentType.Schema.Ref != "" {
return strings.TrimPrefix(contentType.Schema.Ref, "#/components/schemas/"), contentType.Schema.Value.Type, nil
} else {
return strings.TrimPrefix(contentType.Schema.Value.Items.Ref, "#/components/schemas/"), contentType.Schema.Value.Type, nil
}
}
}
return "", "", fmt.Errorf("request schema not found")
}
func getResponseSchema(endpoint, httpMethod string, openapi *openapi3.T) (string, string, error) {
pathItem, ok := openapi.Paths.Map()[endpoint]
if !ok {
return "", "", fmt.Errorf("endpoint not found")
}
operation, ok := pathItem.Operations()[strings.ToUpper(httpMethod)]
if !ok {
return "", "", fmt.Errorf("operation not found for method %s", httpMethod)
}
responseBody := operation.Responses.Value("200")
if responseBody == nil {
return "", "", fmt.Errorf("response for status code 200 not found")
}
contentType := responseBody.Value.Content.Get("application/json")
if contentType == nil {
return "", "", fmt.Errorf("response content type application/json not found")
}
if contentType.Schema != nil {
if contentType.Schema.Ref != "" {
return strings.TrimPrefix(contentType.Schema.Ref, "#/components/schemas/"), "", nil
} else if contentType.Schema.Value != nil && contentType.Schema.Value.Items != nil && contentType.Schema.Value.Items.Ref != "" {
responseType := contentType.Schema.Value.Type
return strings.TrimPrefix(contentType.Schema.Value.Items.Ref, "#/components/schemas/"), responseType, nil
}
}
return "", "", fmt.Errorf("response schema not found")
}
func execute(action, methodName string, endpointData map[string]EndpointData) string {
switch action {
case "method":
methodData, ok := endpointData[methodName]
if !ok {
return ""
}
return httpMethods[strings.ToUpper(methodData.HttpMethod)]
case "endpoint":
methodData, ok := endpointData[methodName]
if !ok {
return ""
}
return methodData.Endpoint
case "model":
modelData, ok := endpointData[methodName]
if !ok {
return ""
}
return modelData.ModelName
case "response":
responseData, ok := endpointData[methodName]
if !ok {
return ""
}
return responseData.Response
case "responseType":
responseData, ok := endpointData[methodName]
if !ok {
return ""
}
if responseData.ResponseType == "array" {
return "[]"
}
return responseData.ResponseType
case "requestType":
requestData, ok := endpointData[methodName]
if !ok {
return ""
}
if requestData.ModelType == "array" {
return "[]"
} else if requestData.ModelType == "object" {
return ""
}
return requestData.ModelType
default:
return ""
}
}
func executeValidation(methodName string, endpointData map[string]EndpointData, validation map[string]string) string {
data, ok := endpointData[methodName]
if !ok {
return ""
}
return validation[data.HttpMethod+"_"+data.Endpoint]
}
func RunGoimports() error {
cmd := exec.Command("goimports", "-w", ".")
err := cmd.Run()
if err != nil {
return err
}
return nil
}
func InstallGoimports() error {
cmd := exec.Command("go", "install", "golang.org/x/tools/cmd/goimports@latest")
err := cmd.Run()
if err != nil {
return err
}
return nil
}
func selectRepoMethodTemplate(methodName string, dbType dbtypes.DBType) string {
if strings.Contains(strings.ToLower(methodName), "getlist") {
switch dbType {
case dbtypes.DBTypeSQL:
return fmt.Sprintf(dbtypes.PaginationSqlRepoTemplate, methodName, methodName)
case dbtypes.DBTypeMongo:
return dbtypes.PaginationMongoTemplate
}
}
return "//TODO:IMPLEMENT ME"
}
func generateReceiverRepoStruct(structType string, dbType dbtypes.DBType) string {
if structType == "deps" {
if dbType == dbtypes.DBTypeSQL {
return "Queries *sqlcgen.Queries\n\tPool *sql.DB\n"
}
if dbType == dbtypes.DBTypeMongo {
return "Mdb *mongo.Collection"
}
}
if structType == "main" {
if dbType == dbtypes.DBTypeSQL {
return "queries *sqlcgen.Queries\n\tpool *sql.DB\n"
}
if dbType == dbtypes.DBTypeMongo {
return "mdb *mongo.Collection"
}
}
if structType == "receiver" {
if dbType == dbtypes.DBTypeSQL {
return "queries:deps.Queries,\n\tpool:deps.Pool,"
}
if dbType == dbtypes.DBTypeMongo {
return "mdb:deps.Mdb,"
}
}
return ""
}
func imports(layerType LayerType, whatIt interface{}) string {
if layerType == RepositoryLayer {
if whatIt.(dbtypes.DBType) == dbtypes.DBTypeSQL {
return ""
}
if whatIt.(dbtypes.DBType) == dbtypes.DBTypeMongo {
return "\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/readpref\""
}
}
if layerType == ServiceLayer {
// todo
return ""
}
if layerType == ControllerLayer {
// todo
return ""
}
return ""
}

@ -0,0 +1,335 @@
package layers
import (
"fmt"
"github.com/getkin/kin-openapi/openapi3"
"os"
"path/filepath"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/types/dbtypes"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
"strings"
"text/template"
)
type LayerType string
const (
ControllerLayer LayerType = "controller"
ServiceLayer LayerType = "service"
RepositoryLayer LayerType = "repository"
)
type Deps struct {
SaveModelPath string
PackageName string
SavePathController string
SavePathService string
SavePathRepository string
SavePathServer string
DB dbtypes.DB
}
type Layers struct {
SaveModelPath string
PackageName string
SavePathController string
SavePathService string
SavePathRepository string
SavePathServer string
Controllers map[string]ControllerData
Services map[string]ServiceData
Repositories map[string]RepositoryData
db dbtypes.DB
}
type ControllerData struct {
Name string
PackageName string
}
type ServiceData struct {
Name string
PackageName string
}
type RepositoryData struct {
Name string
PackageName string
}
type AllData struct {
Controllers map[string]ControllerData
Services map[string]ServiceData
Repositories map[string]RepositoryData
ServerData string
}
func NewLayers(deps Deps) *Layers {
return &Layers{
SaveModelPath: deps.SaveModelPath,
PackageName: deps.PackageName,
SavePathController: deps.SavePathController,
SavePathService: deps.SavePathService,
SavePathRepository: deps.SavePathRepository,
SavePathServer: deps.SavePathServer,
Controllers: make(map[string]ControllerData),
Services: make(map[string]ServiceData),
Repositories: make(map[string]RepositoryData),
db: deps.DB,
}
}
func (l *Layers) GetData() AllData {
return AllData{
Controllers: l.Controllers,
Services: l.Services,
Repositories: l.Repositories,
ServerData: filepath.Base(l.SavePathServer),
}
}
func (l *Layers) FillFromOpenapi(openapi *openapi3.T) {
// Генерация репозитория сервиса контроллера сервера
l.GenerateAll(openapi)
}
func (l *Layers) GenerateAll(openapi *openapi3.T) {
tags := make(map[string][]Endpoint)
for path, pathItem := range openapi.Paths.Map() {
for method, operation := range pathItem.Operations() {
for _, tag := range operation.Tags {
tags[tag] = append(tags[tag], Endpoint{
MethodName: utils.TextToCamelCase(operation.OperationID),
EndpointData: map[string]EndpointData{
utils.TextToCamelCase(operation.OperationID): {
HttpMethod: httpMethods[method],
Endpoint: path,
},
},
})
}
}
}
for tagName, endpoints := range tags {
controllerName := strings.Title(tagName)
err := l.GenerateControllerData(controllerName, endpoints, openapi)
if err != nil {
fmt.Println("Failed to generate layers:", err)
continue
}
}
for tagName, endpoints := range tags {
serviceName := strings.Title(tagName)
err := l.GenerateServiceFiles(serviceName, endpoints)
if err != nil {
fmt.Println("Failed to generate service:", err)
continue
}
l.Services[strings.ToLower(serviceName)] = ServiceData{
Name: serviceName,
PackageName: filepath.Base(l.SavePathService),
}
}
for tagName, endpoints := range tags {
repositoryName := strings.Title(tagName)
err := l.GenerateRepositoryFiles(repositoryName, endpoints)
if err != nil {
fmt.Println("Failed to generate repository:", err)
continue
}
l.Repositories[strings.ToLower(repositoryName)] = RepositoryData{
Name: repositoryName,
PackageName: filepath.Base(l.SavePathRepository),
}
}
err := l.GenerateServerFiles()
if err != nil {
fmt.Println("Failed to generate Server Files:", err)
}
err = l.db.GenerateSettings()
if err != nil {
fmt.Println("Failed to generate database settings:", err)
}
}
func (l *Layers) GenerateControllerData(controllerName string, endpoints []Endpoint, openapi *openapi3.T) error {
validationCodes, reqMap, respMap := generateValidationCode(endpoints, openapi)
for _, endpoint := range endpoints {
tipe := respMap[endpoint.MethodName].ResponseType
resp := respMap[endpoint.MethodName].Response
req := reqMap[endpoint.MethodName]
data := EndpointData{
Response: resp,
ResponseType: tipe,
ModelName: req.Request,
ModelType: req.RequestType,
HttpMethod: endpoint.EndpointData[endpoint.MethodName].HttpMethod,
Endpoint: endpoint.EndpointData[endpoint.MethodName].Endpoint,
}
endpoint.EndpointData[endpoint.MethodName] = data
}
err := l.GenerateControllerFiles(controllerName, endpoints, validationCodes)
if err != nil {
return err
}
l.Controllers[strings.ToLower(controllerName)] = ControllerData{
Name: controllerName,
PackageName: filepath.Base(l.SavePathController),
}
return nil
}
func (l *Layers) GenerateControllerFiles(controllerName string, endpoints []Endpoint, validationCodes map[string]string) error {
data := ControllerTemplateData{
PackageName: filepath.Base(l.SavePathController),
ControllerName: controllerName,
Endpoints: endpoints,
ValidationCode: validationCodes,
PackageService: filepath.Base(l.SavePathService),
ModelPackageName: l.PackageName,
}
err := os.MkdirAll(filepath.Dir(filepath.Join(l.SavePathController, strings.ToLower(controllerName)+".go")), 0755)
if err != nil {
return err
}
file, err := os.Create(filepath.Join(l.SavePathController, strings.ToLower(controllerName)+".go"))
if err != nil {
return err
}
defer file.Close()
tmpl, err := template.New("layers").Funcs(template.FuncMap{
"execute": execute,
"executeValidation": executeValidation,
}).Parse(controllerTemplate)
if err != nil {
return err
}
if err := tmpl.Execute(file, data); err != nil {
return err
}
return nil
}
func (l *Layers) GenerateRepositoryFiles(repositoryName string, endpoints []Endpoint) error {
data := Repository{
PackageName: filepath.Base(l.SavePathRepository),
ModelPackageName: l.PackageName,
RepoName: repositoryName,
Endpoints: endpoints,
DBType: l.db.DBType(),
}
err := os.MkdirAll(filepath.Dir(filepath.Join(l.SavePathRepository, strings.ToLower(repositoryName)+".go")), 0755)
if err != nil {
return err
}
file, err := os.Create(filepath.Join(l.SavePathRepository, strings.ToLower(repositoryName)+".go"))
if err != nil {
return err
}
defer file.Close()
tmpl, err := template.New("repository").Funcs(template.FuncMap{
"execute": execute,
"executeValidation": executeValidation,
"selectRepoMethodTemplate": selectRepoMethodTemplate,
"generateReceiverRepoStruct": generateReceiverRepoStruct,
"imports": imports,
"setDBTemplate": l.db.SetTemplate,
}).Parse(repositoryTemplate)
if err != nil {
return err
}
if err := tmpl.Execute(file, data); err != nil {
return err
}
return nil
}
func (l *Layers) GenerateServiceFiles(serviceName string, endpoints []Endpoint) error {
data := Service{
PackageName: filepath.Base(l.SavePathService),
ModelPackageName: l.PackageName,
ServiceName: serviceName,
Endpoints: endpoints,
PackageRepo: filepath.Base(l.SavePathRepository),
}
err := os.MkdirAll(filepath.Dir(filepath.Join(l.SavePathService, strings.ToLower(serviceName)+".go")), 0755)
if err != nil {
return err
}
file, err := os.Create(filepath.Join(l.SavePathService, strings.ToLower(serviceName)+".go"))
if err != nil {
return err
}
defer file.Close()
tmpl, err := template.New("service").Funcs(template.FuncMap{
"execute": execute,
"executeValidation": executeValidation,
}).Parse(serviceTemplate)
if err != nil {
return err
}
if err := tmpl.Execute(file, data); err != nil {
return err
}
return nil
}
func (l *Layers) GenerateServerFiles() error {
data := Server{
PackageName: filepath.Base(l.SavePathServer),
}
err := os.MkdirAll(filepath.Dir(filepath.Join(l.SavePathServer, strings.ToLower("http")+".go")), 0755)
if err != nil {
return err
}
file, err := os.Create(filepath.Join(l.SavePathServer, strings.ToLower("http")+".go"))
if err != nil {
return err
}
defer file.Close()
tmpl, err := template.New("server").Parse(serverTemplate)
if err != nil {
return err
}
if err := tmpl.Execute(file, data); err != nil {
return err
}
return nil
}

@ -0,0 +1,60 @@
package layers
import (
"fmt"
"github.com/getkin/kin-openapi/openapi3"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/db"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/kroki"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/model"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/types/dbtypes"
"testing"
)
const (
testFile = "../openapi_test.yaml"
)
func TestCreateRequestModelsFiles(t *testing.T) {
deps := Deps{
SaveModelPath: "../model/model_test",
PackageName: "model_test",
SavePathController: "./controller/",
SavePathService: "./service/",
SavePathRepository: "./repository/",
SavePathServer: "./server/http/",
DB: db.NewMongo(dbtypes.DBTypeSQL),
}
c := NewLayers(deps)
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(testFile)
if err != nil {
fmt.Println(err)
}
c.FillFromOpenapi(doc)
}
func TestCreateModels(t *testing.T) {
c := model.NewModel("../model/model_test")
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(testFile)
if err != nil {
fmt.Println(err)
}
err = c.FillFromOpenapi(doc)
if err != nil {
fmt.Println(err)
}
}
func TestKroki(t *testing.T) {
c := kroki.NewKroki()
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(testFile)
if err != nil {
fmt.Println(err)
}
c.FillFromOpenapi(doc)
if err = c.SavePUMLFile("../model/model_test"); err != nil {
fmt.Println(err)
}
}

@ -0,0 +1,61 @@
package layers
import (
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/types/dbtypes"
)
const repositoryTemplate = `
package {{ .PackageName }}
import (
"context"
{{ imports "repository" .DBType }}
)
type {{ .RepoName }}Repository struct {
{{ generateReceiverRepoStruct "main" .DBType }}
}
type {{ .RepoName }}Deps struct {
{{ generateReceiverRepoStruct "deps" .DBType }}
}
func New{{ .RepoName }}Repository(deps {{ .RepoName }}Deps) *{{ .RepoName }}Repository {
return &{{ .RepoName }}Repository{
{{ generateReceiverRepoStruct "receiver" .DBType }}
}
}
{{ range .Endpoints }}
{{ setDBTemplate .MethodName}}
{{ $modelName := execute "model" .MethodName .EndpointData }}
{{ $response := execute "response" .MethodName .EndpointData }}
{{ $responseType := execute "responseType" .MethodName .EndpointData }}
{{ $requestType := execute "requestType" .MethodName .EndpointData }}
func (r *{{ $.RepoName }}Repository) {{ .MethodName }}(ctx context.Context{{ if $modelName }}, request *{{ $requestType }}{{ $.ModelPackageName }}.{{ $modelName }}{{ end }}) ({{ if $response }}{{$responseType}}*{{ $.ModelPackageName }}.{{ $response }}, {{ end }}error) {
{{ selectRepoMethodTemplate .MethodName $.DBType }}
{{ if $modelName }}
{{ if $response }}
return &{{ $.ModelPackageName }}.{{ $response }}{}, nil
{{ else }}
return nil
{{ end }}
{{ else }}
{{ if $response }}
return &{{ $.ModelPackageName }}.{{ $response }}{}, nil
{{ else }}
return nil
{{ end }}
{{ end }}
}
{{ end }}
`
type Repository struct {
PackageName string
ModelPackageName string
RepoName string
Endpoints []Endpoint
DBType dbtypes.DBType
}

@ -0,0 +1,69 @@
package layers
const serverTemplate = `
package {{ .PackageName }}
import (
"context"
"fmt"
"github.com/gofiber/fiber/v2"
)
type ServerConfig struct {
Controllers []Controller
}
type Server struct {
Controllers []Controller
app *fiber.App
}
func NewServer(config ServerConfig) *Server {
app := fiber.New()
s := &Server{
Controllers: config.Controllers,
app: app,
}
s.registerRoutes()
return s
}
func (s *Server) Start(addr string) error {
if err := s.app.Listen(addr); err != nil {
return err
}
return nil
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.app.Shutdown()
}
func (s *Server) registerRoutes() {
for _, c := range s.Controllers {
router := s.app.Group(c.Name())
c.Register(router)
}
}
type Controller interface {
Register(router fiber.Router)
Name() string
}
func (s *Server) ListRoutes() {
fmt.Println("Registered routes:")
for _, stack := range s.app.Stack() {
for _, route := range stack {
fmt.Printf("%s %s\n", route.Method, route.Path)
}
}
}
`
type Server struct {
PackageName string
}

@ -0,0 +1,65 @@
package layers
const serviceTemplate = `
package {{ .PackageName }}
import (
"context"
)
type {{ .ServiceName }}Service struct {
{{ .ServiceName }}Repository *{{ .PackageRepo }}.{{ .ServiceName }}Repository
}
func New{{ .ServiceName }}Service(repository *{{ .PackageRepo }}.{{ .ServiceName }}Repository) *{{ .ServiceName }}Service {
return &{{ .ServiceName }}Service{
{{ .ServiceName }}Repository: repository,
}
}
{{ range .Endpoints }}
{{ $modelName := execute "model" .MethodName .EndpointData }}
{{ $response := execute "response" .MethodName .EndpointData }}
{{ $responseType := execute "responseType" .MethodName .EndpointData }}
{{ $requestType := execute "requestType" .MethodName .EndpointData }}
func (s *{{ $.ServiceName }}Service) {{ .MethodName }}(ctx context.Context{{ if $modelName }}, request *{{ $requestType }}{{ $.ModelPackageName }}.{{ $modelName }}{{ end }}) ({{ if $response }}{{$responseType}}*{{ $.ModelPackageName }}.{{ $response }}, {{ end }}error) {
{{ if $modelName }}
{{ if $response }}
response, err := s.{{ $.ServiceName }}Repository.{{ .MethodName }}(ctx,{{ if $modelName }}request{{ end }})
if err != nil {
return nil, err
}
return response, nil
{{ else }}
err := s.{{ $.ServiceName }}Repository.{{ .MethodName }}(ctx,{{ if $modelName }}request{{ end }})
if err != nil {
return err
}
return nil
{{ end }}
{{ else }}
{{ if $response }}
response, err := s.{{ $.ServiceName }}Repository.{{ .MethodName }}(ctx)
if err != nil {
return nil, err
}
return response, nil
{{ else }}
err := s.{{ $.ServiceName }}Repository.{{ .MethodName }}(ctx)
if err != nil {
return err
}
return nil
{{ end }}
{{ end }}
}
{{ end }}
`
type Service struct {
PackageName string
ModelPackageName string
ServiceName string
Endpoints []Endpoint
PackageRepo string
}

@ -0,0 +1,94 @@
package model
import (
"github.com/getkin/kin-openapi/openapi3"
"path/filepath"
)
type Model struct {
SavePath string
Files []*File
Tags []string
}
func (r *Model) AddTags(tags ...string) {
r.Tags = append(r.Tags, tags...)
for _, file := range r.Files {
file.AddTagsToFields(tags...)
}
}
func (r *Model) SaveFiles() error {
for _, file := range r.Files {
if err := file.Save(r.SavePath); err != nil {
return err
}
}
return nil
}
func NewModel(savePath string) *Model {
return &Model{
SavePath: savePath,
Files: make([]*File, 0),
}
}
func (r *Model) FillFromOpenapi(openAPI *openapi3.T) error {
for name, schemaRef := range openAPI.Components.Schemas {
file := NewFile(name, filepath.Base(r.SavePath))
file.SetName(name)
file.FillFromSchema(schemaRef.Value)
if err := file.Save(filepath.Join(r.SavePath)); err != nil {
return err
}
}
return nil
}
// создает структуры по ревесту и респонсу который не внутри компонентс
func (r *Model) FillFromOpenapiForLayers(openAPI *openapi3.T) error {
for _, pathItem := range openAPI.Paths.Map() {
for _, operation := range pathItem.Operations() {
if operation.RequestBody != nil {
if err := r.processSchema(operation.OperationID, operation.RequestBody.Value.Content, r.SavePath); err != nil {
return err
}
}
for _, response := range operation.Responses.Map() {
if err := r.processSchema(operation.OperationID, response.Value.Content, r.SavePath); err != nil {
return err
}
}
}
}
return nil
}
func (r *Model) processSchema(name string, content openapi3.Content, savePath string) error {
for _, contentType := range content {
if contentType.Schema != nil {
if contentType.Schema.Ref == "" && contentType.Schema.Value.Items == nil {
file := NewFile(name, filepath.Base(savePath))
file.SetName(name + "Req")
file.parseSchema(contentType.Schema)
if err := file.Save(filepath.Join(savePath)); err != nil {
return err
}
} else if contentType.Schema.Value.Items != nil && contentType.Schema.Value.Items.Ref == "" {
file := NewFile(name, filepath.Base(savePath))
file.SetName(name + "Resp")
file.parseSchema(contentType.Schema.Value.Items)
if err := file.Save(filepath.Join(savePath)); err != nil {
return err
}
}
}
}
return nil
}

@ -0,0 +1,302 @@
package model
import (
"bytes"
"fmt"
"os"
"path/filepath"
"text/template"
"github.com/getkin/kin-openapi/openapi3"
"github.com/rs/zerolog/log"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
)
var FileTemplate = `package {{.PackageName}}
type {{.Name}} struct {
{{- range .Fields}}
/*{{.Title}} - {{.Description}}*/
{{.Name}} {{.Type}} {{.Tag.String -}}
{{end}}
}
{{range .Enums -}}
{{.String}}
{{end -}}
`
type File struct {
PackageName string
Name string
Fields []FileField
Enums []FileEnum
}
type FileField struct {
Name string
Type string
Tag *FileTag
Title string
Description string
}
type FileTag struct {
Name string
Types []string
}
func NewTag(name string, types ...string) *FileTag {
return &FileTag{
Name: name,
Types: types,
}
}
func (r *FileTag) String() string {
var result string
if len(r.Types) == 0 {
return result
}
for iterator, t := range r.Types {
result += fmt.Sprintf(`%v:"%v"`, t, r.Name)
if iterator < len(r.Types)-1 {
result += " "
}
}
return fmt.Sprintf("`%v`", result)
}
func (r *FileTag) AddTypes(types ...string) {
r.Types = append(r.Types, types...)
}
type FileEnum struct {
Name string
Items []any
}
func (r FileEnum) String() string {
// constants
result := fmt.Sprintf("type %v int\n\nconst (\n", r.Name)
for iterator, item := range r.Items {
itemName := r.Name + utils.TextToCamelCase(item.(string))
if iterator == 0 {
result += fmt.Sprintf("\t%v %v = iota\n", itemName, r.Name)
continue
}
result += fmt.Sprintf("\t%v\n", itemName)
}
result += ")\n\n"
// string function
result += fmt.Sprintf("func (r %v) String() string {\n\tswitch r {\n", r.Name)
for _, item := range r.Items {
itemName := r.Name + utils.TextToCamelCase(item.(string))
result += fmt.Sprintf("\tcase %v:\n", itemName)
result += fmt.Sprintf("\t\treturn \"%v\"\n", item)
}
result += "\t}\n"
result += "\treturn \"Unknown\"\n}"
return result
}
func NewFile(name, packageName string) *File {
return &File{
Name: name,
PackageName: packageName,
Fields: make([]FileField, 0),
}
}
func (r *File) SetName(name string) {
r.Name = name
}
func (r *File) AddFields(fields ...FileField) {
r.Fields = append(r.Fields, fields...)
}
func (r *File) AddTagsToFields(tag ...string) {
for i := range r.Fields {
r.Fields[i].Tag.AddTypes(tag...)
}
}
func (r *File) FillFromSchema(schema *openapi3.Schema) {
iterator := 0
for propertyName, property := range schema.Properties {
field := FileField{
Name: utils.TextToCamelCase(propertyName),
Type: openapiTypeToGolangType(property.Value.Type),
Tag: NewTag(propertyName, "json"),
Title: property.Value.Title,
Description: property.Value.Description,
}
if len(property.Value.Enum) > 0 {
field.Type = utils.TextToCamelCase(propertyName)
r.Enums = append(r.Enums, FileEnum{
Name: field.Type,
Items: property.Value.Enum,
})
}
if property.Value.Type == openapi3.TypeObject {
field.Type = utils.TextToCamelCase(propertyName)
if property.Ref != "" {
field.Type = utils.TextGetLastElement(property.Ref, "/")
}
}
if property.Value.Type == openapi3.TypeArray {
field.Type = fmt.Sprintf("[]%v", openapiTypeToGolangType(property.Value.Items.Value.Type))
if property.Value.Items.Value.Type == openapi3.TypeObject {
field.Type = "[]any"
}
if property.Value.Items.Ref != "" {
field.Type = "[]" + utils.TextGetLastElement(property.Value.Items.Ref, "/")
}
}
if utils.IsPrimaryKey(propertyName) {
if iterator != 0 {
r.AddFields(r.Fields[0])
r.Fields[0] = field
iterator++
continue
}
}
r.AddFields(field)
iterator++
}
}
func (r *File) Generate() (string, error) {
t, err := template.New("model_file").Parse(FileTemplate)
if err != nil {
return "", fmt.Errorf("Generate: %v", err)
}
buffer := bytes.NewBuffer(nil)
if err = t.Execute(buffer, r); err != nil {
return "", fmt.Errorf("Generate: %v", err)
}
return buffer.String(), nil
}
func (r *File) Save(savePath string) error {
if savePath == "" {
return fmt.Errorf("Model: path cannot be empty")
}
if _, err := os.Stat(savePath); os.IsNotExist(err) {
if err := os.MkdirAll(savePath, 0755); err != nil {
return fmt.Errorf("Model: cannot create directory: %v", err)
}
}
fileName := fmt.Sprintf("%v.go", utils.TextToLower(r.Name))
filePath := filepath.Join(savePath, fileName)
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("Model.CreateFile: %v", err)
}
defer func() {
if err = file.Close(); err != nil {
log.Err(fmt.Errorf("Model.CloseFile: %v", err)).Str("filepath", filePath).Msg("cannot close file")
}
}()
text, err := r.Generate()
if err != nil {
return fmt.Errorf("Model.%v", err)
}
_, err = file.WriteString(text)
if err != nil {
return fmt.Errorf("Model.WriteToFile: %v", err)
}
absPath, err := filepath.Abs(filePath)
if err != nil {
return fmt.Errorf("Model.Abs: %v", err)
}
log.Info().Str("file", absPath).Msg("model: file generated")
return nil
}
func openapiTypeToGolangType(t string) string {
switch t {
case openapi3.TypeArray:
return "[]"
case openapi3.TypeBoolean:
return "bool"
case openapi3.TypeInteger:
return "int"
case openapi3.TypeObject:
return "any"
case openapi3.TypeString:
return "string"
}
return t
}
func (r *File) parseSchema(schema *openapi3.SchemaRef) {
if schema.Ref != "" {
return
}
for propertyName, property := range schema.Value.Properties {
field := FileField{
Name: utils.TextToCamelCase(propertyName),
Type: openapiTypeToGolangType(property.Value.Type),
Tag: NewTag(propertyName, "json"),
Title: property.Value.Title,
Description: property.Value.Description,
}
if len(property.Value.Enum) > 0 {
field.Type = utils.TextToCamelCase(propertyName)
r.Enums = append(r.Enums, FileEnum{
Name: field.Type,
Items: property.Value.Enum,
})
}
if property.Value.Type == openapi3.TypeObject {
field.Type = utils.TextToCamelCase(propertyName)
if property.Ref != "" {
field.Type = utils.TextGetLastElement(property.Ref, "/")
}
}
if property.Value.Type == openapi3.TypeArray {
field.Type = fmt.Sprintf("[]%v", openapiTypeToGolangType(property.Value.Items.Value.Type))
if property.Value.Items.Value.Type == openapi3.TypeObject {
field.Type = "[]any"
}
if property.Value.Items.Ref != "" {
field.Type = "[]" + utils.TextGetLastElement(property.Value.Items.Ref, "/")
}
}
r.AddFields(field)
}
}

@ -0,0 +1,196 @@
package model
import (
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/suite"
)
const (
testFile = "../openapi_test.yaml"
)
type ModelFileTestSuite struct {
doc *openapi3.T
models []*File
suite.Suite
}
func (r *ModelFileTestSuite) SetupSuite() {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(testFile)
r.NoErrorf(err, "cannot load openapi file: %v")
err = doc.Validate(loader.Context)
r.NoErrorf(err, "cannot validate openapi: %s")
r.doc = doc
}
func (r *ModelFileTestSuite) TearDownSuite() {
}
func TestModelTestSuite(t *testing.T) {
suite.Run(t, new(ModelFileTestSuite))
}
func (r *ModelFileTestSuite) TestFillFromSchema() {
type args struct {
modelName string
schema *openapi3.Schema
}
modelColor := &File{
Name: "Color",
Fields: []FileField{
{
Name: "R",
Type: "int",
Tag: NewTag("r", "json"),
Title: "Red",
},
{
Name: "G",
Type: "int",
Tag: NewTag("g", "json"),
Title: "Green",
},
{
Type: "int",
Tag: NewTag("b", "json"),
Title: "Blue",
Name: "B",
},
{
Name: "A",
Type: "int",
Tag: NewTag("a", "json"),
Title: "Alpha",
},
},
}
modelProduct := &File{
Name: "Product",
Fields: []FileField{
{
Name: "ID",
Type: "string",
Tag: NewTag("_id", "json"),
Title: "ID",
Description: "Auto generated ID",
},
{
Name: "Name",
Type: "string",
Tag: NewTag("name", "json"),
Title: "Product name",
},
{
Name: "Color",
Type: "Color",
Tag: NewTag("color", "json"),
Title: "", // Title for refs cannot parsed
Description: "", // Description for refs cannot parsed
},
{
Name: "Status",
Type: "Status",
Tag: NewTag("status", "json"),
},
{
Name: "Tags",
Type: "[]string",
Tag: NewTag("tags", "json"),
Title: "Tags",
Description: "Array of tags",
},
},
Enums: []FileEnum{
{
Name: "Status",
Items: []any{"in_stock", "in reserve", "out-of-stock"},
},
},
}
modelCategory := &File{
Name: "Category",
Fields: []FileField{
{
Name: "ID",
Type: "string",
Tag: NewTag("_id", "json"),
Title: "ID",
Description: "Auto generated ID",
},
{
Name: "Name",
Type: "string",
Tag: NewTag("name", "json"),
Title: "Category name",
},
{
Name: "Products",
Type: "[]Product",
Tag: NewTag("products", "json"),
Title: "Products",
Description: "Array of included products",
},
},
}
tests := []struct {
name string
got *File
args args
want *File
}{
{
name: "Color",
got: NewFile("Color", "models"),
args: args{
modelName: "Color",
schema: r.doc.Components.Schemas["Color"].Value,
},
want: modelColor,
},
{
name: "Product",
got: NewFile("Product", "models"),
args: args{
modelName: "Product",
schema: r.doc.Components.Schemas["Product"].Value,
},
want: modelProduct,
},
{
name: "Category",
got: NewFile("Category", "models"),
args: args{
modelName: "Category",
schema: r.doc.Components.Schemas["Category"].Value,
},
want: modelCategory,
},
}
for _, tt := range tests {
r.Run(tt.name, func() {
tt.got.SetName(tt.args.modelName)
tt.got.FillFromSchema(tt.args.schema)
r.Equal(tt.want.Name, tt.got.Name)
r.ElementsMatch(tt.want.Fields, tt.got.Fields)
r.models = append(r.models, tt.got)
})
}
}
func (r *ModelFileTestSuite) TestSave() {
for _, model := range r.models {
path := "./model_test"
err := model.Save(path)
r.NoError(err)
}
}

@ -0,0 +1,211 @@
package openapi
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/layers"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/types/dbtypes"
"github.com/getkin/kin-openapi/openapi3"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/db"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/kroki"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/model"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/utils"
)
type OpenAPIModule struct {
settings Settings
db dbtypes.DB
doc *openapi3.T
kroki *kroki.Kroki
layers *layers.Layers
}
type Settings struct {
// FullPath to the OpenAPI file
FullPath string `yaml:"full_path"`
// Path to the folder for save generated models
ModelSavePath string `yaml:"model_save_path"`
// Path to the folder for save generated kroki puml file
KrokiSavePath string `yaml:"kroki_save_path"`
// Database type
DB dbtypes.DBType `yaml:"db"`
// Path to the folder for save generated controllers
ControllerSavePath string `yaml:"controller_save_path"`
ServiceSavePath string `yaml:"service_save_path"`
RepositorySavePath string `yaml:"repository_save_path"`
ServerSavePath string `yaml:"server_save_path"`
}
var (
allowedFilenames = []string{"Openapi.yaml", "openapi.yaml", "Openapi.yml", "openapi.yml",
"Openapi.json", "openapi.json"}
)
func NewOpenAPIModule() *OpenAPIModule {
return &OpenAPIModule{}
}
func (r *OpenAPIModule) Name() string {
return "openapi"
}
func (r *OpenAPIModule) SetSettings(settings any) error {
out, err := yaml.Marshal(settings)
if err != nil {
return err
}
if err = yaml.Unmarshal(out, &r.settings); err != nil {
return err
}
if r.settings.DB != "" {
if !r.settings.DB.Valid() {
return fmt.Errorf("invalid db type")
}
}
switch r.settings.DB {
case dbtypes.DBTypeMongo:
r.db = db.NewMongo(r.settings.DB)
case dbtypes.DBTypeSQL:
r.db = db.NewSQL(r.settings.DB)
default:
r.db = nil
}
if r.settings.KrokiSavePath == "" {
r.settings.KrokiSavePath = "./"
}
if r.settings.ModelSavePath == "" {
r.settings.ModelSavePath = "./models"
}
if r.settings.FullPath == "" {
return nil
}
fileStat, err := os.Stat(r.settings.FullPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("bad path for openapi file")
}
return err
}
if fileStat.Size() == 0 {
return fmt.Errorf("openapi file is empty")
}
return nil
}
func (r *OpenAPIModule) Execute() (*layers.AllData, error) {
fullPath := r.settings.FullPath
var err error
var fileStat fs.FileInfo
if fullPath == "" {
for _, filename := range allowedFilenames {
fullPath = filename
fileStat, err = os.Stat(fullPath)
if os.IsNotExist(err) {
continue
}
err = nil
break
}
}
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("openapi file not found")
}
return nil, fmt.Errorf("cannot read openapi file: %w", err)
}
if fileStat.Size() == 0 {
return nil, fmt.Errorf("openapi file is empty")
}
fullPath, err = filepath.Abs(fullPath)
if err != nil {
return nil, fmt.Errorf("cannot get absolute path: %v", err)
}
if err = r.setDocument(fullPath); err != nil {
return nil, err
}
modelModule := model.NewModel(r.settings.ModelSavePath)
err = modelModule.FillFromOpenapi(r.doc)
if err != nil {
return nil, err
}
layerDeps := layers.Deps{
SaveModelPath: r.settings.ModelSavePath,
PackageName: filepath.Base(r.settings.ModelSavePath),
SavePathController: r.settings.ControllerSavePath,
SavePathService: r.settings.ServiceSavePath,
SavePathRepository: r.settings.RepositorySavePath,
SavePathServer: r.settings.ServerSavePath,
DB: r.db,
}
layersModule := layers.NewLayers(layerDeps)
layersModule.FillFromOpenapi(r.doc)
layerData := layersModule.GetData()
if r.db != nil {
modelModule.AddTags(r.db.Tag())
}
if err = modelModule.SaveFiles(); err != nil {
return nil, fmt.Errorf("saveGoFiles: %v", err)
}
krokiModule := kroki.NewKroki()
krokiModule.FillFromOpenapi(r.doc)
if err = krokiModule.SavePUMLFile(r.settings.KrokiSavePath); err != nil {
return nil, fmt.Errorf("saveUmlFile: %v", err)
}
r.kroki = krokiModule
return &layerData, nil
}
func (r *OpenAPIModule) KrokiPUML() string {
result, err := r.kroki.Generate()
if err != nil {
return utils.TemplateError(fmt.Sprintf("openapi.KrokiUML got %v", err.Error()))
}
return result
}
func (r *OpenAPIModule) setDocument(fullPath string) error {
log.Info().Str("file", fullPath).Msg("Reading openapi file")
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(fullPath)
if err != nil {
return fmt.Errorf("loadFromFile: %v", err)
}
if err = doc.Validate(loader.Context); err != nil {
return fmt.Errorf("validate: %v", err)
}
r.doc = doc
return nil
}

@ -0,0 +1,218 @@
openapi: 3.0.3
info:
title: Example test
version: ver.1.0
servers:
- url: http://localhost:8225
tags:
- name: Product
description: Запросы к продукту
- name: Category
description: Запросы к категориям
paths:
/product:
post:
operationId: createProduct
tags:
- Product
summary: Создать продукт
description: Создать продукт
requestBody:
description: Создаваемый продукт
content:
application/json:
schema:
$ref: "#/components/schemas/Product"
responses:
200:
description: Success
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Product"
/category:
post:
operationId: createCategory
tags:
- Category
summary: Создать категорию
description: Создать категорию
requestBody:
description: Создаваемая категория
content:
application/json:
schema:
$ref: "#/components/schemas/Category"
responses:
200:
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/Category"
get:
operationId: GetListCategories
tags:
- Category
summary: Получить список категорий
description: Получить список всех категорий
requestBody:
description: Создаваемая категория
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Color"
responses:
200:
description: Success
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Color"
500:
description: PLOHO
/category/{categoryId}:
get:
operationId: getCategory
tags:
- Category
summary: Получить информацию о категории
description: Получить информацию о категории по идентификатору
parameters:
- in: path
name: categoryId
description: Идентификатор категории
required: true
schema:
type: string
responses:
200:
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/Category"
404:
description: Category not found
500:
description: Internal Server Error
components:
schemas:
Product:
type: object
properties:
_id:
title: ID
description: Auto generated ID
type: string
example: 65046106121361691e6861de
readOnly: true
name:
title: Product name
type: string
example: Black Knight
minLength: 3
maxLength: 50
color:
title: Product color
description: Main color for product in rgba
$ref: "#/components/schemas/Color"
status:
title: Product status
type: string
format: enum
enum:
- in_stock
- in reserve
- out-of-stock
tags:
title: Tags
description: Array of tags
type: array
items:
type: string
example: in_black
maxItems: 10
uniqueItems: true
Color:
type: object
properties:
r:
title: Red
type: integer
example: 0
minimum: 0
maximum: 255
g:
title: Green
type: integer
example: 0
minimum: 0
maximum: 255
b:
title: Blue
type: integer
example: 0
minimum: 0
maximum: 255
a:
title: Alpha
type: integer
example: 0
minimum: 0
maximum: 255
Category:
type: object
properties:
_id:
title: ID
description: Auto generated ID
type: string
example: 65046106121361691e6861de
readOnly: true
name:
title: Category name
type: string
example: InBlack
minLength: 3
maxLength: 30
products:
title: Products
description: Array of included products
type: array
items:
$ref: "#/components/schemas/Product"
minItems: 1
CategoryReq:
type: object
properties:
id:
title: ID
description: Auto generated ID
type: string
example: 65046106121361691e6861de
readOnly: true
name:
title: Category name
type: array
items:
type: uint64
example: 1
products:
title: Products
description: Array of included products
type: array
items:
$ref: "#/components/schemas/Product"

@ -5,8 +5,8 @@ import (
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint/modules/openapi/layers"
"strings"
"text/template"
@ -26,6 +26,7 @@ type TemplateSettings struct {
TemplatePath string `yaml:"path"`
Vars map[string]any `yaml:"vars,omitempty"`
Modules map[string]BlueprintModule `yaml:"-"`
LayersData *layers.AllData
}
type Template struct {
@ -67,12 +68,16 @@ func (r *Template) Execute() error {
}
}
return r.execCommands()
return nil
}
func (r *Template) templateFile(templatePath string) error {
// Create output path - replace template path to exec path
sp := strings.Split(templatePath, "template")
if len(sp) < 2 {
log.Info().Str("file", templatePath).Msg("file not in template folder")
return nil
}
outputFile := r.settings.ExecPath + sp[1]
@ -121,26 +126,6 @@ func (r *Template) templateFile(templatePath string) error {
return nil
}
func (r *Template) execCommands() error {
cmd := exec.Command("go", "mod", "init")
log.Info().Msg("execCommands: go mod init")
if err := cmd.Run(); err != nil {
return fmt.Errorf("execCommands: go mod init. %v", err)
}
cmd = exec.Command("go", "mod", "tidy")
log.Info().Msg("execCommands: go mod tidy")
if err := cmd.Run(); err != nil {
return fmt.Errorf("execCommands: go mod tidy. %v", err)
}
return nil
}
func (r *Template) walkFunc(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("walkFunc: %v", err)

@ -7,7 +7,9 @@ import (
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"penahub.gitlab.yandexcloud.net/pena-services/blueprint/blueprint"
)
@ -34,6 +36,7 @@ func NewApp() *App {
// Execute - Любой вызов приложения.
func (r *App) Execute() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.DateTime})
versionFlag := flag.Bool("v", false, "Show current version")
projectNameFlag := flag.String("n", "", "Set project name")
gitFlag := flag.String("g", "", "Get blueprint from git")

@ -12,3 +12,11 @@ Modules:
- name: APP_NAME
type: string
default: "{{.ProjectName}}"
openapi:
model_save_path: ./internal/models
controller_save_path: ./internal/controllers
service_save_path: ./internal/service
repository_save_path: ./internal/repository
server_save_path: ./internal/server/http
db: mongodb
# db: postgres

@ -158,4 +158,4 @@ fabric.properties
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go

@ -0,0 +1,5 @@
# Example
```plantuml
{{.Modules.openapi.KrokiPUML}}
```

@ -7,20 +7,20 @@ import (
"{{.Vars.ProjectName}}/internal/app"
{{.Modules.logger.Import}}
{{.Modules.logger.ImportCore}}
"{{.Modules.logger.Import}}"
"{{.Modules.logger.ImportCore}}"
{{.Modules.env.Import}}
"{{.Modules.env.Import}}"
)
func main() {
{{.Modules.logger.Init}}
{{.Modules.logger.Declaration "logger"}}
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
var config app.Config
{{.Modules.env.Declaration "config"}}
if err := app.Run(ctx, config, logger); err != nil {

@ -3,17 +3,65 @@ package app
import (
"context"
{{.Modules.logger.Import}}
"{{.Modules.logger.Import}}"
)
{{.Modules.env.Struct}}
func Run(ctx context.Context, config Config, logger {{.Modules.logger.Type}}) error {
defer func() {
if r := recover(); r != nil {
logger.Error("Recovered from a panic", zap.Any("error", r))
}
}()
{{.Modules.logger.Message "info" "App started" "config" "config"}}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Инициализация репозиториев
{{range $key, $value := .LayersData.Repositories}}
{{$key}}Repository := {{$value.PackageName}}.New{{$value.Name}}Repository()
{{end}}
// Инициализация сервисов
{{range $key, $value := .LayersData.Services}}
{{$key}}Service := {{$value.PackageName}}.New{{$value.Name}}Service({{$key}}Repository)
{{end}}
// Инициализация контроллеров
{{range $key, $value := .LayersData.Controllers}}
{{$key}}Controller := {{$value.PackageName}}.New{{$value.Name}}Controller({{$key}}Service)
{{end}}
// Создание сервера
server := {{.LayersData.ServerData}}.NewServer({{.LayersData.ServerData}}.ServerConfig{
Controllers: []{{.LayersData.ServerData}}.Controller{
{{range $key, $value := .LayersData.Controllers}}
{{$key}}Controller,
{{end}}
},
})
go func() {
err := server.Start("Host + : + Port")
if err != nil {
logger.Error("Server startup error", zap.Error(err))
cancel()
}
}()
// Вывод маршрутов
server.ListRoutes()
<-ctx.Done()
logger.Info("App shutting down gracefully")
//TODO
// Остановка сервера
return nil
}

17
go.mod

@ -3,13 +3,24 @@ module penahub.gitlab.yandexcloud.net/pena-services/blueprint
go 1.21.0
require (
github.com/rs/zerolog v1.31.0
github.com/getkin/kin-openapi v0.122.0 // обязательно этой версии должен быть опенапи если новее то сломается все...
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.17.0 // indirect
)

45
go.sum

@ -1,25 +1,60 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10=
github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw=
github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4=
github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

22
types/dbtypes/dbtypes.go Normal file

@ -0,0 +1,22 @@
package dbtypes
type DBType string
const (
DBTypeSQL DBType = "postgres"
DBTypeMongo DBType = "mongodb"
)
func (r DBType) Valid() bool {
return r == DBTypeMongo || r == DBTypeSQL
}
const DirSqlcPath = "internal/sqlc/db_query"
const PathToQueriesFile = "internal/sqlc/db_query/queries.sql"
type DB interface {
Tag() string
GenerateSettings() error
SetTemplate(methodName string) string
DBType() DBType
}

57
types/dbtypes/template.go Normal file

@ -0,0 +1,57 @@
package dbtypes
const SqlcYamlTemplate = `version: "1"
packages:
- name: "sqlcgen"
path: ".internal/sqlc/sqlcgen"
queries: ".internal/sqlc/db_query/queries.sql"
schema:
- "todo"
engine: "postgresql"
emit_json_tags: true
emit_db_tags: true
emit_prepared_queries: false
emit_interface: false
emit_exact_table_names: false
emit_empty_slices: false
sql_package: "database/sql"
`
const PaginationMongoTemplate = `
filter := bson.M{}
total, err := r.mdb.CountDocuments(ctx, filter)
if err != nil {
return nil, err
}
cursor, err := r.mdb.Find(ctx, filter, options.Find().SetSkip(int64(request.Page * request.Limit)).SetLimit(int64(request.Limit)))
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var records []bson.M
if err := cursor.All(ctx, &records); err != nil {
return nil, err
}
`
const PaginationSqlRepoTemplate = `
rows, err := r.queries.%s(ctx, sqlcgen.%sParams{
Page: request.Page,
Limit: request.Size,
})
if err != nil {
return nil, err
}
`
// $1-PAGE, $2-LIMIT
const PaginationSqlTemplate = `-- name: %s :many
SELECT *
FROM {{ .TableName }} AS a
ORDER BY a.created_at DESC
LIMIT $2 OFFSET ($1-1)*$2;
`

7
utils/template.go Normal file

@ -0,0 +1,7 @@
package utils
import "fmt"
func TemplateError(err string) string {
return fmt.Sprintf("//FIXME TEMPLATE ERROR: %v", err)
}

98
utils/text.go Normal file

@ -0,0 +1,98 @@
package utils
import (
"slices"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func TextToLower(text string) string {
caser := cases.Lower(language.English)
return caser.String(text)
}
func TextToUpper(text string) string {
caser := cases.Upper(language.English)
return caser.String(text)
}
func TextToTitle(text string) string {
caser := cases.Title(language.English)
return caser.String(text)
}
func TextToCamelCase(text string) string {
var result string
text = strings.ReplaceAll(text, " ", "_")
text = strings.ReplaceAll(text, "-", "_")
for _, item := range strings.Split(text, "_") {
if item == "" {
continue
}
result += textToUpperWitnInitialisms(item)
}
return result
}
func TextGetLastElement(text, sep string) string {
splitRef := strings.Split(text, sep)
return splitRef[len(splitRef)-1]
}
func textToUpperWitnInitialisms(text string) string {
initialismKey, ok := slices.BinarySearch(commonInitialisms, TextToUpper(text))
if ok {
text = commonInitialisms[initialismKey]
} else {
text = TextToTitle(text)
}
return text
}
var commonInitialisms = []string{
"ACL",
"API",
"ASCII",
"CPU",
"CSS",
"DNS",
"EOF",
"GUID",
"HTML",
"HTTP",
"HTTPS",
"ID",
"IP",
"JSON",
"LHS",
"QPS",
"RAM",
"RHS",
"RPC",
"SLA",
"SMTP",
"SQL",
"SSH",
"TCP",
"TLS",
"TTL",
"UDP",
"UI",
"UID",
"UUID",
"URI",
"URL",
"UTF8",
"VM",
"XML",
"XMPP",
"XSRF",
"XSS",
}

68
utils/text_test.go Normal file

@ -0,0 +1,68 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTextToLower(t *testing.T) {
text := "Some Text"
want := "some text"
assert.Equal(t, want, TextToLower(text))
}
func TestTextToUpper(t *testing.T) {
text := "some text"
want := "SOME TEXT"
assert.Equal(t, want, TextToUpper(text))
}
func TestTextToTitle(t *testing.T) {
text := "some text"
want := "Some Text"
assert.Equal(t, want, TextToTitle(text))
}
func TestTextFromSnakeCaseToCamelCase(t *testing.T) {
type args struct {
text string
}
tests := []struct {
name string
args args
want string
}{
{
name: "_id",
args: args{
text: "_id",
},
want: "ID",
},
{
name: "product_id",
args: args{
text: "product_id",
},
want: "ProductID",
},
{
name: "product",
args: args{
text: "product",
},
want: "Product",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, TextToCamelCase(tt.args.text))
})
}
}

5
utils/vars.go Normal file

@ -0,0 +1,5 @@
package utils
func IsPrimaryKey(s string) bool {
return s == "id" || s == "_id" || s == "uuid" || s == "guid"
}