This commit is contained in:
r8zavetr8v 2024-05-03 00:11:28 +03:00
parent ebf2edafff
commit 42c577f80a
14 changed files with 627 additions and 0 deletions

View File

@ -0,0 +1,13 @@
package commands
import "github.com/urfave/cli/v2"
var commandPool []*cli.Command
func Register(c *cli.Command) {
commandPool = append(commandPool, c)
}
func Commands() []*cli.Command {
return commandPool
}

55
backend/cmd/main.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/emochka2007/block-accounting/cmd/commands"
cli "github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "biocom-ioannes",
Version: "0.0.1a",
Commands: commands.Commands(),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "log-level",
Value: "debug",
},
&cli.StringFlag{
Name: "address",
Value: "localhost:8080",
},
&cli.StringFlag{
Name: "db-host",
},
&cli.StringFlag{
Name: "db-user",
},
&cli.StringFlag{
Name: "db-password",
},
},
Action: func(c *cli.Context) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// todo build config
// todo build service
// todo run service
return nil
},
}
if err := app.Run(os.Args); err != nil {
panic(err)
}
}

19
backend/go.mod Normal file
View File

@ -0,0 +1,19 @@
module github.com/emochka2007/block-accounting
go 1.22.2
require (
github.com/fatih/color v1.16.0
github.com/go-chi/chi/v5 v5.0.12
github.com/urfave/cli/v2 v2.27.2
golang.org/x/sync v0.7.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/sys v0.14.0 // indirect
)

23
backend/go.sum Normal file
View File

@ -0,0 +1,23 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@ -0,0 +1 @@
package factory

View File

@ -0,0 +1,29 @@
package controllers
import (
"context"
"log/slog"
"net/http"
)
type PingController interface {
HandlePing(ctx context.Context, req *http.Request, w http.ResponseWriter) error
}
type pingController struct {
log *slog.Logger
}
func NewPingController(
log *slog.Logger,
) PingController {
return &pingController{
log: log,
}
}
func (c *pingController) HandlePing(ctx context.Context, req *http.Request, w http.ResponseWriter) error {
_, err := w.Write([]byte("pong"))
return err
}

View File

@ -0,0 +1,13 @@
package controllers
type RootController struct {
Ping PingController
}
func NewRootController(
ping PingController,
) *RootController {
return &RootController{
Ping: ping,
}
}

View File

@ -0,0 +1,23 @@
package rest
import "net/http"
type apiError struct {
Code int `json:"code"`
Error string `json:"error"`
}
func buildApiError(code int, message string) apiError {
return apiError{
Code: code,
Error: message,
}
}
func mapError(_ error) apiError {
// todo map typed errors
switch {
default:
return buildApiError(http.StatusInternalServerError, "Internal Server Error")
}
}

View File

