Compare commits
1 Commits
master
...
299d11d510
Author | SHA1 | Date | |
---|---|---|---|
299d11d510
|
@@ -1,7 +0,0 @@
|
|||||||
COMPOSE_PROJECT_NAME=h-skills
|
|
||||||
ALPINE_VER=3.19
|
|
||||||
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
|
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
/.task
|
|
||||||
/.env
|
|
||||||
/bin/env
|
|
||||||
/tmp/
|
|
@@ -1,2 +0,0 @@
|
|||||||
# golangci-lint configuration file.
|
|
||||||
# Read more at: https://github.com/golangci/golangci-lint#config-file
|
|
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
|||||||
ARG POSTGRES_VER
|
|
||||||
ARG GO_VER
|
|
||||||
ARG ALPINE_VER
|
|
||||||
|
|
||||||
FROM postgres:${POSTGRES_VER} AS postgres
|
|
||||||
|
|
||||||
COPY ./string-unpack/internal/db/* /docker-entrypoint-initdb.d/
|
|
||||||
|
|
||||||
RUN set -ex \
|
|
||||||
&& chmod 1777 /tmp
|
|
||||||
|
|
||||||
FROM golang:${GO_VER}-alpine${ALPINE_VER} AS golang
|
|
||||||
|
|
||||||
ARG AIR_VER
|
|
||||||
RUN set -ex \
|
|
||||||
&& chmod 1777 /tmp \
|
|
||||||
&& apk add --no-cache \
|
|
||||||
git \
|
|
||||||
tzdata \
|
|
||||||
&& go install github.com/cosmtrek/air@v${AIR_VER}
|
|
129
Taskfile.go.yaml
129
Taskfile.go.yaml
@@ -1,129 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
GOFUMPT_VER: '{{.GOFUMPT_VER | default "latest"}}'
|
|
||||||
GOIMPORTS_LOCAL: '{{.GOIMPORTS_LOCAL | default nil}}'
|
|
||||||
GOIMPORT_VER: '{{.GOIMPORT_VER | default "latest"}}'
|
|
||||||
GOLANGCI_LINT_VER: '{{.GOLANGCI_LINT_VER | default "latest"}}'
|
|
||||||
GOVULNCHECK_VER: '{{.GOVULNCHECK_VER | default "latest"}}'
|
|
||||||
PROTOLINT_VER: '{{.PROTOLINT_VER | default "latest"}}'
|
|
||||||
STATICCHECK_VER: '{{.STATICCHECK_VER | default "latest"}}'
|
|
||||||
TMP_DIR: '{{.TMP_DIR | default "./tmp"}}'
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
# =======
|
|
||||||
# HELPERS
|
|
||||||
# =======
|
|
||||||
|
|
||||||
lint:
|
|
||||||
desc: golangci-lint + protolint.
|
|
||||||
cmds:
|
|
||||||
- task: govulncheck
|
|
||||||
- task: staticcheck
|
|
||||||
- task: golangci-lint
|
|
||||||
- task: protolint
|
|
||||||
|
|
||||||
cs:
|
|
||||||
desc: fmt + goimports.
|
|
||||||
cmds:
|
|
||||||
- task: fmt
|
|
||||||
- task: goimports
|
|
||||||
|
|
||||||
# ===============
|
|
||||||
# QUALITY CONTROL
|
|
||||||
# ===============
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
desc: format code using gofumpt.
|
|
||||||
cmds:
|
|
||||||
- >-
|
|
||||||
go run mvdan.cc/gofumpt@{{.GOFUMPT_VER}} -l -w .
|
|
||||||
sources:
|
|
||||||
- ./**/*.go
|
|
||||||
|
|
||||||
goimports:
|
|
||||||
desc: format imports using goimports.
|
|
||||||
cmds:
|
|
||||||
- >-
|
|
||||||
go run golang.org/x/tools/cmd/goimports@{{.GOIMPORT_VER}}
|
|
||||||
{{if .GOIMPORTS_LOCAL}} -local="{{.GOIMPORTS_LOCAL}}"{{end}}
|
|
||||||
-w .
|
|
||||||
sources:
|
|
||||||
- ./**/*.go
|
|
||||||
|
|
||||||
tidy:
|
|
||||||
desc: go mod tidy + go mod verify.
|
|
||||||
cmds:
|
|
||||||
- go mod tidy -v
|
|
||||||
- go mod verify
|
|
||||||
sources:
|
|
||||||
- ./**/*.go
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
|
|
||||||
golangci-lint:
|
|
||||||
desc: golangci-lint.
|
|
||||||
cmds:
|
|
||||||
- go run github.com/golangci/golangci-lint/cmd/golangci-lint@{{.GOLANGCI_LINT_VER}} run -v ./...
|
|
||||||
sources:
|
|
||||||
- ./**/*.go
|
|
||||||
|
|
||||||
govulncheck:
|
|
||||||
desc: govulncheck.
|
|
||||||
cmds:
|
|
||||||
- go run golang.org/x/vuln/cmd/govulncheck@{{.GOVULNCHECK_VER}} ./...
|
|
||||||
sources:
|
|
||||||
- ./**/*.go
|
|
||||||
|
|
||||||
staticcheck:
|
|
||||||
desc: staticcheck.
|
|
||||||
cmds:
|
|
||||||
- go run honnef.co/go/tools/cmd/staticcheck@{{.STATICCHECK_VER}} -checks=all,-ST1000 ./...
|
|
||||||
sources:
|
|
||||||
- ./**/*.go
|
|
||||||
|
|
||||||
protolint:
|
|
||||||
desc: protolint -fix.
|
|
||||||
cmds:
|
|
||||||
- cmd: go run github.com/yoheimuta/protolint/cmd/protolint@{{.PROTOLINT_VER}} lint -fix .
|
|
||||||
ignore_error: true
|
|
||||||
sources: &protolint_sources
|
|
||||||
- ./**/*.proto
|
|
||||||
|
|
||||||
protolint:check:
|
|
||||||
desc: protolint check.
|
|
||||||
cmds:
|
|
||||||
- go run github.com/yoheimuta/protolint/cmd/protolint@{{.PROTOLINT_VER}} lint .
|
|
||||||
sources: *protolint_sources
|
|
||||||
|
|
||||||
# ===========
|
|
||||||
# DEVELOPMENT
|
|
||||||
# ===========
|
|
||||||
|
|
||||||
generate:
|
|
||||||
desc: go generate
|
|
||||||
cmds:
|
|
||||||
- go generate ./...
|
|
||||||
sources:
|
|
||||||
- ./**/*.go
|
|
||||||
|
|
||||||
vendor:
|
|
||||||
desc: go mod vendor.
|
|
||||||
cmds:
|
|
||||||
- go mod vendor -v
|
|
||||||
sources:
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
generates:
|
|
||||||
- vendor
|
|
||||||
|
|
||||||
test:
|
|
||||||
desc: go tests ./...
|
|
||||||
cmds:
|
|
||||||
- go test -race -buildvcs -coverprofile={{.TMP_DIR}}/coverage.out -vet=off ./...
|
|
||||||
|
|
||||||
test:cover:
|
|
||||||
desc: tests and display coverage.
|
|
||||||
deps: [ test ]
|
|
||||||
cmds:
|
|
||||||
- go tool cover -html={{.TMP_DIR}}/coverage.out
|
|
192
Taskfile.yaml
192
Taskfile.yaml
@@ -1,192 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
includes:
|
|
||||||
go:
|
|
||||||
taskfile: ./Taskfile.go.yaml
|
|
||||||
|
|
||||||
output: prefixed
|
|
||||||
|
|
||||||
silent: true
|
|
||||||
interval: 1s
|
|
||||||
|
|
||||||
dotenv:
|
|
||||||
- ./.env
|
|
||||||
|
|
||||||
vars:
|
|
||||||
NEWLINE_EXCLUDE: >-
|
|
||||||
-not -path "./.git/*"
|
|
||||||
-not -path "./.idea/*"
|
|
||||||
-not -path "./vendor/*"
|
|
||||||
-not -path "./tmp/*"
|
|
||||||
-not -name "*.ico"
|
|
||||||
-not -name "*.png"
|
|
||||||
-not -name "*.svg"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
# =======
|
|
||||||
# HELPERS
|
|
||||||
# =======
|
|
||||||
|
|
||||||
all:
|
|
||||||
desc: generate + tidy + lint + test.
|
|
||||||
cmds:
|
|
||||||
- task: generate
|
|
||||||
- task: go:tidy
|
|
||||||
- task: go:lint
|
|
||||||
- task: go:test
|
|
||||||
|
|
||||||
generate:
|
|
||||||
desc: all generators + cs.
|
|
||||||
cmds:
|
|
||||||
- task: sqlc
|
|
||||||
- task: goverter
|
|
||||||
- task: go:generate
|
|
||||||
- task: cs
|
|
||||||
|
|
||||||
cs:
|
|
||||||
desc: go:cs + newline.
|
|
||||||
cmds:
|
|
||||||
- task: go:cs
|
|
||||||
- task: newline
|
|
||||||
|
|
||||||
# ===============
|
|
||||||
# QUALITY CONTROL
|
|
||||||
# ===============
|
|
||||||
|
|
||||||
newline:
|
|
||||||
desc: add newline at end file.
|
|
||||||
cmds:
|
|
||||||
- >-
|
|
||||||
find .
|
|
||||||
{{.NEWLINE_EXCLUDE}}
|
|
||||||
-type f -exec grep -Iq . {} \; -and -print0
|
|
||||||
| xargs -0 -n 1 sh -c 'test -z "$(tail -c 1 "$0")"
|
|
||||||
|| (echo "" >> $0 && echo $0)'
|
|
||||||
|| exit 1
|
|
||||||
sources: &newline_sources
|
|
||||||
- ./**/*
|
|
||||||
- exclude: "*.ico"
|
|
||||||
- exclude: "*.png"
|
|
||||||
- exclude: "*.svg"
|
|
||||||
|
|
||||||
newline:check:
|
|
||||||
desc: check newline at end file.
|
|
||||||
cmds:
|
|
||||||
- find .
|
|
||||||
{{.NEWLINE_EXCLUDE}}
|
|
||||||
-type f -exec grep -Iq . {} \; -and -print0
|
|
||||||
| xargs -0 -n 1 sh -c 'test -z "$(tail -c 1 "$0")"
|
|
||||||
|| (echo "No new line at end of $0" && exit 1)'
|
|
||||||
|| exit 1
|
|
||||||
sources: *newline_sources
|
|
||||||
|
|
||||||
# ===========
|
|
||||||
# DEVELOPMENT
|
|
||||||
# ===========
|
|
||||||
|
|
||||||
dotenv:install:
|
|
||||||
internal: true
|
|
||||||
cmds:
|
|
||||||
- mkdir -p bin
|
|
||||||
- curl -o bin/env https://raw.githubusercontent.com/bashup/dotenv/master/dotenv
|
|
||||||
- chmod +x bin/env
|
|
||||||
status:
|
|
||||||
- test -f bin/env
|
|
||||||
|
|
||||||
dotenv:
|
|
||||||
deps: [ dotenv:install ]
|
|
||||||
desc: fill .env file.
|
|
||||||
vars:
|
|
||||||
UID:
|
|
||||||
sh: id -u
|
|
||||||
GID:
|
|
||||||
sh: id -g
|
|
||||||
GO_VER:
|
|
||||||
sh: go mod edit -json | jq -r .Go
|
|
||||||
GOIMPORTS_LOCAL:
|
|
||||||
sh: go list -m | awk -F/ '{print $1}'
|
|
||||||
ENV_DIST:
|
|
||||||
sh: cat .env.dist|tr '\n' ' '
|
|
||||||
GOPATH:
|
|
||||||
sh: go env GOPATH
|
|
||||||
cmds:
|
|
||||||
- bin/env set GROUP_ID={{.UID}} USER_ID={{.GID}} GO_VER={{.GO_VER}} GOPATH={{.GOPATH}} {{.ENV_DIST}}
|
|
||||||
sources:
|
|
||||||
- .env.dist
|
|
||||||
- Taskfile.yaml
|
|
||||||
generates:
|
|
||||||
- .env
|
|
||||||
|
|
||||||
goverter:
|
|
||||||
desc: go run goverter
|
|
||||||
deps: [ dotenv ]
|
|
||||||
cmds:
|
|
||||||
- go run github.com/jmattheis/goverter/cmd/goverter@{{.GOVERTER_VER}} gen ./...
|
|
||||||
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
|
|
||||||
# =========
|
|
||||||
|
|
||||||
up:
|
|
||||||
desc: up all services.
|
|
||||||
cmds:
|
|
||||||
- task: postgres:up
|
|
||||||
- task: api:up
|
|
||||||
|
|
||||||
down:
|
|
||||||
desc: docker compose down.
|
|
||||||
deps: [ dotenv ]
|
|
||||||
cmds:
|
|
||||||
- docker compose down -v --remove-orphans
|
|
||||||
|
|
||||||
postgres:build:
|
|
||||||
internal: true
|
|
||||||
desc: docker compose build postgres.
|
|
||||||
deps: [ dotenv ]
|
|
||||||
cmds:
|
|
||||||
- docker compose build postgres
|
|
||||||
sources:
|
|
||||||
- ./**/db/**/*
|
|
||||||
- Dockerfile
|
|
||||||
- docker-compose.yml
|
|
||||||
|
|
||||||
postgres:up:
|
|
||||||
desc: docker compose up postgres.
|
|
||||||
deps: [ dotenv, postgres:build ]
|
|
||||||
cmds:
|
|
||||||
- docker compose up -d --force-recreate postgres
|
|
||||||
|
|
||||||
postgres:push:
|
|
||||||
desc: docker compose push postgres.
|
|
||||||
deps: [ dotenv, postgres:build ]
|
|
||||||
cmds:
|
|
||||||
- docker compose push postgres
|
|
||||||
|
|
||||||
api:build:
|
|
||||||
internal: true
|
|
||||||
desc: docker compose build api image.
|
|
||||||
deps: [ dotenv ]
|
|
||||||
cmds:
|
|
||||||
- docker compose build api
|
|
||||||
sources:
|
|
||||||
- .env
|
|
||||||
- Dockerfile
|
|
||||||
- docker-compose.yml
|
|
||||||
|
|
||||||
api:up:
|
|
||||||
desc: docker compose up api.
|
|
||||||
deps: [ dotenv, api:build ]
|
|
||||||
cmds:
|
|
||||||
- docker compose up -d --force-recreate api
|
|
@@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
113
cmd/api/main.go
113
cmd/api/main.go
@@ -1,113 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
@@ -1,26 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "flag"
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Key int
|
|
||||||
Numeric bool
|
|
||||||
Reverse bool
|
|
||||||
Unique bool
|
|
||||||
Sources []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ParseFlags() {
|
|
||||||
flag.IntVar(&c.Key, "k", 0, "sort via column")
|
|
||||||
flag.BoolVar(&c.Numeric, "n", false, "compare according to string numerical value")
|
|
||||||
flag.BoolVar(&c.Reverse, "r", false, "reverse the result of comparisons")
|
|
||||||
flag.BoolVar(&c.Unique, "u", false, "output only the first of an equal run")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
c.Sources = flag.Args()
|
|
||||||
|
|
||||||
if len(c.Sources) == 0 {
|
|
||||||
c.Sources = []string{"-"}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
sortcli "git.grachevko.ru/grachevko/h/sort-cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
result := run()
|
|
||||||
|
|
||||||
if _, err := os.Stdout.WriteString(result); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func run() string {
|
|
||||||
cfg := &Config{}
|
|
||||||
cfg.ParseFlags()
|
|
||||||
|
|
||||||
lines := sortcli.Content{}
|
|
||||||
lines.Load(sortcli.Open(cfg.Sources))
|
|
||||||
|
|
||||||
if cfg.Unique {
|
|
||||||
lines.Uniques()
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.Sort(cfg.Reverse)
|
|
||||||
|
|
||||||
return lines.String()
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
services:
|
|
||||||
postgres:
|
|
||||||
build:
|
|
||||||
target: postgres
|
|
||||||
context: .
|
|
||||||
args: &args
|
|
||||||
AIR_VER: ${AIR_VER}
|
|
||||||
ALPINE_VER: ${ALPINE_VER}
|
|
||||||
GO_VER: ${GO_VER}
|
|
||||||
POSTGRES_VER: ${POSTGRES_VER}
|
|
||||||
image: harbor.grachevko.ru/grachevko/h-skills/postgres:dev
|
|
||||||
labels:
|
|
||||||
ru.grachevko.dhu: 'postgres.h-skills.local'
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: postgres
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
PGDATABASE: postgres
|
|
||||||
PGUSER: postgres
|
|
||||||
PGPASSWORD: postgres
|
|
||||||
volumes:
|
|
||||||
- type: tmpfs
|
|
||||||
target: /var/lib/postgresql/data
|
|
||||||
tmpfs:
|
|
||||||
size: 4294967296
|
|
||||||
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
target: golang
|
|
||||||
args: *args
|
|
||||||
command: air --build.cmd "go build -o ./tmp/api ./cmd/api/" --build.bin "./tmp/api"
|
|
||||||
labels:
|
|
||||||
ru.grachevko.dhu: 'api.h-skills.local'
|
|
||||||
environment:
|
|
||||||
GOPATH: ${GOPATH}
|
|
||||||
working_dir: /app
|
|
||||||
volumes:
|
|
||||||
- ./:/app
|
|
||||||
- ${GOPATH}:${GOPATH}
|
|
||||||
- ${HOME}/.cache/go-build:/.cache/go-build
|
|
||||||
user: ${USER_ID}:${GROUP_ID}
|
|
53
go.mod
53
go.mod
@@ -1,53 +0,0 @@
|
|||||||
module git.grachevko.ru/grachevko/h
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
126
go.sum
126
go.sum
@@ -1,126 +0,0 @@
|
|||||||
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=
|
|
||||||
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=
|
|
48
lru/Makefile
Normal file
48
lru/Makefile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
# ==================================================================================== #
|
||||||
|
# HELPERS
|
||||||
|
# ==================================================================================== #
|
||||||
|
|
||||||
|
## help: print this help message
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo 'Usage:'
|
||||||
|
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
|
||||||
|
|
||||||
|
|
||||||
|
# ==================================================================================== #
|
||||||
|
# QUALITY CONTROL
|
||||||
|
# ==================================================================================== #
|
||||||
|
|
||||||
|
## tidy: format code and tidy modfile
|
||||||
|
.PHONY: tidy
|
||||||
|
tidy:
|
||||||
|
go fmt ./...
|
||||||
|
go mod tidy -v
|
||||||
|
|
||||||
|
## audit: run quality control checks
|
||||||
|
.PHONY: audit
|
||||||
|
audit:
|
||||||
|
go mod verify
|
||||||
|
go vet ./...
|
||||||
|
go run honnef.co/go/tools/cmd/staticcheck@latest -checks=all,-ST1000,-U1000 ./...
|
||||||
|
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||||
|
go test -race -buildvcs -vet=off ./...
|
||||||
|
go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run -v ./...
|
||||||
|
|
||||||
|
|
||||||
|
# ==================================================================================== #
|
||||||
|
# DEVELOPMENT
|
||||||
|
# ==================================================================================== #
|
||||||
|
|
||||||
|
## test: run all tests
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -v -race -buildvcs ./...
|
||||||
|
|
||||||
|
## test/cover: run all tests and display coverage
|
||||||
|
.PHONY: test/cover
|
||||||
|
test/cover:
|
||||||
|
go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./...
|
||||||
|
go tool cover -html=/tmp/coverage.out
|
11
lru/go.mod
Normal file
11
lru/go.mod
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module lru
|
||||||
|
|
||||||
|
go 1.21.6
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.8.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
10
lru/go.sum
Normal file
10
lru/go.sum
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@@ -12,7 +12,7 @@ type LRU[T any] struct {
|
|||||||
}
|
}
|
||||||
type item[T any] struct {
|
type item[T any] struct {
|
||||||
key string
|
key string
|
||||||
value T
|
value *T
|
||||||
eol time.Time
|
eol time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ func (lru *LRU[T]) Set(key string, value T, ttl time.Duration) (evicted bool) {
|
|||||||
if element, ok := lru.items[key]; ok {
|
if element, ok := lru.items[key]; ok {
|
||||||
lru.queue.MoveToFront(element)
|
lru.queue.MoveToFront(element)
|
||||||
item := element.Value.(*item[T])
|
item := element.Value.(*item[T])
|
||||||
item.value = value
|
item.value = &value
|
||||||
item.eol = eol
|
item.eol = eol
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -43,7 +43,7 @@ func (lru *LRU[T]) Set(key string, value T, ttl time.Duration) (evicted bool) {
|
|||||||
|
|
||||||
item := &item[T]{
|
item := &item[T]{
|
||||||
key: key,
|
key: key,
|
||||||
value: value,
|
value: &value,
|
||||||
eol: eol,
|
eol: eol,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ func (lru *LRU[T]) Get(name string) (value T, ok bool) {
|
|||||||
|
|
||||||
lru.queue.MoveToFront(element)
|
lru.queue.MoveToFront(element)
|
||||||
|
|
||||||
return item.value, true
|
return *item.value, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lru *LRU[T]) evict() {
|
func (lru *LRU[T]) evict() {
|
||||||
|
@@ -1,11 +1,9 @@
|
|||||||
package lru
|
package lru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTtl(t *testing.T) {
|
func TestTtl(t *testing.T) {
|
||||||
@@ -41,7 +39,7 @@ func TestEvictExpired(t *testing.T) {
|
|||||||
assert.False(t, cache.Set("first", 1, 1000*time.Millisecond))
|
assert.False(t, cache.Set("first", 1, 1000*time.Millisecond))
|
||||||
assert.False(t, cache.Set("second", 2, 100*time.Millisecond))
|
assert.False(t, cache.Set("second", 2, 100*time.Millisecond))
|
||||||
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200*time.Millisecond)
|
||||||
|
|
||||||
assert.True(t, cache.Set("third", 3, 1000*time.Millisecond))
|
assert.True(t, cache.Set("third", 3, 1000*time.Millisecond))
|
||||||
|
|
||||||
@@ -60,7 +58,7 @@ func TestSetExisted(t *testing.T) {
|
|||||||
assert.False(t, cache.Set("first", 1, 200*time.Millisecond))
|
assert.False(t, cache.Set("first", 1, 200*time.Millisecond))
|
||||||
assert.False(t, cache.Set("second", 2, 500*time.Millisecond))
|
assert.False(t, cache.Set("second", 2, 500*time.Millisecond))
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100*time.Millisecond)
|
||||||
|
|
||||||
assert.False(t, cache.Set("first", 11, 1000*time.Millisecond))
|
assert.False(t, cache.Set("first", 11, 1000*time.Millisecond))
|
||||||
assert.True(t, cache.Set("third", 3, 1000*time.Millisecond))
|
assert.True(t, cache.Set("third", 3, 1000*time.Millisecond))
|
||||||
@@ -74,27 +72,7 @@ func TestSetExisted(t *testing.T) {
|
|||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDummy(t *testing.T) { // for coverage 100% coverage
|
func TestDummy(t *testing.T) { // for coverage 100% coverage
|
||||||
cache := New[int](0)
|
cache := New[int](0)
|
||||||
cache.evict()
|
cache.evict()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Benchmark(b *testing.B) {
|
|
||||||
cache := New[string](10)
|
|
||||||
ttl := 1000 * time.Millisecond
|
|
||||||
|
|
||||||
b.Run("Set", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
cache.Set(fmt.Sprintf("item:%d", i), fmt.Sprintf("item:%d", i), ttl)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("Get", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
value, ok := cache.Get(fmt.Sprintf("item:%d", i))
|
|
||||||
if ok {
|
|
||||||
_ = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@@ -1,38 +0,0 @@
|
|||||||
package mergechannel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrInsufficientChannels = errors.New("channels count must be >2")
|
|
||||||
|
|
||||||
func merge[T any](channels ...<-chan T) (<-chan T, error) {
|
|
||||||
out := make(chan T)
|
|
||||||
|
|
||||||
if len(channels) < 2 {
|
|
||||||
close(out)
|
|
||||||
|
|
||||||
return out, ErrInsufficientChannels
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(channels))
|
|
||||||
|
|
||||||
for _, c := range channels {
|
|
||||||
go func(channel <-chan T) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for v := range channel {
|
|
||||||
out <- v
|
|
||||||
}
|
|
||||||
}(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(out)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
@@ -1,36 +0,0 @@
|
|||||||
package mergechannel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
a := make(chan int, 5)
|
|
||||||
b := make(chan int, 5)
|
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
a <- i
|
|
||||||
b <- i
|
|
||||||
}
|
|
||||||
|
|
||||||
merged, err := merge(a, b)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
close(a)
|
|
||||||
close(b)
|
|
||||||
|
|
||||||
sum := 0
|
|
||||||
for v := range merged {
|
|
||||||
sum += v
|
|
||||||
}
|
|
||||||
assert.Equal(t, sum, 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInsufficient(t *testing.T) {
|
|
||||||
a := make(chan int)
|
|
||||||
|
|
||||||
_, err := merge(a)
|
|
||||||
assert.Equal(t, err, ErrInsufficientChannels)
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
package pipeline
|
|
||||||
|
|
||||||
func Pipe[In any, Out any](in <-chan In, pipe func(In) Out) <-chan Out {
|
|
||||||
out := make(chan Out)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for n := range in {
|
|
||||||
out <- pipe(n)
|
|
||||||
}
|
|
||||||
close(out)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
package pipeline
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPipe(t *testing.T) {
|
|
||||||
a := make(chan int, 1)
|
|
||||||
|
|
||||||
pipe := func(in int) int {
|
|
||||||
return in * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
b := Pipe(a, pipe)
|
|
||||||
a <- 5
|
|
||||||
close(a)
|
|
||||||
|
|
||||||
assert.Equal(t, 10, <-b)
|
|
||||||
}
|
|
@@ -1,63 +0,0 @@
|
|||||||
package semaphore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Semaphore interface {
|
|
||||||
Acquire(ctx context.Context) error
|
|
||||||
Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChanSemaphore implementation
|
|
||||||
type ChanSemaphore struct {
|
|
||||||
C chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ChanSemaphore) Acquire(ctx context.Context) error {
|
|
||||||
select {
|
|
||||||
case s.C <- struct{}{}:
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ChanSemaphore) Release() {
|
|
||||||
<-s.C
|
|
||||||
}
|
|
||||||
|
|
||||||
// AtomicSemaphore implementation
|
|
||||||
type AtomicSemaphore struct {
|
|
||||||
acquired atomic.Bool
|
|
||||||
TryDelay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AtomicSemaphore) Acquire(ctx context.Context) error {
|
|
||||||
if s.acquired.CompareAndSwap(false, true) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.slowAcquire(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AtomicSemaphore) slowAcquire(ctx context.Context) error {
|
|
||||||
ticker := time.NewTicker(s.TryDelay)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-ticker.C:
|
|
||||||
if s.acquired.CompareAndSwap(false, true) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AtomicSemaphore) Release() {
|
|
||||||
s.acquired.Store(false)
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
package semaphore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPipe(t *testing.T) {
|
|
||||||
}
|
|
@@ -1,84 +0,0 @@
|
|||||||
package sortcli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NL New line constant
|
|
||||||
const NL = "\n"
|
|
||||||
|
|
||||||
type Content [][]byte
|
|
||||||
|
|
||||||
func (c *Content) Sort(reverse bool) {
|
|
||||||
slices.SortFunc(*c, func(a, b []byte) int {
|
|
||||||
if reverse {
|
|
||||||
a, b = b, a
|
|
||||||
}
|
|
||||||
|
|
||||||
return slices.Compare(a, b)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Content) Uniques() {
|
|
||||||
m := make(map[string]struct{}, len(*c))
|
|
||||||
|
|
||||||
*c = slices.DeleteFunc[Content, []byte](*c, func(line []byte) bool {
|
|
||||||
if _, ok := m[string(line)]; !ok {
|
|
||||||
m[string(line)] = struct{}{}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Content) String() string {
|
|
||||||
lines := *c
|
|
||||||
|
|
||||||
var n int
|
|
||||||
for _, line := range lines {
|
|
||||||
n += len(line)
|
|
||||||
}
|
|
||||||
n += len(lines) * len(NL)
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.Grow(n)
|
|
||||||
|
|
||||||
for i, line := range lines {
|
|
||||||
if i > 0 {
|
|
||||||
sb.WriteString(NL)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rn := range line {
|
|
||||||
sb.WriteByte(rn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Content) Load(r io.Reader) {
|
|
||||||
br := bufio.NewReader(r)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, _, err := br.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(io.EOF, err) {
|
|
||||||
*c = append(*c, line)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Fatalf("can't read line: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = append(*c, line)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,53 +0,0 @@
|
|||||||
package sortcli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
const input = `
|
|
||||||
Zimbabwe
|
|
||||||
Africa
|
|
||||||
America
|
|
||||||
Zimbabwe
|
|
||||||
Africa
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestLoad(t *testing.T) {
|
|
||||||
c := &Content{}
|
|
||||||
c.Load(strings.NewReader(input))
|
|
||||||
|
|
||||||
assert.Equal(t, input, c.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnique(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
Name string
|
|
||||||
Content string
|
|
||||||
Expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"Unique",
|
|
||||||
input,
|
|
||||||
`
|
|
||||||
Zimbabwe
|
|
||||||
Africa
|
|
||||||
America`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
tc := tc
|
|
||||||
|
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
|
||||||
c := &Content{}
|
|
||||||
c.Load(strings.NewReader(tc.Content))
|
|
||||||
|
|
||||||
c.Uniques()
|
|
||||||
|
|
||||||
assert.Equal(t, tc.Expected, c.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
14
sort-cli/go.mod
Normal file
14
sort-cli/go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module sort-cli
|
||||||
|
|
||||||
|
go 1.21.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
12
sort-cli/go.sum
Normal file
12
sort-cli/go.sum
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||||
|
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@@ -1,34 +0,0 @@
|
|||||||
package sortcli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Open(sources []string) io.Reader {
|
|
||||||
rs := make([]io.Reader, 0, len(sources))
|
|
||||||
|
|
||||||
for _, source := range sources {
|
|
||||||
var r io.Reader
|
|
||||||
|
|
||||||
if source == "-" {
|
|
||||||
r = os.Stdin
|
|
||||||
} else {
|
|
||||||
if _, err := os.Stat(source); err != nil {
|
|
||||||
log.Fatalf("file not exists: %s", source)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(source)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("file open file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r = f
|
|
||||||
}
|
|
||||||
|
|
||||||
rs = append(rs, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return io.MultiReader(rs...)
|
|
||||||
}
|
|
211
sort-cli/main.go
Normal file
211
sort-cli/main.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cfg struct {
|
||||||
|
Key int
|
||||||
|
Numeric bool
|
||||||
|
Reverse bool
|
||||||
|
Unique bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Print(run())
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() string {
|
||||||
|
var c Cfg
|
||||||
|
|
||||||
|
flag.IntVar(&c.Key, "k", 0, "sort via column")
|
||||||
|
flag.BoolVar(&c.Numeric, "n", false, "compare according to string numerical value")
|
||||||
|
flag.BoolVar(&c.Reverse, "r", false, "reverse the result of comparisons")
|
||||||
|
flag.BoolVar(&c.Unique, "u", false, "output only the first of an equal run")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
return doSort(&c, flag.Args())
|
||||||
|
}
|
||||||
|
|
||||||
|
func doSort(cfg *Cfg, sources []string) string {
|
||||||
|
r := splitLines(load(sources), []byte("\n"), cfg.Key)
|
||||||
|
|
||||||
|
if cfg.Unique {
|
||||||
|
r = uniques(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := &Rows{s: r, cfg: cfg}
|
||||||
|
sort.Sort(rs)
|
||||||
|
|
||||||
|
return rs.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row struct {
|
||||||
|
Line []byte
|
||||||
|
Column []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rows struct {
|
||||||
|
s []*Row
|
||||||
|
cfg *Cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Rows) Len() int { return len(p.s) }
|
||||||
|
func (p *Rows) Less(i, j int) bool {
|
||||||
|
s := p.s
|
||||||
|
|
||||||
|
lr := s[i]
|
||||||
|
rr := s[j]
|
||||||
|
|
||||||
|
if p.cfg.Reverse {
|
||||||
|
lr, rr = rr, lr
|
||||||
|
}
|
||||||
|
|
||||||
|
var l, r []rune
|
||||||
|
|
||||||
|
if p.cfg.Key == 0 {
|
||||||
|
l, r = bytes.Runes(lr.Line), bytes.Runes(rr.Line)
|
||||||
|
} else {
|
||||||
|
l, r = bytes.Runes(lr.Column), bytes.Runes(rr.Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln := len(l)
|
||||||
|
rn := len(r)
|
||||||
|
|
||||||
|
for i := 0; i < min(ln, rn); i++ {
|
||||||
|
if l[i] == r[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return l[i] < r[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ln < rn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Rows) Swap(i, j int) {
|
||||||
|
s := p.s
|
||||||
|
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Rows) String() string {
|
||||||
|
r := p.s
|
||||||
|
|
||||||
|
var n int
|
||||||
|
for _, s := range r {
|
||||||
|
n += len(s.Line)
|
||||||
|
}
|
||||||
|
n += len(r) * len("\n")
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(n)
|
||||||
|
|
||||||
|
for _, c := range r {
|
||||||
|
for _, rn := range c.Line {
|
||||||
|
sb.WriteByte(rn)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func load(sources []string) []byte {
|
||||||
|
if len(sources) == 0 {
|
||||||
|
return loadStdin()
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := make([][]byte, 0, len(sources))
|
||||||
|
for _, path := range sources {
|
||||||
|
if path == "-" {
|
||||||
|
inputs = append(inputs, loadStdin())
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs = append(inputs, loadFile(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalSize int
|
||||||
|
for _, s := range inputs {
|
||||||
|
totalSize += len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.Grow(totalSize)
|
||||||
|
|
||||||
|
for _, c := range inputs {
|
||||||
|
b.Write(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadStdin() []byte {
|
||||||
|
b, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can't read stdin: %e", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFile(path string) []byte {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
log.Fatalf("file not exists: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("file open file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitLines(b, sp []byte, key int) []*Row {
|
||||||
|
r := make([]*Row, 0, bytes.Count(b, sp))
|
||||||
|
for _, b := range bytes.Split(b, sp) {
|
||||||
|
var column []byte
|
||||||
|
|
||||||
|
if key != 0 {
|
||||||
|
bs := bytes.Split(b, []byte(" "))
|
||||||
|
|
||||||
|
if len(bs) < key {
|
||||||
|
continue // TODO is it error or not?
|
||||||
|
// log.Fatalf("Column for key \"%d\" doesn't exists", cfg.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
column = bs[key-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
r = append(r, &Row{Line: b, Column: column})
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniques(r []*Row) []*Row {
|
||||||
|
m := make(map[string]*Row, len(r))
|
||||||
|
|
||||||
|
for _, r := range r {
|
||||||
|
r := r
|
||||||
|
|
||||||
|
m[string(r.Line)] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return maps.Values(m)
|
||||||
|
}
|
@@ -2,11 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFlags(t *testing.T) {
|
func TestFlags(t *testing.T) {
|
||||||
@@ -21,26 +20,22 @@ func TestFlags(t *testing.T) {
|
|||||||
ExpectedExit int
|
ExpectedExit int
|
||||||
ExpectedOutput string
|
ExpectedOutput string
|
||||||
}{
|
}{
|
||||||
{
|
{"No flags",
|
||||||
"No flags",
|
|
||||||
[]string{"testdata/first"},
|
[]string{"testdata/first"},
|
||||||
0,
|
0,
|
||||||
"alabama barcelona\nbarcelona california\ncalifornia denver\ncalifornia denver\nамур брянск\nбелгород волгоград\nволгоград геленджик",
|
"alabama barcelona\nbarcelona california\ncalifornia denver\ncalifornia denver\nамур брянск\nбелгород волгоград\nволгоград геленджик",
|
||||||
},
|
},
|
||||||
{
|
{"Reverse",
|
||||||
"Reverse",
|
|
||||||
[]string{"-r", "testdata/first"},
|
[]string{"-r", "testdata/first"},
|
||||||
0,
|
0,
|
||||||
"волгоград геленджик\nбелгород волгоград\nамур брянск\ncalifornia denver\ncalifornia denver\nbarcelona california\nalabama barcelona",
|
"волгоград геленджик\nбелгород волгоград\nамур брянск\ncalifornia denver\ncalifornia denver\nbarcelona california\nalabama barcelona",
|
||||||
},
|
},
|
||||||
{
|
{"Unique",
|
||||||
"Unique",
|
|
||||||
[]string{"-u", "testdata/first"},
|
[]string{"-u", "testdata/first"},
|
||||||
0,
|
0,
|
||||||
"alabama barcelona\nbarcelona california\ncalifornia denver\nамур брянск\nбелгород волгоград\nволгоград геленджик",
|
"alabama barcelona\nbarcelona california\ncalifornia denver\nамур брянск\nбелгород волгоград\nволгоград геленджик",
|
||||||
},
|
},
|
||||||
{
|
{"Column 2",
|
||||||
"Column 2",
|
|
||||||
[]string{"-k=2", "testdata/first"},
|
[]string{"-k=2", "testdata/first"},
|
||||||
0,
|
0,
|
||||||
"alabama barcelona\nbarcelona california\ncalifornia denver\ncalifornia denver\nамур брянск\nбелгород волгоград\nволгоград геленджик",
|
"alabama barcelona\nbarcelona california\ncalifornia denver\ncalifornia denver\nамур брянск\nбелгород волгоград\nволгоград геленджик",
|
14
sqlc.yaml
14
sqlc.yaml
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
@@ -1,10 +0,0 @@
|
|||||||
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()
|
|
||||||
)
|
|
||||||
;
|
|
@@ -1,32 +0,0 @@
|
|||||||
// 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,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
-- name: Insert :one
|
|
||||||
INSERT INTO unpack_history (input, result)
|
|
||||||
VALUES (@input, @result)
|
|
||||||
RETURNING
|
|
||||||
id,
|
|
||||||
input,
|
|
||||||
result,
|
|
||||||
created_at
|
|
||||||
;
|
|
@@ -1,37 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
-- name: Latest :many
|
|
||||||
SELECT id,
|
|
||||||
input,
|
|
||||||
result,
|
|
||||||
created_at
|
|
||||||
FROM unpack_history
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 15
|
|
||||||
;
|
|
@@ -1,45 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
// 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"`
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
// 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)
|
|
@@ -1,78 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@@ -1,54 +0,0 @@
|
|||||||
package stringunpack
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrIncorrectString = errors.New("incorrect string")
|
|
||||||
|
|
||||||
func Unpack(s string) (string, error) {
|
|
||||||
if s == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.Grow(len(s))
|
|
||||||
|
|
||||||
var curSymbol rune
|
|
||||||
for _, r := range s {
|
|
||||||
isNumber := false
|
|
||||||
number := 0
|
|
||||||
if n, err := strconv.Atoi(string(r)); err == nil {
|
|
||||||
number = n
|
|
||||||
isNumber = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if curSymbol == rune(0) {
|
|
||||||
switch {
|
|
||||||
case isNumber:
|
|
||||||
return "", ErrIncorrectString
|
|
||||||
case !isNumber:
|
|
||||||
curSymbol = r
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch {
|
|
||||||
case !isNumber:
|
|
||||||
sb.WriteRune(curSymbol)
|
|
||||||
curSymbol = r
|
|
||||||
case isNumber:
|
|
||||||
for i := 0; i < number; i++ {
|
|
||||||
sb.WriteRune(curSymbol)
|
|
||||||
}
|
|
||||||
curSymbol = rune(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if curSymbol != rune(0) {
|
|
||||||
sb.WriteRune(curSymbol)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String(), nil
|
|
||||||
}
|
|
@@ -1,44 +0,0 @@
|
|||||||
package stringunpack_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
stringunpack "git.grachevko.ru/grachevko/h/string-unpack"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnpack(t *testing.T) {
|
|
||||||
testcases := []struct {
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{input: "a4bc2d5e", expected: "aaaabccddddde"},
|
|
||||||
{input: "abcd", expected: "abcd"},
|
|
||||||
{input: "aaa0b", expected: "aab"},
|
|
||||||
{input: "", expected: ""},
|
|
||||||
{input: "d\n5abc", expected: "d\n\n\n\n\nabc"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testcases {
|
|
||||||
t.Run(tc.input, func(t *testing.T) {
|
|
||||||
result, err := stringunpack.Unpack(tc.input)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.expected, result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnpackIncorrectString(t *testing.T) {
|
|
||||||
testcases := []string{
|
|
||||||
"3abc",
|
|
||||||
"45",
|
|
||||||
"aaa10b",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, input := range testcases {
|
|
||||||
t.Run(input, func(t *testing.T) {
|
|
||||||
_, err := stringunpack.Unpack(input)
|
|
||||||
assert.Equal(t, stringunpack.ErrIncorrectString, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
package uniqueschars
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Unique(s string) bool {
|
|
||||||
m := make(map[rune]struct{}, utf8.RuneCountInString(s)/2)
|
|
||||||
|
|
||||||
for _, c := range s {
|
|
||||||
c := unicode.ToLower(c)
|
|
||||||
|
|
||||||
if _, ok := m[c]; ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
m[c] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
@@ -1,44 +0,0 @@
|
|||||||
package uniqueschars
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnique(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
Name string
|
|
||||||
input string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"eng alphabet",
|
|
||||||
"abcdefghijklmnopqrstuvwxyz",
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"eng mixed case",
|
|
||||||
"aAbBfF",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cyrillic alphabet",
|
|
||||||
"абвгдеёжзийклмнопрстуфхцчшщъыьэюя",
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cyrillic case mixed",
|
|
||||||
"аАбБвВ",
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
tc := tc
|
|
||||||
|
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tc.expected, Unique(tc.input))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
package workerpool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WorkerPool[T any](limit int, worker func(int, T)) chan<- T {
|
|
||||||
tasks := make(chan T, limit)
|
|
||||||
|
|
||||||
for i := 1; i <= limit; i++ {
|
|
||||||
go func(i int) {
|
|
||||||
defer fmt.Printf("worker:%d done\n", i)
|
|
||||||
|
|
||||||
for t := range tasks {
|
|
||||||
worker(i, t)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
package workerpool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWorkerPool(t *testing.T) {
|
|
||||||
const tasks = 5
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(tasks)
|
|
||||||
|
|
||||||
var mt sync.Mutex
|
|
||||||
done := make(map[int]bool, tasks)
|
|
||||||
|
|
||||||
worker := func(pid int, task int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
mt.Lock()
|
|
||||||
done[pid] = true
|
|
||||||
mt.Unlock()
|
|
||||||
|
|
||||||
fmt.Printf("task:%+v\n", task)
|
|
||||||
}
|
|
||||||
|
|
||||||
pool := WorkerPool(tasks, worker)
|
|
||||||
|
|
||||||
for j := 0; j < tasks; j++ {
|
|
||||||
pool <- j
|
|
||||||
}
|
|
||||||
close(pool)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for _, v := range done {
|
|
||||||
assert.True(t, v)
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user