Compare commits
No commits in common. "experement" and "master" have entirely different histories.
experement
...
master
1
.example.env
Normal file
1
.example.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
DB_PASSWORD=password
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
.idea
|
.idea
|
||||||
.storage
|
|
||||||
.env
|
.env
|
||||||
2
.storage/.gitignore
vendored
Normal file
2
.storage/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
31
README.md
Normal file
31
README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# pink_fox
|
||||||
|
|
||||||
|
## Развертывание
|
||||||
|
|
||||||
|
Скопировать `cp environment/example.config.yml environment/config.yml` и поправить пароль.
|
||||||
|
|
||||||
|
Скопировать `cp .example.env .env` и поправить пароль.
|
||||||
|
|
||||||
|
## Библиотеки
|
||||||
|
|
||||||
|
- [Веб framework](https://github.com/gin-gonic/gin)
|
||||||
|
- [Framework консольных приложений](https://github.com/spf13/cobra)
|
||||||
|
- [Шаблонизатор](https://github.com/CloudyKit/jet)
|
||||||
|
|
||||||
|
## Планы
|
||||||
|
|
||||||
|
- модели
|
||||||
|
- кастомизация страницы html ошибок
|
||||||
|
- добавить api
|
||||||
|
- реализовать миграции
|
||||||
|
- горячая перезагрузка конфига
|
||||||
|
- валидация
|
||||||
|
- добавить образцы middleware
|
||||||
|
- модуль авторизации
|
||||||
|
- загрузку assets, таких как js, css, jpg, png на страницы html
|
||||||
|
- /robot.txt
|
||||||
|
- настройка timeout через конфиг
|
||||||
|
- транзакции
|
||||||
|
- timeout для базы данных
|
||||||
|
- запуск задач по cron
|
||||||
|
- заполнение базы данных тестовыми значениями консольной командой
|
||||||
@ -1,15 +0,0 @@
|
|||||||
[Best Practices of Building Web Apps with Gin & Golang](https://www.squash.io/optimizing-gin-in-golang-project-structuring-error-handling-and-testing/)
|
|
||||||
|
|
||||||
Папки
|
|
||||||
|
|
||||||
- cmd
|
|
||||||
- internal
|
|
||||||
- pkg
|
|
||||||
|
|
||||||
# План
|
|
||||||
|
|
||||||
- создать консольную команду для создания пользователя
|
|
||||||
- она должна создать нового пользователя, так же к этой команде нужно добавить di container
|
|
||||||
- нужно сделать отдельную команду `exec` которая и будет запускать пользовательские команды с di контейнером, но есть вопросы
|
|
||||||
|
|
||||||
Пока делаем endpoint для этого
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
host: postgres_test
|
|
||||||
user: pink_fox
|
|
||||||
password: pink_fox_pass
|
|
||||||
port: 5432
|
|
||||||
database: pink_fox_db_test
|
|
||||||
migrations: /app/database/migrations
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
debug: true
|
|
||||||
|
|
||||||
db:
|
|
||||||
host: postgres
|
|
||||||
user: pink_fox
|
|
||||||
password: pink_fox_pass
|
|
||||||
port: 5432
|
|
||||||
database: pink_fox_db
|
|
||||||
migrations: /app/database/migrations
|
|
||||||
|
|
||||||
# Путь к файлу лога ошибок
|
|
||||||
#logFile: /app/logs/errors.log
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
DROP TABLE IF EXISTS users;
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
email VARCHAR(128) NOT NULL,
|
|
||||||
password VARCHAR(128) NOT NULL,
|
|
||||||
token VARCHAR(32) NOT NULL,
|
|
||||||
email_confirmed BOOLEAN NOT NULL,
|
|
||||||
created_at timestamptz NOT NULL,
|
|
||||||
updated_at timestamptz NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX idx_users_email ON users (email);
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
DROP TABLE IF EXISTS users;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
TRUNCATE TABLE users RESTART IDENTITY CASCADE;
|
|
||||||
|
|
||||||
INSERT INTO users (email, password, token, email_confirmed, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
('admin@admin.ru', 'password', 'token', true, '2000-10-15 10:00:00 +04:00', '2000-10-15 10:00:00 +04:00');
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"pink_fox/inner/repositories"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CreateUserCommand struct {
|
|
||||||
usersRepository repositories.UserRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO нужно ли мне создавать access, я думаю пока нет, но вообще надо
|
|
||||||
|
|
||||||
func NewCreateUserCommand(usersRepository repositories.UserRepository) *CreateUserCommand {
|
|
||||||
return &CreateUserCommand{
|
|
||||||
usersRepository: usersRepository,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *CreateUserCommand) Exec(email, password string, confirmEmail bool) fw.Error {
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import "pink_fox/packages/fw"
|
|
||||||
|
|
||||||
type UserCommand struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserCommand() *UserCommand {
|
|
||||||
return &UserCommand{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *UserCommand) CheckAndFind(id int) (func(int) ([]string, fw.Error), fw.Error) {
|
|
||||||
// тут мы получаем пользователя или ошибку
|
|
||||||
// получает сущность по id
|
|
||||||
return it.getUsersList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *UserCommand) getUsersList(page int) ([]string, fw.Error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"os"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Debug bool `yaml:"debug"`
|
|
||||||
LogFile string `yaml:"logFile"`
|
|
||||||
DB DB `yaml:"db"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DB struct {
|
|
||||||
Host string `yaml:"host"`
|
|
||||||
User string `yaml:"user"`
|
|
||||||
Password string `yaml:"password"`
|
|
||||||
Port string `yaml:"port"`
|
|
||||||
Database string `yaml:"database"`
|
|
||||||
Migrations string `yaml:"migrations"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, fw.Error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fw.Err(err)
|
|
||||||
}
|
|
||||||
var conf Config
|
|
||||||
err = yaml.Unmarshal(data, &conf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fw.Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return setDefaultValues(&conf), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadTestConfig(path string) (*DB, fw.Error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fw.Err(err)
|
|
||||||
}
|
|
||||||
var db DB
|
|
||||||
err = yaml.Unmarshal(data, &db)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fw.Err(err)
|
|
||||||
}
|
|
||||||
return &db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDefaultValues(conf *Config) *Config {
|
|
||||||
if conf.LogFile == "" {
|
|
||||||
conf.LogFile = "/app/logs/errors.log"
|
|
||||||
}
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDatabaseConfig(path string) (*fw.DatabaseConfig, fw.Error) {
|
|
||||||
conf, err := LoadConfig(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err.Tap()
|
|
||||||
}
|
|
||||||
return &fw.DatabaseConfig{
|
|
||||||
Host: conf.DB.Host,
|
|
||||||
User: conf.DB.User,
|
|
||||||
Password: conf.DB.Password,
|
|
||||||
Port: conf.DB.Port,
|
|
||||||
Database: conf.DB.Database,
|
|
||||||
Migrations: conf.DB.Migrations,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import "pink_fox/packages/fw"
|
|
||||||
|
|
||||||
type HttpErrorController struct {
|
|
||||||
app HttpErrorControllerServices
|
|
||||||
}
|
|
||||||
|
|
||||||
type HttpErrorControllerServices interface {
|
|
||||||
fw.BaseServices
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHttpErrorController(services HttpErrorControllerServices) *HttpErrorController {
|
|
||||||
return &HttpErrorController{
|
|
||||||
app: services,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *HttpErrorController) Page404() (fw.Response, fw.Error) {
|
|
||||||
return it.app.ResponseFactory().HtmlError(404, "general"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *HttpErrorController) Api404() (fw.Response, fw.Error) {
|
|
||||||
return it.app.ResponseFactory().HtmlError(404, "api"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *HttpErrorController) Back404() (fw.Response, fw.Error) {
|
|
||||||
return it.app.ResponseFactory().HtmlError(404, "back"), nil
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import "pink_fox/packages/fw"
|
|
||||||
|
|
||||||
type PingController struct {
|
|
||||||
services PingControllerService
|
|
||||||
}
|
|
||||||
|
|
||||||
type PingControllerService interface {
|
|
||||||
ResponseFactory() *fw.ResponseFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPingController(services PingControllerService) *PingController {
|
|
||||||
return &PingController{
|
|
||||||
services: services,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *PingController) Index() (fw.Response, fw.Error) {
|
|
||||||
return it.services.ResponseFactory().String("ok"), nil
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"pink_fox/inner/commands"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SiteController struct {
|
|
||||||
services SiteControllerService
|
|
||||||
}
|
|
||||||
|
|
||||||
type SiteControllerService interface {
|
|
||||||
fw.BaseServices
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSiteController(services SiteControllerService) *SiteController {
|
|
||||||
return &SiteController{
|
|
||||||
services: services,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *SiteController) Index() (fw.Response, fw.Error) {
|
|
||||||
//return it.services.ResponseFactory().HtmlError(404, "test"), nil
|
|
||||||
//return nil, fw.ErrStr("site controller not yet implemented")
|
|
||||||
|
|
||||||
return it.services.ResponseFactory().View("index.html", map[string]any{
|
|
||||||
"text": "Hello world",
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestActionServices interface {
|
|
||||||
UserCommand() *commands.UserCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *SiteController) Test(services TestActionServices) (fw.Response, fw.Error) {
|
|
||||||
getUserList, err := services.UserCommand().CheckAndFind(100)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err.Tap()
|
|
||||||
}
|
|
||||||
|
|
||||||
list, err := getUserList(1000)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err.Tap()
|
|
||||||
}
|
|
||||||
|
|
||||||
return it.services.ResponseFactory().String(strings.Join(list, ", ")), nil
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
package di
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"pink_fox/inner/storage"
|
|
||||||
"pink_fox/inner/view"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Container struct {
|
|
||||||
storage *storage.Storage
|
|
||||||
writer http.ResponseWriter
|
|
||||||
request *http.Request
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeContainer(storage *storage.Storage, writer http.ResponseWriter, request *http.Request) *Container {
|
|
||||||
return &Container{
|
|
||||||
storage: storage,
|
|
||||||
writer: writer,
|
|
||||||
request: request,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CloseContainer(_ *Container) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Container) Debug() bool {
|
|
||||||
return it.storage.Debug
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Container) Logger() fw.Logger {
|
|
||||||
return nil // FIXME
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Container) ResponseFactory() *fw.ResponseFactory {
|
|
||||||
return fw.NewResponseFactory(it.writer, it.storage.Template, func() any {
|
|
||||||
return view.NewView()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Container) GetWriter() http.ResponseWriter {
|
|
||||||
return it.writer
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
"runtime/debug"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ErrorMiddlewareServices interface {
|
|
||||||
Debug() bool
|
|
||||||
Logger() fw.Logger
|
|
||||||
ResponseFactory() *fw.ResponseFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorMiddleware(services ErrorMiddlewareServices, next fw.ActionFunc) (response fw.Response, err fw.Error) {
|
|
||||||
defer func() {
|
|
||||||
if err_ := recover(); err_ != nil {
|
|
||||||
err = fw.ErrPanic(fmt.Errorf("panic: %v", err_), debug.Stack())
|
|
||||||
err = errorsHandler(services.ResponseFactory(), err, services.Debug(), services.Logger())
|
|
||||||
response = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
response, err = next()
|
|
||||||
if err != nil {
|
|
||||||
err = errorsHandler(services.ResponseFactory(), err, services.Debug(), services.Logger())
|
|
||||||
return nil, err // ошибку отобразили
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorsHandler(responseFactory *fw.ResponseFactory, errIn fw.Error, debugOn bool, logger fw.Logger) fw.Error {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Error(errIn.Message(), errIn.Trace())
|
|
||||||
}
|
|
||||||
|
|
||||||
textError := ""
|
|
||||||
if debugOn {
|
|
||||||
textError = errIn.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
response := responseFactory.View("errors/500.html", map[string]any{
|
|
||||||
"title": fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)),
|
|
||||||
"message": "We have a problem",
|
|
||||||
"error": textError,
|
|
||||||
})
|
|
||||||
response.WriteHeader(http.StatusInternalServerError)
|
|
||||||
err := response.Render()
|
|
||||||
if err != nil {
|
|
||||||
return err.Add("ошибка при создании страницы ошибки")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package middlewares
|
|
||||||
|
|
||||||
import "pink_fox/packages/fw"
|
|
||||||
|
|
||||||
func ResponseMiddleware(next fw.ActionFunc) (fw.Response, fw.Error) {
|
|
||||||
response, err := next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// нет ошибок, пробуем отобразить результат запроса
|
|
||||||
err = response.Render()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err.Tap()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
package inner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"pink_fox/inner/controllers"
|
|
||||||
"pink_fox/inner/di"
|
|
||||||
"pink_fox/inner/middlewares"
|
|
||||||
"pink_fox/inner/storage"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegistrationRoutes(rm *fw.RouterManager[*storage.Storage, *di.Container]) {
|
|
||||||
rm.Use(errorMiddleware, responseMiddleware)
|
|
||||||
rm.NotFound(noRoute2)
|
|
||||||
|
|
||||||
rm.Get("/ping", ping)
|
|
||||||
rm.Get("/", siteIndex)
|
|
||||||
|
|
||||||
api := rm.Group("/api")
|
|
||||||
{
|
|
||||||
api.NotFound(noRouteApi)
|
|
||||||
api.Get("/ping", ping)
|
|
||||||
|
|
||||||
sub := api.Group("/sub")
|
|
||||||
{
|
|
||||||
sub.NotFound(noRouteBack)
|
|
||||||
sub.Get("/ping", ping)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func siteIndex(di *di.Container) (fw.Response, fw.Error) {
|
|
||||||
return controllers.NewSiteController(di).Index()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ping(di *di.Container) (fw.Response, fw.Error) {
|
|
||||||
return controllers.NewPingController(di).Index()
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorMiddleware(di *di.Container, next fw.ActionFunc) (fw.Response, fw.Error) {
|
|
||||||
return middlewares.ErrorMiddleware(di, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
func responseMiddleware(_ *di.Container, next fw.ActionFunc) (fw.Response, fw.Error) {
|
|
||||||
return middlewares.ResponseMiddleware(next)
|
|
||||||
}
|
|
||||||
|
|
||||||
func noRoute2(di *di.Container) (fw.Response, fw.Error) {
|
|
||||||
return controllers.NewHttpErrorController(di).Page404()
|
|
||||||
}
|
|
||||||
|
|
||||||
func noRouteApi(di *di.Container) (fw.Response, fw.Error) {
|
|
||||||
return controllers.NewHttpErrorController(di).Api404()
|
|
||||||
}
|
|
||||||
|
|
||||||
func noRouteBack(di *di.Container) (fw.Response, fw.Error) {
|
|
||||||
return controllers.NewHttpErrorController(di).Back404()
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package pg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"pink_fox/inner/repositories"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UsersRepository struct {
|
|
||||||
ctx context.Context
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUsersRepository(context context.Context, db *sql.DB) *UsersRepository {
|
|
||||||
return &UsersRepository{
|
|
||||||
ctx: context,
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME делаем запись в базу данных
|
|
||||||
// FIXME делаю тест этой функции
|
|
||||||
|
|
||||||
func (it *UsersRepository) CreateNewUser(email, password string, emailConfirm bool) (id int64, err fw.Error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *UsersRepository) GetByID(id int64) (user *repositories.User, ok bool, err fw.Error) {
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package pg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"pink_fox/inner/test"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetByID(t *testing.T) {
|
|
||||||
tool := test.NewDBTest().Seed("users.sql")
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
_ = NewUsersRepository(ctx, tool.GetDB())
|
|
||||||
// FIXME
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
ID int64
|
|
||||||
Email string
|
|
||||||
Password string
|
|
||||||
Token string
|
|
||||||
EmailConfirmed bool
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserRepository interface {
|
|
||||||
CreateNewUser(email, password string, emailConfirm bool) (id int64, err fw.Error)
|
|
||||||
GetByID(id int64) (user *User, ok bool, err fw.Error)
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"pink_fox/inner/config"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Storage struct {
|
|
||||||
Config *config.Config
|
|
||||||
Port int
|
|
||||||
Debug bool
|
|
||||||
Template *fw.JetTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(port int, configPath string) (*Storage, fw.Error) {
|
|
||||||
conf, err := config.LoadConfig(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err.Tap()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Storage{
|
|
||||||
Config: conf,
|
|
||||||
Port: port,
|
|
||||||
Debug: conf.Debug,
|
|
||||||
Template: fw.NewJetTemplate("/app/templates"),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"pink_fox/inner/config"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
dbTest *DBTest = nil
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBTest struct {
|
|
||||||
db *sql.DB
|
|
||||||
seedersPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDBTest() *DBTest {
|
|
||||||
if dbTest == nil {
|
|
||||||
path := "/app/config-test.yml"
|
|
||||||
dbConf, err := config.LoadTestConfig(path)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Add("ошибка загрузки конфига"))
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := fw.CreateConnection(dbConf.Host, dbConf.Port, dbConf.User, dbConf.Password, dbConf.Database)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Add("ошибка соединение с базой данной"))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = refreshDatabase(db, dbConf.Migrations, dbConf.Database, true)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Add("ошибка обновление базы данных"))
|
|
||||||
}
|
|
||||||
|
|
||||||
dbTest = &DBTest{
|
|
||||||
db: db,
|
|
||||||
seedersPath: "/app/database/seeders",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dbTest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *DBTest) GetDB() *sql.DB {
|
|
||||||
return it.db
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *DBTest) Seed(filename string) *DBTest {
|
|
||||||
path := it.seedersPath + "/" + filename
|
|
||||||
queryByte, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
panic(fw.ErrStr(fmt.Sprintf("ошибка загрузки файла для %s: %s", path, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := it.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
panic(fw.ErrStr(fmt.Sprintf("ошибка запуска транзакции: %s", err)))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
cleanedData := it.removeSQLComments(string(queryByte))
|
|
||||||
queries := it.splitSQLQueries(cleanedData)
|
|
||||||
for _, query := range queries {
|
|
||||||
query = strings.TrimSpace(query)
|
|
||||||
if query == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = tx.Exec(query); err != nil {
|
|
||||||
panic(fmt.Errorf("ошибка в запросе `%s`: %s", query, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("ошибка подтверждения транзакции: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshDatabase(db *sql.DB, migrationsPath, dbName string, disableLogger bool) fw.Error {
|
|
||||||
err := fw.DropAppTable(db, dbName)
|
|
||||||
if err != nil {
|
|
||||||
return err.Tap()
|
|
||||||
}
|
|
||||||
|
|
||||||
return fw.MigrateUp(db, migrationsPath, disableLogger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *DBTest) removeSQLComments(sql string) string {
|
|
||||||
// Удаляем однострочные комментарии (-- до конца строки)
|
|
||||||
reSingleLine := regexp.MustCompile(`--.*`)
|
|
||||||
sql = reSingleLine.ReplaceAllString(sql, "")
|
|
||||||
|
|
||||||
// Удаляем многострочные комментарии (/* ... */)
|
|
||||||
reMultiLine := regexp.MustCompile(`/\*.*?\*/`)
|
|
||||||
sql = reMultiLine.ReplaceAllString(sql, "")
|
|
||||||
|
|
||||||
return sql
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitSQLQueries разбивает SQL-код на отдельные запросы
|
|
||||||
func (it *DBTest) splitSQLQueries(sql string) []string {
|
|
||||||
var queries []string
|
|
||||||
var buffer strings.Builder
|
|
||||||
inString := false
|
|
||||||
quoteChar := byte(0)
|
|
||||||
|
|
||||||
for i := 0; i < len(sql); i++ {
|
|
||||||
char := sql[i]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case char == '\'' || char == '"':
|
|
||||||
if !inString {
|
|
||||||
inString = true
|
|
||||||
quoteChar = char
|
|
||||||
} else if char == quoteChar {
|
|
||||||
// Проверяем, не экранирована ли кавычка
|
|
||||||
if i > 0 && sql[i-1] == '\\' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
inString = false
|
|
||||||
quoteChar = 0
|
|
||||||
}
|
|
||||||
buffer.WriteByte(char)
|
|
||||||
|
|
||||||
case char == ';' && !inString:
|
|
||||||
queries = append(queries, buffer.String())
|
|
||||||
buffer.Reset()
|
|
||||||
|
|
||||||
default:
|
|
||||||
buffer.WriteByte(char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем последний запрос (если есть)
|
|
||||||
if buffer.Len() > 0 {
|
|
||||||
queries = append(queries, buffer.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return queries
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package view
|
|
||||||
|
|
||||||
type View struct {
|
|
||||||
SiteTitle string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewView() *View {
|
|
||||||
return &View{
|
|
||||||
SiteTitle: "FW - framework on golang",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"pink_fox/inner"
|
|
||||||
"pink_fox/inner/config"
|
|
||||||
"pink_fox/inner/di"
|
|
||||||
"pink_fox/inner/storage"
|
|
||||||
"pink_fox/packages/fw"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
serverConfig := fw.ServerConfig[*storage.Storage, *di.Container]{
|
|
||||||
InitStorage: storage.New,
|
|
||||||
GetDebugMode: func(s *storage.Storage) bool {
|
|
||||||
return s.Debug
|
|
||||||
},
|
|
||||||
RegistrationRoutes: inner.RegistrationRoutes,
|
|
||||||
MakeContainer: di.MakeContainer,
|
|
||||||
CloseContainer: di.CloseContainer,
|
|
||||||
}
|
|
||||||
|
|
||||||
getConfig := func() (*fw.DatabaseConfig, fw.Error) {
|
|
||||||
return config.GetDatabaseConfig("/app/config.yml")
|
|
||||||
}
|
|
||||||
|
|
||||||
cli := fw.CLI{
|
|
||||||
Use: "pink_fox",
|
|
||||||
Short: "todo example project",
|
|
||||||
Commands: []fw.Command{
|
|
||||||
fw.GetCmdServer(serverConfig, 12001, "/app/config.yml"),
|
|
||||||
fw.GetCmdMigration(getConfig),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := fw.Start(&cli)
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
type BaseServices interface {
|
|
||||||
ResponseFactory() *ResponseFactory
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
type CLI struct {
|
|
||||||
Use string
|
|
||||||
Short string
|
|
||||||
Long string
|
|
||||||
Commands []Command
|
|
||||||
}
|
|
||||||
|
|
||||||
type Command struct {
|
|
||||||
Use string
|
|
||||||
Short string
|
|
||||||
Long string
|
|
||||||
Args cobra.PositionalArgs
|
|
||||||
Run func(cmd *cobra.Command, args []string)
|
|
||||||
Init func(cmd *cobra.Command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmdInit(cli *CLI) *cobra.Command {
|
|
||||||
rootCmd := &cobra.Command{
|
|
||||||
Use: cli.Use,
|
|
||||||
Short: cli.Short,
|
|
||||||
Long: cli.Long,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, command := range cli.Commands {
|
|
||||||
subCmd := &cobra.Command{
|
|
||||||
Use: command.Use,
|
|
||||||
Short: command.Short,
|
|
||||||
Long: command.Long,
|
|
||||||
Args: command.Args,
|
|
||||||
Run: command.Run,
|
|
||||||
}
|
|
||||||
if command.Init != nil {
|
|
||||||
command.Init(subCmd)
|
|
||||||
}
|
|
||||||
rootCmd.AddCommand(subCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootCmd
|
|
||||||
}
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
|
||||||
Host string
|
|
||||||
User string
|
|
||||||
Password string
|
|
||||||
Port string
|
|
||||||
Database string
|
|
||||||
Migrations string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
step = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetCmdMigration(getConfig func() (*DatabaseConfig, Error)) Command {
|
|
||||||
return Command{
|
|
||||||
Use: "migrate",
|
|
||||||
Short: "Выполнить миграции. Подкоманды [down, create]",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
conf, db, err := getConfigAndDatabase(getConfig)
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = MigrateUp(db, conf.Migrations, false)
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Init: func(cmd *cobra.Command) {
|
|
||||||
downCmd := &cobra.Command{
|
|
||||||
Use: "down",
|
|
||||||
Short: "Откатить одну миграцию",
|
|
||||||
Run: downCmdHandler(getConfig),
|
|
||||||
}
|
|
||||||
downCmd.Flags().IntVar(&step, "step", 1, "Количество отката миграции")
|
|
||||||
|
|
||||||
createCmd := &cobra.Command{
|
|
||||||
Use: "create",
|
|
||||||
Short: "Новая миграция",
|
|
||||||
Run: createCmdHandler(getConfig),
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.AddCommand(downCmd, createCmd)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func downCmdHandler(getConfig func() (*DatabaseConfig, Error)) func(*cobra.Command, []string) {
|
|
||||||
return func(cmd *cobra.Command, args []string) {
|
|
||||||
conf, db, err := getConfigAndDatabase(getConfig)
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if step == 0 {
|
|
||||||
step = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < step; i++ {
|
|
||||||
err = MigrateDown(db, conf.Migrations)
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createCmdHandler(getConfig func() (*DatabaseConfig, Error)) func(*cobra.Command, []string) {
|
|
||||||
return func(cmd *cobra.Command, args []string) {
|
|
||||||
conf, err := getConfig()
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = MigrateCreate(conf.Migrations, args[0])
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigAndDatabase(getConfig func() (*DatabaseConfig, Error)) (*DatabaseConfig, *sql.DB, Error) {
|
|
||||||
conf, err := getConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err.Tap()
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := CreateConnection(conf.Host, conf.Port, conf.User, conf.Password, conf.Database)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err.Tap()
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf, db, nil
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServerConfig[T, U any] struct {
|
|
||||||
InitStorage func(port int, pathConfig string) (storage T, err Error)
|
|
||||||
GetDebugMode func(T) bool
|
|
||||||
GetLogger func(T) Logger
|
|
||||||
InitServerEngine func(T) (*chi.Mux, Error)
|
|
||||||
RegistrationRoutes func(*RouterManager[T, U])
|
|
||||||
MakeContainer func(T, http.ResponseWriter, *http.Request) U
|
|
||||||
CloseContainer func(U)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCmdServer[T, U any](config ServerConfig[T, U], portDefault int, configPathDefault string) Command {
|
|
||||||
port := portDefault
|
|
||||||
configPath := configPathDefault
|
|
||||||
return Command{
|
|
||||||
Use: "server",
|
|
||||||
Short: "start the server",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
storage, err := config.InitStorage(port, configPath)
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var engine *chi.Mux
|
|
||||||
if config.InitServerEngine != nil {
|
|
||||||
engine, err = config.InitServerEngine(storage)
|
|
||||||
if err != nil {
|
|
||||||
Exit(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
engine = DefaultServerEngine()
|
|
||||||
}
|
|
||||||
|
|
||||||
debugMode := false
|
|
||||||
if config.GetDebugMode != nil {
|
|
||||||
debugMode = config.GetDebugMode(storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
var logger Logger = nil
|
|
||||||
if config.GetLogger != nil {
|
|
||||||
logger = config.GetLogger(storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
routesManager := NewRouterManager[T, U](
|
|
||||||
engine,
|
|
||||||
storage,
|
|
||||||
config.MakeContainer,
|
|
||||||
config.CloseContainer,
|
|
||||||
debugMode,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
config.RegistrationRoutes(routesManager)
|
|
||||||
|
|
||||||
fmt.Printf("Starting http_server at port %d...", port)
|
|
||||||
err_ := http.ListenAndServe(":12001", engine)
|
|
||||||
if err_ != nil {
|
|
||||||
Exit(Err(fmt.Errorf("error starting http server: %v", err)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Init: func(cmd *cobra.Command) {
|
|
||||||
cmd.Flags().IntVarP(&port, "port", "p", port, "start http server on port")
|
|
||||||
cmd.Flags().StringVarP(&configPath, "config", "c", configPath, "config file path")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultServerEngine() *chi.Mux {
|
|
||||||
return chi.NewRouter()
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Error interface {
|
|
||||||
Add(string) Error
|
|
||||||
Tap() Error
|
|
||||||
Message() string
|
|
||||||
Trace() []string
|
|
||||||
Error() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogError struct {
|
|
||||||
err error
|
|
||||||
messages []string
|
|
||||||
trace []string
|
|
||||||
stack []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func Err(err error) *LogError {
|
|
||||||
return &LogError{
|
|
||||||
err: err,
|
|
||||||
trace: []string{trace()},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrStr(msg string) *LogError {
|
|
||||||
return &LogError{
|
|
||||||
messages: []string{msg},
|
|
||||||
trace: []string{trace()},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *LogError) Add(msg string) Error {
|
|
||||||
it.trace = append(it.trace, trace())
|
|
||||||
it.messages = append(it.messages, msg)
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *LogError) Tap() Error {
|
|
||||||
it.trace = append(it.trace, trace())
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *LogError) Message() string {
|
|
||||||
result := ""
|
|
||||||
for i := len(it.messages) - 1; i >= 0; i-- {
|
|
||||||
if result == "" {
|
|
||||||
result += fmt.Sprintf("%v", it.messages[i])
|
|
||||||
} else {
|
|
||||||
result += fmt.Sprintf(": %v", it.messages[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if it.err != nil {
|
|
||||||
if result == "" {
|
|
||||||
result += fmt.Sprintf("%v", it.err)
|
|
||||||
} else {
|
|
||||||
result += fmt.Sprintf(": %v", it.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *LogError) Error() string {
|
|
||||||
result := ""
|
|
||||||
if len(it.trace) > 0 {
|
|
||||||
result = "\n\t" + strings.Join(it.trace, "\n\t")
|
|
||||||
}
|
|
||||||
return it.Message() + result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *LogError) Trace() []string {
|
|
||||||
return it.trace
|
|
||||||
}
|
|
||||||
|
|
||||||
type PanicError struct {
|
|
||||||
*LogError
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrPanic(err error, stack []byte) *PanicError {
|
|
||||||
e := PanicError{
|
|
||||||
LogError: Err(err),
|
|
||||||
}
|
|
||||||
e.trace = strings.Split(string(stack), "\n")
|
|
||||||
return &e
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
basePath = "packages/fw/error.go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
_, file, _, ok := runtime.Caller(0)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
basePath = file[:len(file)-len(basePath)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func trace() string {
|
|
||||||
_, file, line, ok := runtime.Caller(2)
|
|
||||||
if !ok {
|
|
||||||
return "it was not possible to recover the information"
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(file, basePath) {
|
|
||||||
return fmt.Sprintf("%s:%d", file[len(basePath):], line)
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("%s:%d", file, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTrace(t *testing.T) {
|
|
||||||
test := ErrStr("error 1")
|
|
||||||
_ = test.Add("message 2")
|
|
||||||
_ = test.Tap()
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"packages/fw/error_test.go:10", "packages/fw/error_test.go:11", "packages/fw/error_test.go:12"}, test.Trace())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErr(t *testing.T) {
|
|
||||||
test := Err(fmt.Errorf("test error"))
|
|
||||||
assert.Equal(t, "test error", test.Message())
|
|
||||||
assert.Equal(t, 1, len(test.Trace()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrAddMsq(t *testing.T) {
|
|
||||||
test := Err(fmt.Errorf("test error"))
|
|
||||||
_ = test.Add("message 2")
|
|
||||||
assert.Equal(t, "message 2: test error", test.Message())
|
|
||||||
assert.Equal(t, 2, len(test.Trace()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrAddMsq2(t *testing.T) {
|
|
||||||
test := Err(fmt.Errorf("test error"))
|
|
||||||
_ = test.Add("message 2")
|
|
||||||
_ = test.Add("message 3")
|
|
||||||
assert.Equal(t, "message 3: message 2: test error", test.Message())
|
|
||||||
assert.Equal(t, 3, len(test.Trace()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStr(t *testing.T) {
|
|
||||||
test := ErrStr("error 1")
|
|
||||||
assert.Equal(t, "error 1", test.Message())
|
|
||||||
assert.Equal(t, 1, len(test.Trace()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStrAddMsg(t *testing.T) {
|
|
||||||
test := ErrStr("error 1")
|
|
||||||
_ = test.Add("message 2")
|
|
||||||
assert.Equal(t, "message 2: error 1", test.Message())
|
|
||||||
assert.Equal(t, 2, len(test.Trace()))
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Start(cli *CLI) Error {
|
|
||||||
if err := cmdInit(cli).Execute(); err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Exit(err Error) {
|
|
||||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
type Logger interface {
|
|
||||||
Info(msg string, args ...any)
|
|
||||||
Error(msg string, args ...any)
|
|
||||||
Debug(msg string, args ...any)
|
|
||||||
Warn(msg string, args ...any)
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"github.com/pressly/goose/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MigrateUp(db *sql.DB, dir string, disableLogger bool) Error {
|
|
||||||
if disableLogger {
|
|
||||||
goose.SetLogger(goose.NopLogger())
|
|
||||||
}
|
|
||||||
err := goose.Up(db, dir)
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MigrateDown(conn *sql.DB, migrationPath string) Error {
|
|
||||||
err := goose.Down(conn, migrationPath)
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MigrateCreate(migrationPath, migrationName string) Error {
|
|
||||||
ctx := context.Background()
|
|
||||||
err := goose.RunContext(ctx, "create", nil, migrationPath, migrationName, "sql")
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DropAppTable(db *sql.DB, dbName string) Error {
|
|
||||||
rows, err := db.Query(`
|
|
||||||
SELECT table_name
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public' AND table_type = 'BASE TABLE' AND table_catalog = $1
|
|
||||||
`, dbName)
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = rows.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var tables []string
|
|
||||||
for rows.Next() {
|
|
||||||
var tableName string
|
|
||||||
if err = rows.Scan(&tableName); err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
tables = append(tables, tableName)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = rows.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, table := range tables {
|
|
||||||
_, err = db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", table))
|
|
||||||
if err != nil {
|
|
||||||
return Err(fmt.Errorf("error drop %s: %v", table, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateConnection(host, port, user, password, database string) (*sql.DB, Error) {
|
|
||||||
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
||||||
host, port, user, password, database)
|
|
||||||
var db *sql.DB
|
|
||||||
var err error
|
|
||||||
db, err = sql.Open("postgres", psqlInfo)
|
|
||||||
if err == nil {
|
|
||||||
err = db.Ping()
|
|
||||||
if err == nil {
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, Err(err)
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/CloudyKit/jet/v6"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JetTemplate struct {
|
|
||||||
set *jet.Set
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJetTemplate(path string) *JetTemplate {
|
|
||||||
return &JetTemplate{
|
|
||||||
set: jet.NewSet(jet.NewOSFileSystemLoader(path)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type JetRender struct {
|
|
||||||
engine *JetTemplate
|
|
||||||
Writer http.ResponseWriter
|
|
||||||
Variables jet.VarMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJetRender(engine *JetTemplate, writer http.ResponseWriter, variables jet.VarMap) *JetRender {
|
|
||||||
return &JetRender{
|
|
||||||
engine: engine,
|
|
||||||
Writer: writer,
|
|
||||||
Variables: variables,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *JetRender) Render(template string, data any) Error {
|
|
||||||
tpl, err := it.engine.set.GetTemplate(template)
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = tpl.Execute(&buf, it.Variables, data)
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(it.Writer, &buf)
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/CloudyKit/jet/v6"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Response interface {
|
|
||||||
Render() Error
|
|
||||||
WriteHeader(statusCode int)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseFactory struct {
|
|
||||||
writer http.ResponseWriter
|
|
||||||
template *JetTemplate
|
|
||||||
GetView func() any
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewResponseFactory(writer http.ResponseWriter, template *JetTemplate, getView func() any) *ResponseFactory {
|
|
||||||
return &ResponseFactory{
|
|
||||||
writer: writer,
|
|
||||||
template: template,
|
|
||||||
GetView: getView,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *ResponseFactory) String(str string) *StringResponse {
|
|
||||||
return NewStringResponse(it.writer, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *ResponseFactory) HtmlError(code int, message string) *HtmlErrorResponse {
|
|
||||||
return NewHtmlErrorResponse(it.makeJetRender(), code, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *ResponseFactory) View(template string, data any) *ViewResponse {
|
|
||||||
return NewViewResponse(it.makeJetRender(), template, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *ResponseFactory) makeJetRender() *JetRender {
|
|
||||||
variables := make(jet.VarMap)
|
|
||||||
if it.GetView != nil {
|
|
||||||
variables.Set("vi", it.GetView())
|
|
||||||
}
|
|
||||||
return NewJetRender(it.template, it.writer, variables)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseResponse struct {
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *BaseResponse) WriteHeader(statusCode int) {
|
|
||||||
it.statusCode = statusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
type StringResponse struct {
|
|
||||||
BaseResponse
|
|
||||||
str string
|
|
||||||
writer http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStringResponse(writer http.ResponseWriter, str string) *StringResponse {
|
|
||||||
return &StringResponse{
|
|
||||||
writer: writer,
|
|
||||||
str: str,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *StringResponse) Render() Error {
|
|
||||||
it.writer.WriteHeader(it.statusCode)
|
|
||||||
_, err := it.writer.Write([]byte(it.str))
|
|
||||||
if err != nil {
|
|
||||||
return Err(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HtmlErrorResponse struct {
|
|
||||||
ViewResponse
|
|
||||||
statusCode int
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHtmlErrorResponse(render *JetRender, code int, message string) *HtmlErrorResponse {
|
|
||||||
return &HtmlErrorResponse{
|
|
||||||
ViewResponse: *NewViewResponse(render, "errors/error.html", nil),
|
|
||||||
statusCode: code,
|
|
||||||
message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *HtmlErrorResponse) Render() Error {
|
|
||||||
fmt.Println("status", it.statusCode)
|
|
||||||
it.render.Writer.WriteHeader(it.statusCode)
|
|
||||||
err := it.render.Render(it.template, map[string]any{
|
|
||||||
"title": fmt.Sprintf("%d %s", it.statusCode, http.StatusText(it.statusCode)),
|
|
||||||
"message": it.message,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err.Tap()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ViewResponse struct {
|
|
||||||
BaseResponse
|
|
||||||
render *JetRender
|
|
||||||
template string
|
|
||||||
data any
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewViewResponse(render *JetRender, template string, data any) *ViewResponse {
|
|
||||||
return &ViewResponse{
|
|
||||||
render: render,
|
|
||||||
template: template,
|
|
||||||
data: data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *ViewResponse) Set(key string, value interface{}) *ViewResponse {
|
|
||||||
it.render.Variables.Set(key, value)
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *ViewResponse) Render() Error {
|
|
||||||
it.render.Writer.WriteHeader(it.statusCode)
|
|
||||||
err := it.render.Render(it.template, it.data)
|
|
||||||
if err != nil {
|
|
||||||
return err.Tap()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,224 +0,0 @@
|
|||||||
package fw
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"net/http"
|
|
||||||
"runtime/debug"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MiddlewareFunc[U any] func(U, ActionFunc) (Response, Error)
|
|
||||||
|
|
||||||
type HandlerFunc[U any] func(U) (Response, Error)
|
|
||||||
|
|
||||||
type ActionFunc func() (Response, Error)
|
|
||||||
|
|
||||||
type RouterManager[S, D any] struct {
|
|
||||||
mux *chi.Mux
|
|
||||||
storage S
|
|
||||||
fnMakeContainer func(S, http.ResponseWriter, *http.Request) D
|
|
||||||
fnCloseContainer func(D)
|
|
||||||
debug bool
|
|
||||||
logger Logger
|
|
||||||
middlewares []MiddlewareFunc[D]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouterManager[S, D any](mux *chi.Mux, storage S, fnMakeContainer func(S, http.ResponseWriter, *http.Request) D,
|
|
||||||
fnCloseContainer func(D), debug bool, logger Logger) *RouterManager[S, D] {
|
|
||||||
return &RouterManager[S, D]{
|
|
||||||
mux: mux,
|
|
||||||
storage: storage,
|
|
||||||
fnMakeContainer: fnMakeContainer,
|
|
||||||
fnCloseContainer: fnCloseContainer,
|
|
||||||
debug: debug,
|
|
||||||
logger: logger,
|
|
||||||
middlewares: make([]MiddlewareFunc[D], 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) Use(middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.middlewares = append(it.middlewares, middlewares...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) Group(relativePath string) *Group[S, D] {
|
|
||||||
return NewGroup(it, relativePath, it.middlewares...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) NotFound(handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.NotFound(it.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) NoMethod(handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.MethodNotAllowed(it.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) Get(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.Get(relativePath, it.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) Post(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.Post(relativePath, it.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) List(methods []string, relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
for _, m := range methods {
|
|
||||||
it.mux.Method(m, relativePath, it.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) Common(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.List([]string{http.MethodGet, http.MethodPost, http.MethodOptions}, relativePath, handler, middlewares...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) All(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.List([]string{
|
|
||||||
http.MethodGet,
|
|
||||||
http.MethodHead,
|
|
||||||
http.MethodPost,
|
|
||||||
http.MethodPut,
|
|
||||||
http.MethodPatch,
|
|
||||||
http.MethodDelete,
|
|
||||||
http.MethodConnect,
|
|
||||||
http.MethodOptions,
|
|
||||||
http.MethodTrace,
|
|
||||||
},
|
|
||||||
relativePath,
|
|
||||||
handler,
|
|
||||||
middlewares...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Group[S, D any] struct {
|
|
||||||
routerManager *RouterManager[S, D]
|
|
||||||
mux *chi.Mux
|
|
||||||
middlewares []MiddlewareFunc[D]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGroup[S, D any](routerManager *RouterManager[S, D], relativePath string, middlewares ...MiddlewareFunc[D]) *Group[S, D] {
|
|
||||||
mux := chi.NewRouter()
|
|
||||||
routerManager.mux.Mount(relativePath, mux)
|
|
||||||
return &Group[S, D]{
|
|
||||||
routerManager: routerManager,
|
|
||||||
mux: mux,
|
|
||||||
middlewares: middlewares,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) Use(middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.middlewares = append(it.middlewares, middlewares...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) Group(relativePath string, middlewares ...MiddlewareFunc[D]) *Group[S, D] {
|
|
||||||
mux := chi.NewRouter()
|
|
||||||
it.mux.Mount(relativePath, mux)
|
|
||||||
return &Group[S, D]{
|
|
||||||
routerManager: it.routerManager,
|
|
||||||
mux: mux,
|
|
||||||
middlewares: append(it.middlewares, middlewares...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) NotFound(handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.NotFound(it.routerManager.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) NoMethod(handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.MethodNotAllowed(it.routerManager.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) Get(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.Get(relativePath, it.routerManager.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) Post(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.mux.Post(relativePath, it.routerManager.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) List(methods []string, relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
for _, m := range methods {
|
|
||||||
it.mux.Method(m, relativePath, it.routerManager.makeHandler(handler, append(it.middlewares, middlewares...)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) Common(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.List([]string{http.MethodGet, http.MethodPost, http.MethodOptions}, relativePath, handler, middlewares...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *Group[S, D]) All(relativePath string, handler HandlerFunc[D], middlewares ...MiddlewareFunc[D]) {
|
|
||||||
it.List([]string{
|
|
||||||
http.MethodGet,
|
|
||||||
http.MethodHead,
|
|
||||||
http.MethodPost,
|
|
||||||
http.MethodPut,
|
|
||||||
http.MethodPatch,
|
|
||||||
http.MethodDelete,
|
|
||||||
http.MethodConnect,
|
|
||||||
http.MethodOptions,
|
|
||||||
http.MethodTrace,
|
|
||||||
},
|
|
||||||
relativePath,
|
|
||||||
handler,
|
|
||||||
middlewares...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) makeHandler(handler HandlerFunc[D], middlewares []MiddlewareFunc[D]) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
var err Error
|
|
||||||
defer func() {
|
|
||||||
if err_ := recover(); err_ != nil {
|
|
||||||
err = ErrPanic(fmt.Errorf("panic: %v", err_), debug.Stack())
|
|
||||||
it.errorsHandler(writer, err, it.debug, it.logger)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
container := it.fnMakeContainer(it.storage, writer, request)
|
|
||||||
defer it.fnCloseContainer(container)
|
|
||||||
|
|
||||||
err = it.pipeline(handler, middlewares, container)
|
|
||||||
if err != nil {
|
|
||||||
it.errorsHandler(writer, err, it.debug, it.logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[S, D]) pipeline(endpoint HandlerFunc[D], middlewares []MiddlewareFunc[D], container D) (err Error) {
|
|
||||||
handler := func() (Response, Error) {
|
|
||||||
return endpoint(container)
|
|
||||||
}
|
|
||||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
|
||||||
handler = it.createMiddleware(handler, middlewares[i], container)
|
|
||||||
}
|
|
||||||
resp, err := handler()
|
|
||||||
if err != nil {
|
|
||||||
_ = err.Tap()
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
err = ErrStr("response должен быть отображен в middleware, по всей видимости этот шаг был пропущен")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[T, U]) createMiddleware(next ActionFunc, middleware MiddlewareFunc[U], container U) ActionFunc {
|
|
||||||
return func() (Response, Error) {
|
|
||||||
return middleware(container, next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *RouterManager[T, U]) errorsHandler(writer http.ResponseWriter, err Error, debugOn bool, logger Logger) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Error(err.Message(), err.Trace())
|
|
||||||
}
|
|
||||||
|
|
||||||
message := ""
|
|
||||||
if debugOn {
|
|
||||||
message = fmt.Sprintf("<pre>%s</pre>", err.Error())
|
|
||||||
}
|
|
||||||
message = fmt.Sprintf("<h1>%d %s</h1>\n%s\n", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), message)
|
|
||||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
|
||||||
_, err_ := writer.Write([]byte(message))
|
|
||||||
if err_ != nil {
|
|
||||||
panic(ErrPanic(err_, debug.Stack()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<h1>{{ .title }}</h1>
|
|
||||||
|
|
||||||
<p>{{ .message }}</p>
|
|
||||||
|
|
||||||
{{ if .error != "" }}
|
|
||||||
<pre>{{ .error }}</pre>
|
|
||||||
{{ end }}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<h1>{{ .title }}</h1>
|
|
||||||
|
|
||||||
{{ if .message != "" }}
|
|
||||||
<p>{{ .message }}</p>
|
|
||||||
{{ end }}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
<h1>{{ vi.SiteTitle }}</h1>
|
|
||||||
|
|
||||||
<p>{{ .text }}</p>
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main1() {
|
|
||||||
name := Map(getUser(100), getName).OrElse("-")
|
|
||||||
fmt.Println(name)
|
|
||||||
|
|
||||||
value := Map(Map(getUser(200), getCard), getValue).OrElse(0)
|
|
||||||
fmt.Println(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUser(id int) Maybe[User] {
|
|
||||||
if id == 100 {
|
|
||||||
return None[User]()
|
|
||||||
}
|
|
||||||
user := User{
|
|
||||||
Name: "Test",
|
|
||||||
Card: &Card{Value: 100},
|
|
||||||
}
|
|
||||||
return Some[User](user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCard(user User) Maybe[Card] {
|
|
||||||
if user.Card != nil {
|
|
||||||
return Some[Card](*user.Card)
|
|
||||||
}
|
|
||||||
return None[Card]()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getValue(card Card) Maybe[int] {
|
|
||||||
return Some[int](card.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getName(user User) Maybe[string] {
|
|
||||||
return Some[string](user.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
Name string
|
|
||||||
Card *Card
|
|
||||||
}
|
|
||||||
|
|
||||||
type Card struct {
|
|
||||||
Value int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Maybe[T any] struct {
|
|
||||||
value T
|
|
||||||
present bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func Some[T any](value T) Maybe[T] {
|
|
||||||
return Maybe[T]{value: value, present: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
func None[T any]() Maybe[T] {
|
|
||||||
return Maybe[T]{present: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Map[T, R any](m Maybe[T], fn func(T) Maybe[R]) Maybe[R] {
|
|
||||||
if m.present {
|
|
||||||
return fn(m.value)
|
|
||||||
}
|
|
||||||
return None[R]()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Maybe[T]) IsPresent() bool {
|
|
||||||
return m.present
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Maybe[T]) OrElse(defaultValue T) T {
|
|
||||||
if m.present {
|
|
||||||
return m.value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
2
dist/.gitignore
vendored
Normal file
2
dist/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@ -9,8 +9,9 @@ services:
|
|||||||
- "12001:12001"
|
- "12001:12001"
|
||||||
- "12002:2345"
|
- "12002:2345"
|
||||||
volumes:
|
volumes:
|
||||||
- ./application:/app
|
- ./pink_fox_app:/app
|
||||||
- ./application/log:/var/log/pink_fox
|
- ./pink_fox_app/log:/var/log/pink_fox
|
||||||
|
- ./dist:/dist
|
||||||
- ./environment:/var/environment/pink_fox
|
- ./environment:/var/environment/pink_fox
|
||||||
- ./.storage/go:/go
|
- ./.storage/go:/go
|
||||||
environment:
|
environment:
|
||||||
@ -30,17 +31,3 @@ services:
|
|||||||
- ./services/postgres/init:/docker-entrypoint-initdb.d
|
- ./services/postgres/init:/docker-entrypoint-initdb.d
|
||||||
ports:
|
ports:
|
||||||
- "12003:5432"
|
- "12003:5432"
|
||||||
|
|
||||||
postgres_test:
|
|
||||||
image: postgres:16
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=pink_fox
|
|
||||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
||||||
- TZ=Europe/Moscow
|
|
||||||
- PGTZ=Europe/Moscow
|
|
||||||
volumes:
|
|
||||||
- ./services/postgres/init-test:/docker-entrypoint-initdb.d
|
|
||||||
tmpfs:
|
|
||||||
- /var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- "12004:5432"
|
|
||||||
|
|||||||
15
environment/example.config.yml
Normal file
15
environment/example.config.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Порт для приложения
|
||||||
|
port: 12001
|
||||||
|
|
||||||
|
# Соединение с postgres
|
||||||
|
db:
|
||||||
|
host: postgres
|
||||||
|
user: pink_fox
|
||||||
|
password: password
|
||||||
|
port: 5432
|
||||||
|
database: pink_fox_db
|
||||||
|
|
||||||
|
debug: true
|
||||||
|
|
||||||
|
# Путь к файлу лога
|
||||||
|
#logFile: /var/log/pink_fox/app.log
|
||||||
@ -1,5 +1,5 @@
|
|||||||
dlv.log
|
|
||||||
err.log
|
|
||||||
local_app
|
|
||||||
out.log
|
out.log
|
||||||
pink_fox
|
err.log
|
||||||
|
pink_fox
|
||||||
|
dlv.log
|
||||||
|
|
||||||
11
pink_fox_app/database/links.postgres.sql
Normal file
11
pink_fox_app/database/links.postgres.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
DROP TABLE IF EXISTS links;
|
||||||
|
|
||||||
|
CREATE TABLE links (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
token VARCHAR(24) NOT NULL UNIQUE,
|
||||||
|
url VARCHAR(512) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_links_token ON links (token);
|
||||||
@ -2,26 +2,19 @@ module pink_fox
|
|||||||
|
|
||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
|
||||||
github.com/spf13/cobra v1.9.1
|
|
||||||
github.com/stretchr/testify v1.10.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||||
github.com/CloudyKit/jet/v6 v6.3.1 // indirect
|
github.com/CloudyKit/jet/v6 v6.3.1 // indirect
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
github.com/bytedance/sonic v1.13.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
github.com/gin-gonic/gin v1.10.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.25.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
@ -29,23 +22,18 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/spf13/cobra v1.9.1 // indirect
|
||||||
github.com/pressly/goose/v3 v3.24.2 // indirect
|
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
golang.org/x/crypto v0.37.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
@ -2,8 +2,8 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4s
|
|||||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||||
github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
|
github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
|
||||||
github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
|
github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
@ -13,7 +13,6 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
|||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
@ -21,14 +20,12 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E
|
|||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@ -46,22 +43,15 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
|
||||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
|
|
||||||
github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
|
||||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
@ -76,29 +66,24 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
2
pink_fox_app/log/.gitignore
vendored
Normal file
2
pink_fox_app/log/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
20
pink_fox_app/main.go
Normal file
20
pink_fox_app/main.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"pink_fox/src/app/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
// перегрузка конфига
|
||||||
|
// отображение html страницы
|
||||||
|
// вынести перехват ошибок в middleware
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
pink_fox_app/src/app/app.go
Normal file
21
pink_fox_app/src/app/app.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"pink_fox/src/app/config"
|
||||||
|
"pink_fox/src/app/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
Conf *config.Config
|
||||||
|
Conn *sql.DB
|
||||||
|
Logger *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(conf *config.Config, conn *sql.DB, logger *logger.Logger) *Application {
|
||||||
|
return &Application{
|
||||||
|
Conf: conf,
|
||||||
|
Conn: conn,
|
||||||
|
Logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
47
pink_fox_app/src/app/cmd/cmd.go
Normal file
47
pink_fox_app/src/app/cmd/cmd.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
port = 12001
|
||||||
|
config = "/var/environment/pink_fox/config.yml"
|
||||||
|
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "pink_fox",
|
||||||
|
Short: "Розовый лис\nСквозь ссылок паутину —\nСвет в ночи горит.",
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCmd = &cobra.Command{
|
||||||
|
Use: "server",
|
||||||
|
Short: "Запуск сервера",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := NewServer().Execute(config, port)
|
||||||
|
if err != nil {
|
||||||
|
exit(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
serverCmd.Flags().IntVarP(&port, "port", "p", port, "Порт доступ к приложению")
|
||||||
|
serverCmd.Flags().StringVarP(&config, "config", "c", config, "Путь к файлу конфигурации")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(serverCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() error {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
return fmt.Errorf("cmd: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exit(err error) {
|
||||||
|
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
151
pink_fox_app/src/app/cmd/server.go
Normal file
151
pink_fox_app/src/app/cmd/server.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/CloudyKit/jet/v6"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/render"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"pink_fox/src/app"
|
||||||
|
c "pink_fox/src/app/config"
|
||||||
|
"pink_fox/src/app/db"
|
||||||
|
l "pink_fox/src/app/logger"
|
||||||
|
"pink_fox/src/app/routes_manager"
|
||||||
|
"pink_fox/src/app/views_manager"
|
||||||
|
"pink_fox/src/routes"
|
||||||
|
"pink_fox/src/views"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer() *Server {
|
||||||
|
return &Server{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Server) Execute(defaultConfig string, defaultPort int) error {
|
||||||
|
conf, err := c.LoadConfig(defaultConfig, defaultPort)
|
||||||
|
if err != nil {
|
||||||
|
exit(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := db.CreateConnection(&conf.Db)
|
||||||
|
if err != nil {
|
||||||
|
exit(err)
|
||||||
|
}
|
||||||
|
defer func(conn *sql.DB) {
|
||||||
|
_ = conn.Close()
|
||||||
|
}(conn)
|
||||||
|
|
||||||
|
logger, err := l.NewLogger(conf.LogFile)
|
||||||
|
if err != nil {
|
||||||
|
exit(err)
|
||||||
|
}
|
||||||
|
defer func(logger *l.Logger) {
|
||||||
|
_ = logger.Close()
|
||||||
|
}(logger)
|
||||||
|
|
||||||
|
return it.StartServer(app.New(conf, conn, logger))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Server) StartServer(application *app.Application) error {
|
||||||
|
r := gin.Default()
|
||||||
|
_ = r.SetTrustedProxies(nil)
|
||||||
|
r.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "ok")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Use(it.errorHandler(application.Logger))
|
||||||
|
|
||||||
|
err := it.loadTemplates(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := views_manager.NewManager()
|
||||||
|
views.RegistrationViews(tm)
|
||||||
|
|
||||||
|
routes.RegistrationRoutes(routes_manager.NewManager(r, application, tm))
|
||||||
|
|
||||||
|
fmt.Printf("Starting http_server at port %d...", application.Conf.Port)
|
||||||
|
err = r.Run(fmt.Sprintf(":%d", application.Conf.Port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Server) loadTemplates(e *gin.Engine) error {
|
||||||
|
set := jet.NewSet(jet.NewOSFileSystemLoader("views"))
|
||||||
|
renderer := &JetRenderer{set: set}
|
||||||
|
e.HTMLRender = renderer
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Server) errorHandler(logger *l.Logger) gin.HandlerFunc {
|
||||||
|
return func(ctx *gin.Context) {
|
||||||
|
it.errorHandlerSetRecovery(ctx)
|
||||||
|
if len(ctx.Errors) > 0 {
|
||||||
|
logger.Error(ctx.Errors[0].Error())
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"errors": "Internal Server Error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Server) errorHandlerSetRecovery(ctx *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
_ = ctx.Error(fmt.Errorf("panic: %v\n%s", err, debug.Stack()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
type JetRenderer struct {
|
||||||
|
set *jet.Set
|
||||||
|
template string
|
||||||
|
data any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *JetRenderer) Instance(tpl string, data any) render.Render {
|
||||||
|
it.template = tpl
|
||||||
|
it.data = data
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *JetRenderer) Render(w http.ResponseWriter) error {
|
||||||
|
tpl, err := it.set.GetTemplate(it.template)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := make(jet.VarMap)
|
||||||
|
if it.data != nil {
|
||||||
|
if v, ok := it.data.(jet.VarMap); ok {
|
||||||
|
vars = v
|
||||||
|
} else {
|
||||||
|
vars.Set("data", it.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = tpl.Execute(&buf, vars, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(w, &buf)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *JetRenderer) WriteContentType(http.ResponseWriter) {
|
||||||
|
// не знаю в каких случаях будет вызван этот метод
|
||||||
|
panic("implement me") // TODO
|
||||||
|
}
|
||||||
45
pink_fox_app/src/app/config/config.go
Normal file
45
pink_fox_app/src/app/config/config.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Port string `yaml:"port"`
|
||||||
|
Database string `yaml:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Db DB `yaml:"db"`
|
||||||
|
LogFile string `yaml:"logFile"`
|
||||||
|
Debug bool `yaml:"debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(defaultConfig string, defaultPort int) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(defaultConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка чтения файла конфига: %s", err)
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("не получилось распарсить конфиг: %s", err)
|
||||||
|
}
|
||||||
|
return setDefaultValue(&config, defaultPort), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaultValue(conf *Config, defaultPort int) *Config {
|
||||||
|
if conf.Port == 0 {
|
||||||
|
conf.Port = defaultPort
|
||||||
|
}
|
||||||
|
if conf.LogFile == "" {
|
||||||
|
conf.LogFile = "/var/log/pink_fox/app.log"
|
||||||
|
}
|
||||||
|
return conf
|
||||||
|
}
|
||||||
30
pink_fox_app/src/app/db/db.go
Normal file
30
pink_fox_app/src/app/db/db.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"pink_fox/src/app/config"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateConnection(dbConf *config.DB) (*sql.DB, error) {
|
||||||
|
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
dbConf.Host, dbConf.Port, dbConf.User, dbConf.Password, dbConf.Database)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
db, err = sql.Open("postgres", psqlInfo)
|
||||||
|
if err == nil {
|
||||||
|
err = db.Ping()
|
||||||
|
if err == nil {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("не удалось подключиться к базе данных: %v", err)
|
||||||
|
}
|
||||||
17
pink_fox_app/src/app/http_server/request.go
Normal file
17
pink_fox_app/src/app/http_server/request.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package http_server
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
ctx *gin.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequest(ctx *gin.Context) *Request {
|
||||||
|
return &Request{
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Request) Param(key string) string {
|
||||||
|
return it.ctx.Param(key)
|
||||||
|
}
|
||||||
34
pink_fox_app/src/app/http_server/response-factory.go
Normal file
34
pink_fox_app/src/app/http_server/response-factory.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package http_server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"pink_fox/src/app/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResponseFactory struct {
|
||||||
|
ctx *gin.Context
|
||||||
|
makeViewObject types.MakeViewObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponseFactory(ctx *gin.Context, makeViewObject types.MakeViewObject) *ResponseFactory {
|
||||||
|
return &ResponseFactory{
|
||||||
|
ctx: ctx,
|
||||||
|
makeViewObject: makeViewObject,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ResponseFactory) String(s string) *StringResponse {
|
||||||
|
return NewStringResponse(s, it.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ResponseFactory) HtmlError(code int) *HtmlErrorResponse {
|
||||||
|
return NewHtmlErrorResponse(code, it.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ResponseFactory) Redirect(url string) *RedirectResponse {
|
||||||
|
return NewRedirectResponse(url, it.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ResponseFactory) View(id int16, file string, data any) *ViewResponse {
|
||||||
|
return NewViewResponse(it.ctx, it.makeViewObject, id, file, data)
|
||||||
|
}
|
||||||
95
pink_fox_app/src/app/http_server/response.go
Normal file
95
pink_fox_app/src/app/http_server/response.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package http_server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/CloudyKit/jet/v6"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"pink_fox/src/app/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response interface {
|
||||||
|
Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringResponse struct {
|
||||||
|
str string
|
||||||
|
ctx *gin.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStringResponse(s string, ctx *gin.Context) *StringResponse {
|
||||||
|
return &StringResponse{str: s, ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *StringResponse) Render() {
|
||||||
|
it.ctx.String(http.StatusOK, it.str)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtmlErrorResponse struct {
|
||||||
|
code int
|
||||||
|
msg string
|
||||||
|
ctx *gin.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHtmlErrorResponse(code int, ctx *gin.Context) *HtmlErrorResponse {
|
||||||
|
return &HtmlErrorResponse{code: code, ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *HtmlErrorResponse) Msg(msg string) *HtmlErrorResponse {
|
||||||
|
it.msg = msg
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *HtmlErrorResponse) Render() {
|
||||||
|
message := ""
|
||||||
|
if it.msg != "" {
|
||||||
|
message = fmt.Sprintf("<p>%s</p>", it.msg)
|
||||||
|
}
|
||||||
|
it.ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
it.ctx.String(it.code, fmt.Sprintf("<h1>%d %s</h1>\n%s\n", it.code, http.StatusText(it.code), message))
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedirectResponse struct {
|
||||||
|
code int
|
||||||
|
to string
|
||||||
|
ctx *gin.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedirectResponse(to string, ctx *gin.Context) *RedirectResponse {
|
||||||
|
return &RedirectResponse{code: 302, to: to, ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RedirectResponse) SetCode(code int) *RedirectResponse {
|
||||||
|
it.code = code
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RedirectResponse) Render() {
|
||||||
|
it.ctx.Redirect(it.code, it.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewResponse struct {
|
||||||
|
ctx *gin.Context
|
||||||
|
makeViewObject types.MakeViewObject
|
||||||
|
id int16
|
||||||
|
file string
|
||||||
|
data any
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewViewResponse(ctx *gin.Context, makeViewObject types.MakeViewObject, id int16, file string, data any) *ViewResponse {
|
||||||
|
return &ViewResponse{
|
||||||
|
ctx: ctx,
|
||||||
|
makeViewObject: makeViewObject,
|
||||||
|
id: id,
|
||||||
|
file: file,
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ViewResponse) Render() {
|
||||||
|
data := make(jet.VarMap)
|
||||||
|
data.Set("vi", it.makeViewObject(it.id))
|
||||||
|
data.Set("it", it.data)
|
||||||
|
|
||||||
|
it.ctx.HTML(http.StatusOK, it.file, data)
|
||||||
|
}
|
||||||
81
pink_fox_app/src/app/lerror/lerror.go
Normal file
81
pink_fox_app/src/app/lerror/lerror.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package lerror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lenBasePath int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
path, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
path = ""
|
||||||
|
}
|
||||||
|
lenBasePath = len(filepath.Dir(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
msg string
|
||||||
|
trace []string
|
||||||
|
debugStack []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewString(msg string) *Error {
|
||||||
|
return &Error{
|
||||||
|
msg: msg,
|
||||||
|
trace: []string{trace()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(err error) *Error {
|
||||||
|
return &Error{
|
||||||
|
msg: err.Error(),
|
||||||
|
trace: []string{trace()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Error) Add(msg string) *Error {
|
||||||
|
it.trace = append(it.trace, trace())
|
||||||
|
it.msg = msg + ": " + it.msg
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Error) Tap() *Error {
|
||||||
|
it.trace = append(it.trace, trace())
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Error) String() string {
|
||||||
|
result := it.msg
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Error) GetTrace() []string {
|
||||||
|
return it.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Error) SetDebugStack(stack []byte) *Error {
|
||||||
|
it.debugStack = stack
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Error) GetDebugStack() string {
|
||||||
|
return string(it.debugStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trace() string {
|
||||||
|
_, file, line, ok := runtime.Caller(2)
|
||||||
|
if !ok {
|
||||||
|
return "not possible to recover the information"
|
||||||
|
}
|
||||||
|
if lenBasePath < len(file) {
|
||||||
|
return fmt.Sprintf("%s:%d", file[lenBasePath:], line)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
pink_fox_app/src/app/logger/log.go
Normal file
34
pink_fox_app/src/app/logger/log.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
file *os.File
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(path string) (*Logger, error) {
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось открыть файл лога: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := slog.New(slog.NewTextHandler(file, nil))
|
||||||
|
|
||||||
|
return &Logger{
|
||||||
|
file: file,
|
||||||
|
logger: logger,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Logger) Close() error {
|
||||||
|
return it.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Logger) Error(msg string, args ...any) {
|
||||||
|
it.logger.Error(msg, args...)
|
||||||
|
}
|
||||||
192
pink_fox_app/src/app/routes_manager/routes_manager.go
Normal file
192
pink_fox_app/src/app/routes_manager/routes_manager.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
package routes_manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"pink_fox/src/app"
|
||||||
|
hs "pink_fox/src/app/http_server"
|
||||||
|
le "pink_fox/src/app/lerror"
|
||||||
|
"pink_fox/src/app/types"
|
||||||
|
vm2 "pink_fox/src/app/views_manager"
|
||||||
|
"pink_fox/src/dependence"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoutesManager struct {
|
||||||
|
engine *gin.Engine
|
||||||
|
application *app.Application
|
||||||
|
middlewares []MiddlewareFunc
|
||||||
|
vm *vm2.ViewsManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(e *gin.Engine, application *app.Application, vm *vm2.ViewsManager) *RoutesManager {
|
||||||
|
return &RoutesManager{
|
||||||
|
engine: e,
|
||||||
|
application: application,
|
||||||
|
middlewares: make([]MiddlewareFunc, 0),
|
||||||
|
vm: vm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiddlewareFunc func(*dependence.Container, ActionFunc) (hs.Response, *le.Error)
|
||||||
|
|
||||||
|
type HandlerFunc func(container *dependence.Container) (hs.Response, *le.Error)
|
||||||
|
|
||||||
|
type ActionFunc func() (hs.Response, *le.Error)
|
||||||
|
|
||||||
|
func (it *RoutesManager) Group(relativePath string, middlewares ...MiddlewareFunc) *Group {
|
||||||
|
return NewGroup(it, relativePath, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RoutesManager) Use(middlewares ...MiddlewareFunc) {
|
||||||
|
it.middlewares = append(it.middlewares, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RoutesManager) ANY(relativePath string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
|
||||||
|
handlerForGin := makeHandlerGin(handler, append(it.middlewares, middlewares...), it.application, it.vm)
|
||||||
|
it.engine.GET(relativePath, handlerForGin)
|
||||||
|
it.engine.POST(relativePath, handlerForGin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RoutesManager) GET(relativePath string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
|
||||||
|
handlerForGin := makeHandlerGin(handler, append(it.middlewares, middlewares...), it.application, it.vm)
|
||||||
|
it.engine.GET(relativePath, handlerForGin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RoutesManager) POST(relativePath string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
|
||||||
|
handlerForGin := makeHandlerGin(handler, append(it.middlewares, middlewares...), it.application, it.vm)
|
||||||
|
it.engine.POST(relativePath, handlerForGin)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
routesManager *RoutesManager
|
||||||
|
relativePath string
|
||||||
|
middlewares []MiddlewareFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroup(route *RoutesManager, relativePath string, middlewares ...MiddlewareFunc) *Group {
|
||||||
|
return &Group{
|
||||||
|
routesManager: route,
|
||||||
|
relativePath: relativePath,
|
||||||
|
middlewares: append(route.middlewares, middlewares...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Group) Group(relativePath string, middlewares ...MiddlewareFunc) *Group {
|
||||||
|
return &Group{
|
||||||
|
routesManager: it.routesManager,
|
||||||
|
relativePath: it.relativePath + relativePath,
|
||||||
|
middlewares: append(it.middlewares, middlewares...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Group) Use(middlewares ...MiddlewareFunc) {
|
||||||
|
it.middlewares = append(it.middlewares, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Group) ANY(relativePath string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
|
||||||
|
handlerForGin := makeHandlerGin(handler, append(it.middlewares, middlewares...), it.routesManager.application, it.routesManager.vm)
|
||||||
|
it.routesManager.engine.GET(it.relativePath+relativePath, handlerForGin)
|
||||||
|
it.routesManager.engine.POST(it.relativePath+relativePath, handlerForGin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Group) GET(relativePath string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
|
||||||
|
handlerForGin := makeHandlerGin(handler, append(it.middlewares, middlewares...), it.routesManager.application, it.routesManager.vm)
|
||||||
|
it.routesManager.engine.GET(it.relativePath+relativePath, handlerForGin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Group) POST(relativePath string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
|
||||||
|
handlerForGin := makeHandlerGin(handler, append(it.middlewares, middlewares...), it.routesManager.application, it.routesManager.vm)
|
||||||
|
it.routesManager.engine.POST(it.relativePath+relativePath, handlerForGin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHandlerGin(handler HandlerFunc, middlewares []MiddlewareFunc, application *app.Application, vm *vm2.ViewsManager) gin.HandlerFunc {
|
||||||
|
return func(ctx *gin.Context) {
|
||||||
|
dic := dependence.NewContainer(application, ctx)
|
||||||
|
dic.SetViewManager(castMakeViewObject(dic, vm))
|
||||||
|
defer dic.Close()
|
||||||
|
err := execHandlerGin(handler, middlewares, dic, ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeToLog(err, application)
|
||||||
|
showHtmlError(err, application, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func castMakeViewObject(depend *dependence.Container, vm *vm2.ViewsManager) types.MakeViewObject {
|
||||||
|
return func(id int16) any {
|
||||||
|
obj, err := vm.GetViewObject(id, depend)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("шаблон с id=%d не найден", id))
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func execHandlerGin(handler HandlerFunc, middlewares []MiddlewareFunc, dic *dependence.Container, ctx *gin.Context) *le.Error {
|
||||||
|
res, err := pipeline(handler, middlewares, dic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Render()
|
||||||
|
if len(ctx.Errors) > 0 {
|
||||||
|
errorGin := ctx.Errors[0]
|
||||||
|
ctx.Errors = make([]*gin.Error, 0)
|
||||||
|
return le.New(errorGin)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipeline(endpoint HandlerFunc, middlewares []MiddlewareFunc, di *dependence.Container) (resp hs.Response, lerr *le.Error) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
resp = nil
|
||||||
|
lerr = le.New(fmt.Errorf("произошла паника: %v", err)).SetDebugStack(debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler := func() (hs.Response, *le.Error) {
|
||||||
|
return endpoint(di)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||||
|
handler = createMiddleware(handler, middlewares[i], di)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, lerr = handler()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMiddleware(next ActionFunc, middleware MiddlewareFunc, di *dependence.Container) ActionFunc {
|
||||||
|
return func() (hs.Response, *le.Error) {
|
||||||
|
return middleware(di, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeToLog(err *le.Error, application *app.Application) {
|
||||||
|
moreMessage := make([]any, 0)
|
||||||
|
if debugStack := err.GetDebugStack(); debugStack != "" {
|
||||||
|
moreMessage = append(moreMessage, "stack", debugStack)
|
||||||
|
}
|
||||||
|
moreMessage = append(moreMessage, "trace")
|
||||||
|
moreMessage = append(moreMessage, strings.Join(err.GetTrace(), ";\n")+";")
|
||||||
|
application.Logger.Error(err.String(), moreMessage...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showHtmlError(err *le.Error, application *app.Application, ctx *gin.Context) {
|
||||||
|
message := ""
|
||||||
|
if application.Conf.Debug {
|
||||||
|
if debugStack := err.GetDebugStack(); debugStack != "" {
|
||||||
|
message = fmt.Sprintf("<pre>%s\n\n%s\n%s\n<pre>", err.String(), debugStack, strings.Join(err.GetTrace(), "\n"))
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("<pre>%s\n\n%s\n<pre>", err.String(), strings.Join(err.GetTrace(), "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
ctx.String(http.StatusInternalServerError, fmt.Sprintf("<h1>%d %s</h1>\n%s\n", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), message))
|
||||||
|
}
|
||||||
3
pink_fox_app/src/app/timer/timer.go
Normal file
3
pink_fox_app/src/app/timer/timer.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package timer
|
||||||
|
|
||||||
|
// TODO
|
||||||
3
pink_fox_app/src/app/types/types.go
Normal file
3
pink_fox_app/src/app/types/types.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type MakeViewObject func(int16) any
|
||||||
30
pink_fox_app/src/app/views_manager/views_manager.go
Normal file
30
pink_fox_app/src/app/views_manager/views_manager.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package views_manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"pink_fox/src/dependence"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ViewsManager struct {
|
||||||
|
list map[int16]ViewsFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *ViewsManager {
|
||||||
|
return &ViewsManager{
|
||||||
|
list: make(map[int16]ViewsFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ViewsManager) Add(id int16, tplFunc ViewsFunc) {
|
||||||
|
it.list[id] = tplFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ViewsManager) GetViewObject(id int16, depend *dependence.Container) (any, error) {
|
||||||
|
tplFunc, ok := it.list[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("GetViewObject: template id %v not found", id)
|
||||||
|
}
|
||||||
|
return tplFunc(depend), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewsFunc func(*dependence.Container) any
|
||||||
33
pink_fox_app/src/controllers/Go.go
Normal file
33
pink_fox_app/src/controllers/Go.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"pink_fox/src/app/http_server"
|
||||||
|
le "pink_fox/src/app/lerror"
|
||||||
|
"pink_fox/src/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoController struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGoController() *GoController {
|
||||||
|
return &GoController{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexActionDependence interface {
|
||||||
|
MakeRequest() *http_server.Request
|
||||||
|
MakeResponseFactory() *http_server.ResponseFactory
|
||||||
|
MakeLinksRepository() repositories.LinksRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *GoController) IndexAction(depend IndexActionDependence) (http_server.Response, *le.Error) {
|
||||||
|
token := depend.MakeRequest().Param("token")
|
||||||
|
link, err := depend.MakeLinksRepository().ByToken(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err.Tap()
|
||||||
|
} else if link == nil {
|
||||||
|
return depend.MakeResponseFactory().HtmlError(404), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return depend.MakeResponseFactory().String(fmt.Sprintf("Ссылка %d, token \"%s\" to \"%v\", дата %v", link.ID, link.Token, link.Url, link.CreatedAt)), nil
|
||||||
|
}
|
||||||
25
pink_fox_app/src/controllers/Html.go
Normal file
25
pink_fox_app/src/controllers/Html.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"pink_fox/src/app/http_server"
|
||||||
|
le "pink_fox/src/app/lerror"
|
||||||
|
"pink_fox/src/views"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HtmlController struct {
|
||||||
|
responseFactory *http_server.ResponseFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtmlControllerDependence interface {
|
||||||
|
MakeResponseFactory() *http_server.ResponseFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHtmlController(depend HtmlControllerDependence) *HtmlController {
|
||||||
|
return &HtmlController{
|
||||||
|
responseFactory: depend.MakeResponseFactory(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *HtmlController) IndexAction() (http_server.Response, *le.Error) {
|
||||||
|
return it.responseFactory.View(views.Base, "test.jet", "hello"), nil
|
||||||
|
}
|
||||||
24
pink_fox_app/src/controllers/Index.go
Normal file
24
pink_fox_app/src/controllers/Index.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"pink_fox/src/app/http_server"
|
||||||
|
le "pink_fox/src/app/lerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexController struct {
|
||||||
|
responseFactory *http_server.ResponseFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexControllerDependence interface {
|
||||||
|
MakeResponseFactory() *http_server.ResponseFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndexController(depend IndexControllerDependence) *IndexController {
|
||||||
|
return &IndexController{
|
||||||
|
responseFactory: depend.MakeResponseFactory(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *IndexController) IndexAction() (http_server.Response, *le.Error) {
|
||||||
|
return it.responseFactory.String("Hello world!"), nil
|
||||||
|
}
|
||||||
41
pink_fox_app/src/dependence/container.go
Normal file
41
pink_fox_app/src/dependence/container.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package dependence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"pink_fox/src/app"
|
||||||
|
"pink_fox/src/app/http_server"
|
||||||
|
"pink_fox/src/app/types"
|
||||||
|
repo "pink_fox/src/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
application *app.Application
|
||||||
|
context *gin.Context
|
||||||
|
makeViewObject types.MakeViewObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContainer(application *app.Application, context *gin.Context) *Container {
|
||||||
|
return &Container{
|
||||||
|
application: application,
|
||||||
|
context: context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Container) SetViewManager(makeViewObject types.MakeViewObject) {
|
||||||
|
it.makeViewObject = makeViewObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Container) Close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Container) MakeRequest() *http_server.Request {
|
||||||
|
return http_server.NewRequest(it.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Container) MakeResponseFactory() *http_server.ResponseFactory {
|
||||||
|
return http_server.NewResponseFactory(it.context, it.makeViewObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Container) MakeLinksRepository() repo.LinksRepository {
|
||||||
|
return repo.NewLinksRepository(it.application.Conn)
|
||||||
|
}
|
||||||
48
pink_fox_app/src/repositories/links.go
Normal file
48
pink_fox_app/src/repositories/links.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
le "pink_fox/src/app/lerror"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LinkData struct {
|
||||||
|
ID int
|
||||||
|
Token string
|
||||||
|
Url string
|
||||||
|
CreatedAt time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinksRepository interface {
|
||||||
|
ByToken(string) (*LinkData, *le.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinksRepositoryImpl struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLinksRepository(conn *sql.DB) *LinksRepositoryImpl {
|
||||||
|
return &LinksRepositoryImpl{
|
||||||
|
db: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME нужно настроить время
|
||||||
|
// время для го
|
||||||
|
// время для postgres
|
||||||
|
|
||||||
|
func (it *LinksRepositoryImpl) ByToken(token string) (*LinkData, *le.Error) {
|
||||||
|
var link LinkData
|
||||||
|
query := `SELECT id, token, url, created_at, expires_at FROM links WHERE token = $1`
|
||||||
|
err := it.db.QueryRow(query, token).Scan(&link.ID, &link.Token, &link.Url, &link.CreatedAt, &link.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
} else {
|
||||||
|
return nil, le.New(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &link, nil
|
||||||
|
}
|
||||||
29
pink_fox_app/src/routes/registration.go
Normal file
29
pink_fox_app/src/routes/registration.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
hs "pink_fox/src/app/http_server"
|
||||||
|
le "pink_fox/src/app/lerror"
|
||||||
|
"pink_fox/src/app/routes_manager"
|
||||||
|
"pink_fox/src/controllers"
|
||||||
|
di "pink_fox/src/dependence"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegistrationRoutes(r *routes_manager.RoutesManager) {
|
||||||
|
r.GET("/", IndexController)
|
||||||
|
|
||||||
|
r.GET("/link/:token", GoController)
|
||||||
|
|
||||||
|
r.GET("/html", HtmlController)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HtmlController(depend *di.Container) (hs.Response, *le.Error) {
|
||||||
|
return controllers.NewHtmlController(depend).IndexAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexController(depend *di.Container) (hs.Response, *le.Error) {
|
||||||
|
return controllers.NewIndexController(depend).IndexAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GoController(depend *di.Container) (hs.Response, *le.Error) {
|
||||||
|
return controllers.NewGoController().IndexAction(depend)
|
||||||
|
}
|
||||||
21
pink_fox_app/src/views/base.go
Normal file
21
pink_fox_app/src/views/base.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
repo "pink_fox/src/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Base int16 = 1
|
||||||
|
|
||||||
|
type BaseView struct {
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseViewInterface interface {
|
||||||
|
MakeLinksRepository() repo.LinksRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBase(_ BaseViewInterface) *BaseView {
|
||||||
|
return &BaseView{
|
||||||
|
Title: "Hello World!",
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pink_fox_app/src/views/registration.go
Normal file
16
pink_fox_app/src/views/registration.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"pink_fox/src/app/views_manager"
|
||||||
|
"pink_fox/src/dependence"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegistrationViews(tm *views_manager.ViewsManager) {
|
||||||
|
tm.Add(Base, MakeBaseView())
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeBaseView() func(container *dependence.Container) any {
|
||||||
|
return func(depend *dependence.Container) any {
|
||||||
|
return NewBase(depend)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
pink_fox_app/views/test.jet
Normal file
2
pink_fox_app/views/test.jet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<h1>{{ vi.Title }}</h1>
|
||||||
|
<p>{{ it }}</p>
|
||||||
@ -1 +0,0 @@
|
|||||||
CREATE DATABASE pink_fox_db_test with owner pink_fox;
|
|
||||||
@ -16,30 +16,30 @@ fi
|
|||||||
if [ "$ARG1" = "debug" ]; then
|
if [ "$ARG1" = "debug" ]; then
|
||||||
# Собираем приложение для дебага
|
# Собираем приложение для дебага
|
||||||
cd /app || exit 1
|
cd /app || exit 1
|
||||||
rm -f "/app/local_app"
|
rm -f "/dist/local_app_name"
|
||||||
echo "" > /app/out.log
|
echo "" > /app/out.log
|
||||||
echo "" > /app/err.log
|
echo "" > /app/err.log
|
||||||
echo "" > /app/dlv.log
|
echo "" > /app/dlv.log
|
||||||
|
|
||||||
pkill -f /app/local_app
|
pkill -f /dist/local_app_name
|
||||||
|
|
||||||
go build -gcflags "all=-N -l" -o /app/local_app > /app/out.log 2> /app/err.log
|
go build -gcflags "all=-N -l" -o /dist/local_app_name > /app/out.log 2> /app/err.log
|
||||||
|
|
||||||
if [ ! -s "/app/err.log" ]; then
|
if [ ! -s "/app/err.log" ]; then
|
||||||
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec /app/local_app server > /app/out.log 2> /app/dlv.log
|
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec /dist/local_app_name server > /app/out.log 2> /app/dlv.log
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Собираем приложение
|
# Собираем приложение
|
||||||
cd /app || exit 1
|
cd /app || exit 1
|
||||||
rm -f "/app/local_app"
|
rm -f "/dist/local_app_name"
|
||||||
echo "" > /app/out.log
|
echo "" > /app/out.log
|
||||||
echo "" > /app/err.log
|
echo "" > /app/err.log
|
||||||
|
|
||||||
pkill -f /app/local_app
|
pkill -f /dist/local_app_name
|
||||||
|
|
||||||
go build -o "/app/local_app" > /app/out.log 2> /app/err.log
|
go build -o "/dist/local_app_name" > /app/out.log 2> /app/err.log
|
||||||
|
|
||||||
if [ ! -s "/app/err.log" ]; then
|
if [ ! -s "/app/err.log" ]; then
|
||||||
/app/local_app server > /app/out.log 2> /app/err.log
|
/dist/local_app_name server > /app/out.log 2> /app/err.log
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -19,7 +19,7 @@ if len(sys.argv) > 1 and sys.argv[1] == "debug":
|
|||||||
env["ARG1"] = "debug"
|
env["ARG1"] = "debug"
|
||||||
|
|
||||||
# Запускаем сервис через Docker compose
|
# Запускаем сервис через Docker compose
|
||||||
subprocess.run(["docker", "compose", "up", "site", "-d"], env=env)
|
subprocess.run(["docker", "compose", "up", "site", "-d", "--build"], env=env)
|
||||||
|
|
||||||
attempt_count = 0
|
attempt_count = 0
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user