diff --git a/.env.dist b/.env.dist index 06af3b6..9e416b8 100644 --- a/.env.dist +++ b/.env.dist @@ -4,3 +4,4 @@ GOVERTER_VER=v1.4.0 GOLANGCI_LINT_VER=v1.56.2 POSTGRES_VER=16.2 AIR_VER=1.52.0 +SQLC_VER=1.26.0 diff --git a/Dockerfile b/Dockerfile index 358e3ee..000faa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ARG ALPINE_VER FROM postgres:${POSTGRES_VER} AS postgres -COPY ./db/* /docker-entrypoint-initdb.d/ +COPY ./string-unpack/internal/db/* /docker-entrypoint-initdb.d/ RUN set -ex \ && chmod 1777 /tmp diff --git a/Taskfile.yaml b/Taskfile.yaml index cc19078..ac011e0 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -38,6 +38,7 @@ tasks: generate: desc: all generators + cs. cmds: + - task: sqlc - task: goverter - task: go:generate - task: cs @@ -124,6 +125,16 @@ tasks: sources: - ./**/*.go + sqlc: + desc: go run sqlc. + deps: [ dotenv ] + cmds: + - go run github.com/sqlc-dev/sqlc/cmd/sqlc@v{{.SQLC_VER}} generate + sources: + - sqlc.yaml + - ./**/sql/**/* + - ./**/db/**/* + # ========= # OPERATION # ========= @@ -147,7 +158,7 @@ tasks: cmds: - docker compose build postgres sources: - - db/**/* + - ./**/db/**/* - Dockerfile - docker-compose.yml diff --git a/cmd/api/config.go b/cmd/api/config.go new file mode 100644 index 0000000..2ea894a --- /dev/null +++ b/cmd/api/config.go @@ -0,0 +1,24 @@ +package main + +import ( + "flag" + "time" + + "github.com/kelseyhightower/envconfig" +) + +type Config struct { + DSN string `envconfig:"DSN" default:"postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable"` + ReconnectTimeout time.Duration `envconfig:"RECONNECT_TIMEOUT" default:"2s"` +} + +func (c *Config) Parse() error { + flag.StringVar(&c.DSN, "DSN", "", "postgresql DSN") + flag.Parse() + + if err := envconfig.Process("", c); err != nil { + return err + } + + return nil +} diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..6ea66a3 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "errors" + "net/http" + "os/signal" + "syscall" + "time" + + "github.com/jackc/pgx/v5" + + stringunpack "git.grachevko.ru/grachevko/h/string-unpack" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + collector "github.com/prometheus/client_golang/prometheus/collectors/version" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" +) + +func main() { + logger, _ := zap.NewProduction() + defer func(logger *zap.Logger) { + _ = logger.Sync() + }(logger) // flushes buffer, if any + + cfg := Config{} + if err := cfg.Parse(); err != nil { + logger.Fatal("config parse", zap.Error(err)) + } + + ctx := context.Background() + + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer stop() + + var metricServer *http.Server + { // Metrics + prometheus.MustRegister(collector.NewCollector("unpack")) + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + metricServer = &http.Server{ + Handler: mux, + Addr: ":8089", + } + + go func() { + if err := metricServer.ListenAndServe(); err != nil { + logger.Error("metric server failed", zap.Error(err)) + } + }() + } // Metrics + +pgconn: + conn, err := pgx.Connect(ctx, cfg.DSN) + if err != nil { + logger.Error("pgx", zap.Error(err)) + + logger.Info("Wait before reconnect", zap.Duration("after", cfg.ReconnectTimeout)) + <-time.After(cfg.ReconnectTimeout) + logger.Info("Retrying connection") + goto pgconn + } + defer func(conn *pgx.Conn, ctx context.Context) { + if err = conn.Close(ctx); err != nil { + logger.Error("pgx close", zap.Error(err)) + } + }(conn, ctx) + + var httpServer *http.Server + { // Http + r := gin.Default() + + stringunpack.Setup(ctx, logger, r.Group("unpack"), conn) + + httpServer = &http.Server{ + Addr: ":8080", + Handler: r, + } + + go func() { + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error("http server failed", zap.Error(err)) + } + }() + } // Http + + // Listen for the interrupt signal. + <-ctx.Done() + + { // Graceful shutdown + // Restore default behavior on the interrupt signal and notify user of shutdown. + stop() + logger.Info("shutting down gracefully, press Ctrl+C again to force") + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { return metricServer.Shutdown(ctx) }) + g.Go(func() error { return httpServer.Shutdown(ctx) }) + + if err := g.Wait(); err != nil { + logger.Error("graceful shutdown failed", zap.Error(err)) + } + } // Graceful shutdown + + logger.Info("Server exiting") +} diff --git a/db/.gitkeep b/db/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/go.mod b/go.mod index d070bb3..9f1719c 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,53 @@ module git.grachevko.ru/grachevko/h -go 1.22.2 +go 1.22.3 require ( + github.com/gin-gonic/gin v1.10.0 + github.com/jackc/pgx/v5 v5.5.5 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/prometheus/client_golang v1.19.0 github.com/stretchr/testify v1.9.0 + go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/sync v0.7.0 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // 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.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.53.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bb8106c..9a7fe6f 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,126 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= +github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..62a4f85 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,14 @@ +version: "2" +sql: +- engine: "postgresql" + queries: "string-unpack/internal/sql/" + schema: "string-unpack/internal/db/schema.sql" + gen: + go: + out: "string-unpack/internal/sql" + sql_package: "pgx/v5" + emit_db_tags: true + emit_interface: true + emit_json_tags: true + emit_methods_with_db_argument: false + query_parameter_limit: 0 diff --git a/string-unpack/internal/db/schema.sql b/string-unpack/internal/db/schema.sql new file mode 100644 index 0000000..d081a21 --- /dev/null +++ b/string-unpack/internal/db/schema.sql @@ -0,0 +1,10 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE unpack_history +( + id uuid default gen_random_uuid() primary key, + input text NOT NULL, + result text NOT NULL, + created_at timestamptz DEFAULT NOW() +) +; diff --git a/string-unpack/internal/sql/db.go b/string-unpack/internal/sql/db.go new file mode 100644 index 0000000..c4b45fb --- /dev/null +++ b/string-unpack/internal/sql/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package sql + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/string-unpack/internal/sql/insert.sql b/string-unpack/internal/sql/insert.sql new file mode 100644 index 0000000..5e8236d --- /dev/null +++ b/string-unpack/internal/sql/insert.sql @@ -0,0 +1,9 @@ +-- name: Insert :one +INSERT INTO unpack_history (input, result) +VALUES (@input, @result) +RETURNING + id, + input, + result, + created_at +; diff --git a/string-unpack/internal/sql/insert.sql.go b/string-unpack/internal/sql/insert.sql.go new file mode 100644 index 0000000..a24356f --- /dev/null +++ b/string-unpack/internal/sql/insert.sql.go @@ -0,0 +1,37 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: insert.sql + +package sql + +import ( + "context" +) + +const insert = `-- name: Insert :one +INSERT INTO unpack_history (input, result) +VALUES ($1, $2) +RETURNING + id, + input, + result, + created_at +` + +type InsertParams struct { + Input string `db:"input" json:"input"` + Result string `db:"result" json:"result"` +} + +func (q *Queries) Insert(ctx context.Context, arg InsertParams) (UnpackHistory, error) { + row := q.db.QueryRow(ctx, insert, arg.Input, arg.Result) + var i UnpackHistory + err := row.Scan( + &i.ID, + &i.Input, + &i.Result, + &i.CreatedAt, + ) + return i, err +} diff --git a/string-unpack/internal/sql/latest.sql b/string-unpack/internal/sql/latest.sql new file mode 100644 index 0000000..a6d05f8 --- /dev/null +++ b/string-unpack/internal/sql/latest.sql @@ -0,0 +1,9 @@ +-- name: Latest :many +SELECT id, + input, + result, + created_at +FROM unpack_history +ORDER BY created_at DESC +LIMIT 15 +; diff --git a/string-unpack/internal/sql/latest.sql.go b/string-unpack/internal/sql/latest.sql.go new file mode 100644 index 0000000..7aca412 --- /dev/null +++ b/string-unpack/internal/sql/latest.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: latest.sql + +package sql + +import ( + "context" +) + +const latest = `-- name: Latest :many +SELECT id, + input, + result, + created_at +FROM unpack_history +ORDER BY created_at DESC +LIMIT 15 +` + +func (q *Queries) Latest(ctx context.Context) ([]UnpackHistory, error) { + rows, err := q.db.Query(ctx, latest) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UnpackHistory + for rows.Next() { + var i UnpackHistory + if err := rows.Scan( + &i.ID, + &i.Input, + &i.Result, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/string-unpack/internal/sql/models.go b/string-unpack/internal/sql/models.go new file mode 100644 index 0000000..941035f --- /dev/null +++ b/string-unpack/internal/sql/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package sql + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type UnpackHistory struct { + ID pgtype.UUID `db:"id" json:"id"` + Input string `db:"input" json:"input"` + Result string `db:"result" json:"result"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` +} diff --git a/string-unpack/internal/sql/querier.go b/string-unpack/internal/sql/querier.go new file mode 100644 index 0000000..acd6473 --- /dev/null +++ b/string-unpack/internal/sql/querier.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package sql + +import ( + "context" +) + +type Querier interface { + Insert(ctx context.Context, arg InsertParams) (UnpackHistory, error) + Latest(ctx context.Context) ([]UnpackHistory, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/string-unpack/server.go b/string-unpack/server.go new file mode 100644 index 0000000..66778db --- /dev/null +++ b/string-unpack/server.go @@ -0,0 +1,78 @@ +package stringunpack + +import ( + "context" + "errors" + "fmt" + "net/http" + + "git.grachevko.ru/grachevko/h/string-unpack/internal/sql" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5" + "go.uber.org/zap" +) + +var queries sql.Querier + +func Setup(ctx context.Context, logger *zap.Logger, r *gin.RouterGroup, conn *pgx.Conn) { + if queries != nil { + panic("Setup must call only once") + } + + queries = sql.New(conn) + + unpack := func(input string) (r string, err error) { + r, err = Unpack(input) + + { // history + result := r + if err != nil { + result = fmt.Sprintf("err: %s", err.Error()) + } + _, err := queries.Insert(ctx, sql.InsertParams{ + Input: input, + Result: result, + }) + if err != nil { + logger.Error("pg insert", zap.Error(err)) + } + } // history + + return + } + + r.GET("history", func(c *gin.Context) { + latest, err := queries.Latest(ctx) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "err": err.Error(), + }) + + return + } + + c.JSON(http.StatusOK, latest) + }) + + r.GET(":s", func(c *gin.Context) { + s := c.Param("s") + + result, err := unpack(s) + if errors.Is(err, ErrIncorrectString) { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "err": err.Error(), + }) + + return + } + if err != nil { + logger.Error("unpack", zap.Error(err)) + + c.AbortWithStatus(http.StatusInternalServerError) + } + + c.JSON(http.StatusOK, gin.H{ + "result": result, + }) + }) +}