@ -0,0 +1,108 @@
package rest
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"sync"
"github.com/emochka2007/block-accounting/internal/interface/controllers"
"github.com/emochka2007/block-accounting/internal/logger"
"github.com/go-chi/chi/v5"
mw "github.com/go-chi/chi/v5/middleware"
)
type Server struct {
*chi.Mux
ctx context.Context
log *slog.Logger
addr string
tls bool
controllers *controllers.RootController
closeMu sync.RWMutex
closed bool
}
func NewServer(
log *slog.Logger,
addr string,
controllers *controllers.RootController,
) *Server {
s := &Server{
log: log,
addr: addr,
controllers: controllers,
}
s.buildRouter()
return s
}
func (s *Server) Serve(ctx context.Context) error {
s.ctx = ctx
if s.tls {
return http.ListenAndServeTLS(s.addr, "/todo", "/todo", s)
}
return http.ListenAndServe(s.addr, s)
}
func (s *Server) Close() {
s.closeMu.Lock()
defer s.closeMu.Unlock()
s.closed = true
}
func (s *Server) buildRouter() {
s.Mux = chi.NewRouter()
s.With(mw.Recoverer)
s.With(mw.RequestID)
s.With(s.handleMw)
s.Get("/ping", s.handlePing)
// todo build rest api router
}
func (s *Server) responseError(w http.ResponseWriter, e error) {
apiErr := mapError(e)
out, err := json.Marshal(apiErr)
if err != nil {
s.log.Error("error marshal api error", logger.Err(err))
return
}
w.WriteHeader(apiErr.Code)
w.Write(out)
}
func (s *Server) handleMw(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
s.closeMu.RLock()
defer s.closeMu.Unlock()
if s.closed { // keep mutex closed
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func (s *Server) handlePing(w http.ResponseWriter, req *http.Request) {
if err := s.controllers.Ping.HandlePing(s.ctx, req, w); err != nil {
s.responseError(w, err)
}
}

View File

@ -0,0 +1,88 @@
package logger
import (
"io"
"log/slog"
"os"
)
type LoggerBuilder struct {
local bool
addSource bool
lvl slog.Level
writers []io.Writer
}
func (b *LoggerBuilder) WithWriter(w io.Writer) *LoggerBuilder {
b.writers = append(b.writers, w)
return b
}
func (b *LoggerBuilder) WithLevel(l slog.Level) *LoggerBuilder {
b.lvl = l
return b
}
func (b *LoggerBuilder) Local() *LoggerBuilder {
b.local = true
return b
}
func (b *LoggerBuilder) WithSource() *LoggerBuilder {
b.addSource = true
return b
}
func (b *LoggerBuilder) Build() *slog.Logger {
w := io.MultiWriter(b.writers...)
if b.local {
opts := PrettyHandlerOptions{
SlogOpts: &slog.HandlerOptions{
Level: b.lvl,
AddSource: b.addSource,
},
}
handler := opts.NewPrettyHandler(w)
return slog.New(handler)
}
return slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: b.lvl,
AddSource: b.addSource,
}),
)
}
func newLogger(lvl slog.Level, w io.Writer) *slog.Logger {
return slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}),
)
}
func Err(err error) slog.Attr {
return slog.Attr{
Key: "error",
Value: slog.StringValue(err.Error()),
}
}
func MapLevel(lvl string) slog.Level {
switch lvl {
case "dev", "local", "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}

View File

@ -0,0 +1,32 @@
package logger
import (
"context"
"log/slog"
)
func NewDiscardLogger() *slog.Logger {
return slog.New(NewDiscardHandler())
}
type DiscardHandler struct{}
func NewDiscardHandler() *DiscardHandler {
return &DiscardHandler{}
}
func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error {
return nil
}
func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return h
}
func (h *DiscardHandler) WithGroup(_ string) slog.Handler {
return h
}
func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool {
return false
}

View File

@ -0,0 +1,97 @@
package logger
import (
"context"
"encoding/json"
"io"
stdlog "log"
"log/slog"
"github.com/fatih/color"
)
type PrettyHandlerOptions struct {
SlogOpts *slog.HandlerOptions
}
type PrettyHandler struct {
opts PrettyHandlerOptions
slog.Handler
l *stdlog.Logger
attrs []slog.Attr
}
func (opts PrettyHandlerOptions) NewPrettyHandler(
out io.Writer,
) *PrettyHandler {
h := &PrettyHandler{
Handler: slog.NewJSONHandler(out, opts.SlogOpts),
l: stdlog.New(out, "", 0),
}
return h
}
func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error {
level := r.Level.String() + ":"
switch r.Level {
case slog.LevelDebug:
level = color.MagentaString(level)
case slog.LevelInfo:
level = color.BlueString(level)
case slog.LevelWarn:
level = color.YellowString(level)
case slog.LevelError:
level = color.RedString(level)
}
fields := make(map[string]interface{}, r.NumAttrs())
r.Attrs(func(a slog.Attr) bool {
fields[a.Key] = a.Value.Any()
return true
})
for _, a := range h.attrs {
fields[a.Key] = a.Value.Any()
}
var b []byte
var err error
if len(fields) > 0 {
b, err = json.MarshalIndent(fields, "", " ")
if err != nil {
return err
}
}
timeStr := r.Time.Format("[15:05:05.000]")
msg := color.CyanString(r.Message)
h.l.Println(
timeStr,
level,
msg,
color.WhiteString(string(b)),
)
return nil
}
func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &PrettyHandler{
Handler: h.Handler,
l: h.l,
attrs: attrs,
}
}
func (h *PrettyHandler) WithGroup(name string) slog.Handler {
return &PrettyHandler{
Handler: h.Handler.WithGroup(name),
l: h.l,
}
}

View File

@ -0,0 +1,41 @@
package service
import (
"context"
"log/slog"
"github.com/emochka2007/block-accounting/internal/interface/rest"
"golang.org/x/sync/errgroup"
)
type Service interface {
Run(ctx context.Context) error
Stop()
}
type service struct {
log *slog.Logger
rest *rest.Server
}
func NewService(
log *slog.Logger,
rest *rest.Server,
) Service {
return &service{
log: log,
}
}
func (s *service) Run(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return s.rest.Serve(ctx)
})
return g.Wait()
}
func (s *service) Stop() {
}

View File

@ -0,0 +1,85 @@
package sqltools
import (
"context"
"database/sql"
"errors"
"fmt"
)
type DBTX interface {
Query(query string, args ...any) (*sql.Rows, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRow(query string, args ...any) *sql.Row
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
Exec(query string, args ...any) (sql.Result, error)
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
type TransactionalStorage interface {
Transaction(ctx context.Context, db *sql.DB, f func(ctx context.Context) error) error
Conn(ctx context.Context) DBTX
}
type txCtxKey struct{}
func Transaction(ctx context.Context, db *sql.DB, fn func(context.Context) error) error {
var err error
var tx *sql.Tx = new(sql.Tx)
hasExternalTx := hasExternalTransaction(ctx)
defer func() {
if hasExternalTx {
if err != nil {
err = fmt.Errorf("error perform operation. %w", err)
return
}
return
}
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
err = errors.Join(fmt.Errorf("error rollback transaction. %w", rbErr), err)
return
}
err = fmt.Errorf("error execute transactional operation. %w", err)
return
}
if commitErr := tx.Commit(); commitErr != nil {
if rbErr := tx.Rollback(); rbErr != nil {
err = errors.Join(fmt.Errorf("error rollback transaction. %w", rbErr), commitErr, err)
return
}
err = fmt.Errorf("error commit transaction. %w", err)
}
}()
if !hasExternalTx {
tx, err = db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
return fmt.Errorf("error begin transaction. %w", err)
}
ctx = context.WithValue(ctx, txCtxKey{}, tx)
}
return fn(ctx)
}
func hasExternalTransaction(ctx context.Context) bool {
if _, ok := ctx.Value(txCtxKey{}).(*sql.Tx); ok {
return true
}
return false
}