Compare commits

...

1 Commits

Author SHA1 Message Date
fac268e526 Эксперемент 2025-04-23 00:35:10 +03:00
78 changed files with 1839 additions and 1151 deletions

View File

@ -1 +0,0 @@
DB_PASSWORD=password

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.idea
.storage
.env

2
.storage/.gitignore vendored
View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -1,31 +0,0 @@
# 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
- заполнение базы данных тестовыми значениями консольной командой

View File

@ -1,5 +1,5 @@
out.log
err.log
pink_fox
dlv.log
err.log
local_app
out.log
pink_fox

15
application/README.md Normal file
View File

@ -0,0 +1,15 @@
[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 для этого

View File

@ -0,0 +1,6 @@
host: postgres_test
user: pink_fox
password: pink_fox_pass
port: 5432
database: pink_fox_db_test
migrations: /app/database/migrations

12
application/config.yml Normal file
View File

@ -0,0 +1,12 @@
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

View File

@ -0,0 +1,21 @@
-- +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

View File

@ -0,0 +1,5 @@
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');

View File

@ -2,19 +2,26 @@ 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.1 // indirect
github.com/bytedance/sonic v1.13.2 // 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/gin-gonic/gin v1.10.0 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.25.0 // indirect
github.com/go-playground/validator/v10 v10.26.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
@ -22,18 +29,23 @@ 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.3 // indirect
github.com/spf13/cobra v1.9.1 // 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/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // 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
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
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -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.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
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/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,6 +13,7 @@ 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=
@ -20,12 +21,14 @@ 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.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
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/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=
@ -43,15 +46,22 @@ 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.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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/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=
@ -66,24 +76,29 @@ 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=
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=
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
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=
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=

View File

@ -0,0 +1,23 @@
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
}

View File

@ -0,0 +1,20 @@
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
}

View File

@ -0,0 +1,71 @@
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
}

View File

@ -0,0 +1,29 @@
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
}

View File

@ -0,0 +1,21 @@
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
}

View File

@ -0,0 +1,48 @@
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
}

View File

@ -0,0 +1,43 @@
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
}

View File

@ -0,0 +1,56 @@
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
}

View File

@ -0,0 +1,18 @@
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
}

View File

@ -0,0 +1,57 @@
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()
}

View File

@ -0,0 +1,31 @@
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
}

View File

@ -0,0 +1,15 @@
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
}

View File

@ -0,0 +1,21 @@
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)
}

View File

@ -0,0 +1,27 @@
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
}

View File

@ -0,0 +1,149 @@
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
}

View File

@ -0,0 +1,11 @@
package view
type View struct {
SiteTitle string
}
func NewView() *View {
return &View{
SiteTitle: "FW - framework on golang",
}
}

42
application/main.go Normal file
View File

@ -0,0 +1,42 @@
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)
}
}

View File

@ -0,0 +1,5 @@
package fw
type BaseServices interface {
ResponseFactory() *ResponseFactory
}

View File

@ -0,0 +1,43 @@
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
}

View File

@ -0,0 +1,103 @@
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
}

View File

@ -0,0 +1,77 @@
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()
}

View File

@ -0,0 +1,115 @@
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)
}
}

View File

@ -0,0 +1,49 @@
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()))
}

View File

@ -0,0 +1,18 @@
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)
}

View File

@ -0,0 +1,8 @@
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)
}

View File

@ -0,0 +1,75 @@
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
}

View File

@ -0,0 +1,21 @@
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)
}

View File

@ -0,0 +1,49 @@
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
}

View File

@ -0,0 +1,132 @@
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
}

View File

@ -0,0 +1,224 @@
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()))
}
}

View File

@ -0,0 +1,7 @@
<h1>{{ .title }}</h1>
<p>{{ .message }}</p>
{{ if .error != "" }}
<pre>{{ .error }}</pre>
{{ end }}

View File

@ -0,0 +1,5 @@
<h1>{{ .title }}</h1>
{{ if .message != "" }}
<p>{{ .message }}</p>
{{ end }}

