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
|
||||
.storage
|
||||
.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"
|
||||
- "12002:2345"
|
||||
volumes:
|
||||
- ./application:/app
|
||||
- ./application/log:/var/log/pink_fox
|
||||
- ./pink_fox_app:/app
|
||||
- ./pink_fox_app/log:/var/log/pink_fox
|
||||
- ./dist:/dist
|
||||
- ./environment:/var/environment/pink_fox
|
||||
- ./.storage/go:/go
|
||||
environment:
|
||||
@ -30,17 +31,3 @@ services:
|
||||
- ./services/postgres/init:/docker-entrypoint-initdb.d
|
||||
ports:
|
||||
- "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
|
||||
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
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // 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/cloudwego/base64x v0.1.5 // 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/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/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/inconshreveable/mousetrap v1.1.0 // 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/lib/pq v1.10.9 // 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/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pressly/goose/v3 v3.24.2 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // 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/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
|
||||
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.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||
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.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
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/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/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||
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/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
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/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/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
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/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.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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/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/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
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/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
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.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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
|
||||
# Собираем приложение для дебага
|
||||
cd /app || exit 1
|
||||
rm -f "/app/local_app"
|
||||
rm -f "/dist/local_app_name"
|
||||
echo "" > /app/out.log
|
||||
echo "" > /app/err.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
|
||||
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
|
||||
else
|
||||
# Собираем приложение
|
||||
cd /app || exit 1
|
||||
rm -f "/app/local_app"
|
||||
rm -f "/dist/local_app_name"
|
||||
echo "" > /app/out.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
|
||||
/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
|
||||
|
||||
@ -19,7 +19,7 @@ if len(sys.argv) > 1 and sys.argv[1] == "debug":
|
||||
env["ARG1"] = "debug"
|
||||
|
||||
# Запускаем сервис через Docker compose
|
||||
subprocess.run(["docker", "compose", "up", "site", "-d"], env=env)
|
||||
subprocess.run(["docker", "compose", "up", "site", "-d", "--build"], env=env)
|
||||
|
||||
attempt_count = 0
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user