Эксперемент
This commit is contained in:
parent
3df4f7facb
commit
fac268e526
@ -1 +0,0 @@
|
|||||||
DB_PASSWORD=password
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.storage
|
||||||
.env
|
.env
|
||||||
2
.storage/.gitignore
vendored
2
.storage/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
||||||
31
README.md
31
README.md
@ -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
|
|
||||||
- заполнение базы данных тестовыми значениями консольной командой
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
out.log
|
|
||||||
err.log
|
|
||||||
pink_fox
|
|
||||||
dlv.log
|
dlv.log
|
||||||
|
err.log
|
||||||
|
local_app
|
||||||
|
out.log
|
||||||
|
pink_fox
|
||||||
15
application/README.md
Normal file
15
application/README.md
Normal 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 для этого
|
||||||
6
application/config-test.yml
Normal file
6
application/config-test.yml
Normal 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
12
application/config.yml
Normal 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
|
||||||
@ -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
|
||||||
5
application/database/seeders/users.sql
Normal file
5
application/database/seeders/users.sql
Normal 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');
|
||||||
@ -2,19 +2,26 @@ module pink_fox
|
|||||||
|
|
||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/spf13/cobra v1.9.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||||
github.com/CloudyKit/jet/v6 v6.3.1 // indirect
|
github.com/CloudyKit/jet/v6 v6.3.1 // indirect
|
||||||
github.com/bytedance/sonic v1.13.1 // indirect
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/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/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.25.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
@ -22,18 +29,23 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/spf13/cobra v1.9.1 // 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/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/arch v0.16.0 // indirect
|
||||||
golang.org/x/net v0.37.0 // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // 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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
@ -2,8 +2,8 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4s
|
|||||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||||
github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
|
github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
|
||||||
github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
|
github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
|
||||||
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
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.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
@ -13,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/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
@ -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-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
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/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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@ -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/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
|
||||||
|
github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
@ -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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
23
application/inner/commands/create_new_user_command.go
Normal file
23
application/inner/commands/create_new_user_command.go
Normal 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
|
||||||
|
}
|
||||||
20
application/inner/commands/user_command.go
Normal file
20
application/inner/commands/user_command.go
Normal 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
|
||||||
|
}
|
||||||
71
application/inner/config/config.org.go
Normal file
71
application/inner/config/config.org.go
Normal 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
|
||||||
|
}
|
||||||
29
application/inner/controllers/http_error_controller.go
Normal file
29
application/inner/controllers/http_error_controller.go
Normal 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
|
||||||
|
}
|
||||||
21
application/inner/controllers/ping_controller.go
Normal file
21
application/inner/controllers/ping_controller.go
Normal 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
|
||||||
|
}
|
||||||
48
application/inner/controllers/site_controller.go
Normal file
48
application/inner/controllers/site_controller.go
Normal 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
|
||||||
|
}
|
||||||
43
application/inner/di/di.go
Normal file
43
application/inner/di/di.go
Normal 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
|
||||||
|
}
|
||||||
56
application/inner/middlewares/ErrorMiddleware.go
Normal file
56
application/inner/middlewares/ErrorMiddleware.go
Normal 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
|
||||||
|
}
|
||||||
18
application/inner/middlewares/ResponseMiddlware.go
Normal file
18
application/inner/middlewares/ResponseMiddlware.go
Normal 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
|
||||||
|
}
|
||||||
57
application/inner/registration_routes.go
Normal file
57
application/inner/registration_routes.go
Normal 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()
|
||||||
|
}
|
||||||
31
application/inner/repositories/pg/users_repository.go
Normal file
31
application/inner/repositories/pg/users_repository.go
Normal 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
|
||||||
|
}
|
||||||
15
application/inner/repositories/pg/users_repository_test.go
Normal file
15
application/inner/repositories/pg/users_repository_test.go
Normal 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
|
||||||
|
}
|
||||||
21
application/inner/repositories/repositories.go
Normal file
21
application/inner/repositories/repositories.go
Normal 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)
|
||||||
|
}
|
||||||
27
application/inner/storage/storage.go
Normal file
27
application/inner/storage/storage.go
Normal 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
|
||||||
|
}
|
||||||
149
application/inner/test/test.go
Normal file
149
application/inner/test/test.go
Normal 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
|
||||||
|
}
|
||||||
11
application/inner/view/view.go
Normal file
11
application/inner/view/view.go
Normal 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
42
application/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
application/packages/fw/base.go
Normal file
5
application/packages/fw/base.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package fw
|
||||||
|
|
||||||
|
type BaseServices interface {
|
||||||
|
ResponseFactory() *ResponseFactory
|
||||||
|
}
|
||||||
43
application/packages/fw/cmd.go
Normal file
43
application/packages/fw/cmd.go
Normal 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
|
||||||
|
}
|
||||||
103
application/packages/fw/cmd_migrate.go
Normal file
103
application/packages/fw/cmd_migrate.go
Normal 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
|
||||||
|
}
|
||||||
77
application/packages/fw/cmd_server.go
Normal file
77
application/packages/fw/cmd_server.go
Normal 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()
|
||||||
|
}
|
||||||
115
application/packages/fw/error.go
Normal file
115
application/packages/fw/error.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
application/packages/fw/error_test.go
Normal file
49
application/packages/fw/error_test.go
Normal 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()))
|
||||||
|
}
|
||||||
18
application/packages/fw/fw.go
Normal file
18
application/packages/fw/fw.go
Normal 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)
|
||||||
|
}
|
||||||
8
application/packages/fw/logger.go
Normal file
8
application/packages/fw/logger.go
Normal 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)
|
||||||
|
}
|
||||||
75
application/packages/fw/migration.go
Normal file
75
application/packages/fw/migration.go
Normal 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
|
||||||
|
}
|
||||||
21
application/packages/fw/pg.go
Normal file
21
application/packages/fw/pg.go
Normal 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)
|
||||||
|
}
|
||||||
49
application/packages/fw/render.go
Normal file
49
application/packages/fw/render.go
Normal 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
|
||||||
|
}
|
||||||
132
application/packages/fw/response.go
Normal file
132
application/packages/fw/response.go
Normal 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
|
||||||
|
}
|
||||||
224
application/packages/fw/router_manager.go
Normal file
224
application/packages/fw/router_manager.go
Normal 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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
7
application/templates/errors/500.html
Normal file
7
application/templates/errors/500.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<h1>{{ .title }}</h1>
|
||||||
|
|
||||||
|
<p>{{ .message }}</p>
|
||||||
|
|
||||||
|
{{ if .error != "" }}
|
||||||
|
<pre>{{ .error }}</pre>
|
||||||
|
{{ end }}
|
||||||
5
application/templates/errors/error.html
Normal file
5
application/templates/errors/error.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<h1>{{ .title }}</h1>
|
||||||
|
|
||||||
|
{{ if .message != "" }}
|
||||||
|
<p>{{ .message }}</p>
|
||||||
|
{{ end }}
|
||||||
3
application/templates/index.html
Normal file
3
application/templates/index.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h1>{{ vi.SiteTitle }}</h1>
|
||||||
|
|
||||||
|
<p>{{ .text }}</p>
|
||||||
77
application/ttt.go
Normal file
77
application/ttt.go
Normal 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
2
dist/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
||||||
@ -9,9 +9,8 @@ services:
|
|||||||
- "12001:12001"
|
- "12001:12001"
|
||||||
- "12002:2345"
|
- "12002:2345"
|
||||||
volumes:
|
volumes:
|
||||||
- ./pink_fox_app:/app
|
- ./application:/app
|
||||||
- ./pink_fox_app/log:/var/log/pink_fox
|
- ./application/log:/var/log/pink_fox
|
||||||
- ./dist:/dist
|
|
||||||
- ./environment:/var/environment/pink_fox
|
- ./environment:/var/environment/pink_fox
|
||||||
- ./.storage/go:/go
|
- ./.storage/go:/go
|
||||||
environment:
|
environment:
|
||||||
@ -31,3 +30,17 @@ services:
|
|||||||
- ./services/postgres/init:/docker-entrypoint-initdb.d
|
- ./services/postgres/init:/docker-entrypoint-initdb.d
|
||||||
ports:
|
ports:
|
||||||
- "12003:5432"
|
- "12003:5432"
|
||||||
|
|
||||||
|
postgres_test:
|
||||||
|
image: postgres:16
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=pink_fox
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
- TZ=Europe/Moscow
|
||||||
|
- PGTZ=Europe/Moscow
|
||||||
|
volumes:
|
||||||
|
- ./services/postgres/init-test:/docker-entrypoint-initdb.d
|
||||||
|
tmpfs:
|
||||||
|
- /var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "12004:5432"
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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);
|
|
||||||
2
pink_fox_app/log/.gitignore
vendored
2
pink_fox_app/log/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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...)
|
|
||||||
}
|
|
||||||
@ -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))
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
package timer
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
type MakeViewObject func(int16) any
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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!",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
<h1>{{ vi.Title }}</h1>
|
|
||||||
<p>{{ it }}</p>
|
|
||||||
1
services/postgres/init-test/01-databases.sql
Normal file
1
services/postgres/init-test/01-databases.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
CREATE DATABASE pink_fox_db_test with owner pink_fox;
|
||||||
@ -16,30 +16,30 @@ fi
|
|||||||
if [ "$ARG1" = "debug" ]; then
|
if [ "$ARG1" = "debug" ]; then
|
||||||
# Собираем приложение для дебага
|
# Собираем приложение для дебага
|
||||||
cd /app || exit 1
|
cd /app || exit 1
|
||||||
rm -f "/dist/local_app_name"
|
rm -f "/app/local_app"
|
||||||
echo "" > /app/out.log
|
echo "" > /app/out.log
|
||||||
echo "" > /app/err.log
|
echo "" > /app/err.log
|
||||||
echo "" > /app/dlv.log
|
echo "" > /app/dlv.log
|
||||||
|
|
||||||
pkill -f /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
|
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
|
fi
|
||||||
else
|
else
|
||||||
# Собираем приложение
|
# Собираем приложение
|
||||||
cd /app || exit 1
|
cd /app || exit 1
|
||||||
rm -f "/dist/local_app_name"
|
rm -f "/app/local_app"
|
||||||
echo "" > /app/out.log
|
echo "" > /app/out.log
|
||||||
echo "" > /app/err.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
|
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
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -19,7 +19,7 @@ if len(sys.argv) > 1 and sys.argv[1] == "debug":
|
|||||||
env["ARG1"] = "debug"
|
env["ARG1"] = "debug"
|
||||||
|
|
||||||
# Запускаем сервис через Docker compose
|
# Запускаем сервис через Docker compose
|
||||||
subprocess.run(["docker", "compose", "up", "site", "-d", "--build"], env=env)
|
subprocess.run(["docker", "compose", "up", "site", "-d"], env=env)
|
||||||
|
|
||||||
attempt_count = 0
|
attempt_count = 0
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user