Compare commits
80 Commits
main
...
pagination
Author | SHA1 | Date | |
---|---|---|---|
![]() |
123e0daa7d | ||
![]() |
b4575f53e8 | ||
![]() |
525994f956 | ||
![]() |
c0e91951c2 | ||
![]() |
40df103023 | ||
![]() |
ae8e0898cc | ||
![]() |
0567abe346 | ||
![]() |
f012a6b031 | ||
![]() |
aead19ae97 | ||
![]() |
9c66efbada | ||
![]() |
f686525eac | ||
![]() |
c34dc73294 | ||
![]() |
a35df3ecc6 | ||
![]() |
7164b56099 | ||
![]() |
86e7319452 | ||
![]() |
7f0edaa9f6 | ||
![]() |
7a46b40c3f | ||
![]() |
47d8ed5e43 | ||
![]() |
fe5c898aeb | ||
![]() |
0da3c8004e | ||
![]() |
1e2c045bcd | ||
![]() |
abd73327ad | ||
![]() |
08e5db900c | ||
![]() |
524cc86ea9 | ||
![]() |
3ea9617ab4 | ||
![]() |
90dff5e262 | ||
![]() |
46b3111e53 | ||
![]() |
100696f35c | ||
![]() |
45bf4f6bb5 | ||
![]() |
ca3f7ef292 | ||
![]() |
5386195bda | ||
![]() |
5ec42b681a | ||
9226ec1e5e | |||
cef7a97724 | |||
bb4adf590b | |||
c0b1412fdf | |||
3eaefe7d82 | |||
41daab9a94 | |||
0dce9424e5 | |||
11f34fa4b3 | |||
5de742ed05 | |||
638c427939 | |||
c3a0b6df56 | |||
ecd2fa15f8 | |||
2fee8935be | |||
38dfd11f85 | |||
d96eff32db | |||
82a891787a | |||
ff60de838b | |||
84a7f64231 | |||
a66b186b05 | |||
220dd43a4f | |||
79695ceac0 | |||
47cfd6c008 | |||
c49ffb1d51 | |||
c08258f612 | |||
6d93f92fe2 | |||
aa5df3fadc | |||
ee7fa89369 | |||
24e798578e | |||
3d4901e746 | |||
da46f5a267 | |||
039ed007e4 | |||
7ceae9b8dc | |||
e19edb346b | |||
ae4e40fc6b | |||
be0cd54df8 | |||
a9d511e5c6 | |||
70afb2781c | |||
1a73a25af5 | |||
4e609e9126 | |||
47b62b5e07 | |||
8f9030f943 | |||
91264c021a | |||
17bc98db5e | |||
c35fb1b761 | |||
![]() |
663ac8a298 | ||
![]() |
e64e32874f | ||
![]() |
293b0b783a | ||
![]() |
fee55ca007 |
83
README.md
83
README.md
@ -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
|
||||
}
|
||||
|
27
blueprint/modules/env/env.go
vendored
27
blueprint/modules/env/env.go
vendored
@ -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 {
|
||||
|
27
blueprint/modules/openapi/db/mongo.go
Normal file
27
blueprint/modules/openapi/db/mongo.go
Normal file
@ -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
|
||||
}
|
51
blueprint/modules/openapi/db/sql.go
Normal file
51
blueprint/modules/openapi/db/sql.go
Normal file
@ -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
|
||||
}
|
177
blueprint/modules/openapi/kroki/kroki.go
Normal file
177
blueprint/modules/openapi/kroki/kroki.go
Normal file
@ -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
|
||||
}
|
128
blueprint/modules/openapi/kroki/kroki_table.go
Normal file
128
blueprint/modules/openapi/kroki/kroki_table.go
Normal file
@ -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
|
||||
}
|
188
blueprint/modules/openapi/kroki/kroki_test.go
Normal file
188
blueprint/modules/openapi/kroki/kroki_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
115
blueprint/modules/openapi/layers/controller_utils.go
Normal file
115
blueprint/modules/openapi/layers/controller_utils.go
Normal file
@ -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
|
||||
}
|
323
blueprint/modules/openapi/layers/generate_tools.go
Normal file
323
blueprint/modules/openapi/layers/generate_tools.go
Normal file
@ -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 ""
|
||||
}
|
335
blueprint/modules/openapi/layers/layers.go
Normal file
335
blueprint/modules/openapi/layers/layers.go
Normal file
@ -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
|
||||
}
|
60
blueprint/modules/openapi/layers/layers_test.go
Normal file
60
blueprint/modules/openapi/layers/layers_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
61
blueprint/modules/openapi/layers/repository_utils.go
Normal file
61
blueprint/modules/openapi/layers/repository_utils.go
Normal file
@ -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
|
||||
}
|
69
blueprint/modules/openapi/layers/server_utils.go
Normal file
69
blueprint/modules/openapi/layers/server_utils.go
Normal file
@ -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
|
||||
}
|
65
blueprint/modules/openapi/layers/service_utils.go
Normal file
65
blueprint/modules/openapi/layers/service_utils.go
Normal file
@ -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
|
||||
}
|
94
blueprint/modules/openapi/model/model.go
Normal file
94
blueprint/modules/openapi/model/model.go
Normal file
@ -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
|
||||
}
|
302
blueprint/modules/openapi/model/model_file.go
Normal file
302
blueprint/modules/openapi/model/model_file.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
196
blueprint/modules/openapi/model/model_file_test.go
Normal file
196
blueprint/modules/openapi/model/model_file_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
211
blueprint/modules/openapi/openapi.go
Normal file
211
blueprint/modules/openapi/openapi.go
Normal file
@ -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
|
||||
}
|
218
blueprint/modules/openapi/openapi_test.yaml
Normal file
218
blueprint/modules/openapi/openapi_test.yaml
Normal file
@ -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
|
||||
|
5
examples/default/template/README.md.tmpl
Normal file
5
examples/default/template/README.md.tmpl
Normal file
@ -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
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
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
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
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
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
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
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
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"
|
||||
}
|
Loading…
Reference in New Issue
Block a user