View File

@ -0,0 +1,3 @@
<h1>{{ vi.SiteTitle }}</h1>
<p>{{ .text }}</p>

77
application/ttt.go Normal file
View File

@ -0,0 +1,77 @@
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
View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -9,9 +9,8 @@ services:
- "12001:12001"
- "12002:2345"
volumes:
- ./pink_fox_app:/app
- ./pink_fox_app/log:/var/log/pink_fox
- ./dist:/dist
- ./application:/app
- ./application/log:/var/log/pink_fox
- ./environment:/var/environment/pink_fox
- ./.storage/go:/go
environment:
@ -31,3 +30,17 @@ 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"

View File

@ -1,15 +0,0 @@
# Порт для приложения
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

View File

@ -1,11 +0,0 @@
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);

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -1,20 +0,0 @@
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)
}
}

View File

@ -1,21 +0,0 @@
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,
}
}

View File

@ -1,47 +0,0 @@
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)
}

View File

@ -1,151 +0,0 @@
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
}

View File

@ -1,45 +0,0 @@
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
}

View File

@ -1,30 +0,0 @@
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)
}

View File

@ -1,17 +0,0 @@
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)
}

View File

@ -1,34 +0,0 @@
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)
}

View File

@ -1,95 +0,0 @@
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)
}

View File

@ -1,81 +0,0 @@
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)
}
}

View File

@ -1,34 +0,0 @@
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...)
}

View File

@ -1,192 +0,0 @@
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))
}

View File

@ -1,3 +0,0 @@
package timer
// TODO

View File

@ -1,3 +0,0 @@
package types
type MakeViewObject func(int16) any

View File

@ -1,30 +0,0 @@
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

View File

@ -1,33 +0,0 @@
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
}

View File

@ -1,25 +0,0 @@
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
}

View File

@ -1,24 +0,0 @@
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
}

View File

@ -1,41 +0,0 @@
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)
}

View File

@ -1,48 +0,0 @@
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
}

View File

@ -1,29 +0,0 @@
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)
}

View File

@ -1,21 +0,0 @@
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!",
}
}

View File

@ -1,16 +0,0 @@
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)
}
}

View File

@ -1,2 +0,0 @@
<h1>{{ vi.Title }}</h1>
<p>{{ it }}</p>

View File

@ -0,0 +1 @@
CREATE DATABASE pink_fox_db_test with owner pink_fox;

View File

@ -16,30 +16,30 @@ fi
if [ "$ARG1" = "debug" ]; then
# Собираем приложение для дебага
cd /app || exit 1
rm -f "/dist/local_app_name"
rm -f "/app/local_app"
echo "" > /app/out.log
echo "" > /app/err.log
echo "" > /app/dlv.log
pkill -f /dist/local_app_name
pkill -f /app/local_app
go build -gcflags "all=-N -l" -o /dist/local_app_name > /app/out.log 2> /app/err.log
go build -gcflags "all=-N -l" -o /app/local_app > /app/out.log 2> /app/err.log
if [ ! -s "/app/err.log" ]; then
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec /dist/local_app_name server > /app/out.log 2> /app/dlv.log
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec /app/local_app server > /app/out.log 2> /app/dlv.log
fi
else
# Собираем приложение
cd /app || exit 1
rm -f "/dist/local_app_name"
rm -f "/app/local_app"
echo "" > /app/out.log
echo "" > /app/err.log
pkill -f /dist/local_app_name
pkill -f /app/local_app
go build -o "/dist/local_app_name" > /app/out.log 2> /app/err.log
go build -o "/app/local_app" > /app/out.log 2> /app/err.log
if [ ! -s "/app/err.log" ]; then
/dist/local_app_name server > /app/out.log 2> /app/err.log
/app/local_app server > /app/out.log 2> /app/err.log
fi
fi

View File

@ -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", "--build"], env=env)
subprocess.run(["docker", "compose", "up", "site", "-d"], env=env)
attempt_count = 0