commit
fc19ac2e89
|
@ -1,13 +1,16 @@
|
||||||
build:
|
build:
|
||||||
go build -o ./bin/calendar ./cmd/calendar
|
go build -o ./bin/calendar ./cmd/calendar/main.go
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run ./cmd/calendar/main.go -config ./configs/config.toml
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -race ./internal/... ./pkg/...
|
go test -race ./internal/...
|
||||||
|
|
||||||
install-lint-deps:
|
|
||||||
(which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0
|
|
||||||
|
|
||||||
lint: install-lint-deps
|
lint: install-lint-deps
|
||||||
golangci-lint run ./...
|
golangci-lint run .cmd/... ./internal/...
|
||||||
|
|
||||||
.PHONY: build test lint
|
install-lint-deps:
|
||||||
|
(which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.30.0
|
||||||
|
|
||||||
|
.PHONY: build run test lint
|
|
@ -1,14 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
// При желании конфигурацию можно вынести в internal/config.
|
|
||||||
// Организация конфига в main принуждает нас сужать API компонентов, использовать
|
|
||||||
// при их конструировании только необходимые параметры, а также уменьшает вероятность циклической зависимости.
|
|
||||||
type Config struct {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfig() Config {
|
|
||||||
return Config{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
|
@ -2,29 +2,47 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
oslog "log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/app"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
"github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/logger"
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/app"
|
||||||
internalhttp "github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/server/http"
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/config"
|
||||||
memorystorage "github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/storage/memory"
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/logger"
|
||||||
|
internalhttp "github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/server/http"
|
||||||
|
store "github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configFile string
|
var configFile string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.StringVar(&configFile, "config", "/etc/calendar/config.toml", "Path to configuration file")
|
flag.StringVar(&configFile, "config", "/etc/calendar/config.toml", "Path to configuration file")
|
||||||
|
flag.Parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := NewConfig()
|
conf, err := config.NewConfig(configFile)
|
||||||
logg := logger.New(config.Logger.Level)
|
if err != nil {
|
||||||
|
oslog.Fatal("не удалось открыть файл конфигурации:", err.Error())
|
||||||
|
}
|
||||||
|
log, err := logger.New(conf)
|
||||||
|
if err != nil {
|
||||||
|
oslog.Fatal("не удалось запустить логер:", err.Error())
|
||||||
|
}
|
||||||
|
storeConf := store.Config{
|
||||||
|
InMemory: conf.Storage.InMemory,
|
||||||
|
SQLHost: conf.Storage.SQLHost,
|
||||||
|
SQLPort: conf.Storage.SQLPort,
|
||||||
|
SQLDbase: conf.Storage.SQLDbase,
|
||||||
|
SQLUser: conf.Storage.SQLUser,
|
||||||
|
SQLPass: conf.Storage.SQLPass,
|
||||||
|
}
|
||||||
|
st := store.NewStore(storeConf)
|
||||||
|
|
||||||
storage := memorystorage.New()
|
calendar := app.New(log, st)
|
||||||
calendar := app.New(logg, storage)
|
|
||||||
|
|
||||||
server := internalhttp.NewServer(calendar)
|
server := internalhttp.NewServer(calendar, conf.Server.Address, conf.Server.Port)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
signals := make(chan os.Signal, 1)
|
signals := make(chan os.Signal, 1)
|
||||||
|
@ -34,12 +52,12 @@ func main() {
|
||||||
signal.Stop(signals)
|
signal.Stop(signals)
|
||||||
|
|
||||||
if err := server.Stop(); err != nil {
|
if err := server.Stop(); err != nil {
|
||||||
logger.Error("failed to stop http server: " + err.String())
|
log.Errorf("failed to stop http server: " + err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
logger.Error("failed to start http server: " + err.String())
|
log.Errorf("failed to start http server: " + err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
[logger]
|
[Server]
|
||||||
level = "INFO"
|
Address = "localhost"
|
||||||
|
Port = "8080"
|
||||||
|
|
||||||
# TODO
|
[Logger]
|
||||||
# ...
|
File = "./calendar.log"
|
||||||
|
Level = "INFO"
|
||||||
|
|
||||||
|
[Storage]
|
||||||
|
inMemory = false
|
||||||
|
SQLHost = "localhost"
|
||||||
|
SQLPort = "5432"
|
||||||
|
SQLDbase = "calendar"
|
||||||
|
SQLUser = "calendar"
|
||||||
|
SQLPass = "12345678"
|
|
@ -1,3 +1,13 @@
|
||||||
module github.com/fixme_my_friend/hw12_13_14_15_calendar
|
module github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v0.3.1
|
||||||
|
github.com/amitrai48/logger v0.0.0-20190214092904-448001c055ec
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
|
github.com/mattn/go-shellwords v1.0.10 // indirect
|
||||||
|
github.com/stretchr/testify v1.4.0
|
||||||
|
go.uber.org/zap v1.15.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||||
|
)
|
||||||
|
|
|
@ -2,28 +2,57 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/storage"
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/logger"
|
||||||
|
store "github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage"
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
CreateEvent(ctx context.Context, title string) (err error)
|
||||||
|
Handler(w http.ResponseWriter, r *http.Request)
|
||||||
|
LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
// TODO
|
Storage store.StorageInterface
|
||||||
|
Logger logger.Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
type Logger interface {
|
func New(logger logger.Interface, storage store.StorageInterface) *App {
|
||||||
// TODO
|
return &App{Logger: logger, Storage: storage}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Storage interface {
|
func (a *App) CreateEvent(ctx context.Context, title string) (err error) {
|
||||||
// TODO
|
_, err = a.Storage.Create(event.Event{Title: title})
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorf("can not create event")
|
||||||
|
}
|
||||||
|
a.Logger.Infof("event created")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger Logger, storage Storage) *App {
|
func (a *App) Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
return &App{}
|
w.WriteHeader(200)
|
||||||
|
_, _ = w.Write([]byte("Hello! I'm calendar app!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CreateEvent(ctx context.Context, id string, title string) error {
|
func (a *App) LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return a.storage.CreateEvent(storage.Event{ID: id, Title: title})
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
var path, useragent string
|
||||||
|
if r.URL != nil {
|
||||||
|
path = r.URL.Path
|
||||||
|
}
|
||||||
|
if len(r.UserAgent()) > 0 {
|
||||||
|
useragent = r.UserAgent()
|
||||||
|
}
|
||||||
|
latency := time.Since(start)
|
||||||
|
a.Logger.Infof("receive %s request from IP: %s on path: %s, duration: %s useragent: %s ", r.Method, r.RemoteAddr, path, latency, useragent)
|
||||||
|
}()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server struct {
|
||||||
|
Address string
|
||||||
|
Port string
|
||||||
|
}
|
||||||
|
Logger struct {
|
||||||
|
File string
|
||||||
|
Level string
|
||||||
|
MuteStdout bool
|
||||||
|
}
|
||||||
|
Storage struct {
|
||||||
|
InMemory bool
|
||||||
|
SQLHost string
|
||||||
|
SQLPort string
|
||||||
|
SQLDbase string
|
||||||
|
SQLUser string
|
||||||
|
SQLPass string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confita может быти и хороша, но она не возвращает ошибки, если не может распарсить файл в структуру. Мне не нравится такая "молчаливость".
|
||||||
|
func NewConfig(configFile string) (Config, error) {
|
||||||
|
f, err := os.Open(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
s, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
_, err = toml.Decode(string(s), &config)
|
||||||
|
return config, err
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"io/ioutil"
|
||||||
|
oslog "log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = func() bool {
|
||||||
|
testing.Init()
|
||||||
|
return true
|
||||||
|
}()
|
||||||
|
|
||||||
|
func TestNewConfig(t *testing.T) {
|
||||||
|
|
||||||
|
badfile, err := ioutil.TempFile("", "conf.")
|
||||||
|
if err != nil {
|
||||||
|
oslog.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(badfile.Name())
|
||||||
|
badfile.WriteString(`aefSD
|
||||||
|
sadfg
|
||||||
|
RFABND FYGUMG
|
||||||
|
V`)
|
||||||
|
badfile.Sync()
|
||||||
|
|
||||||
|
goodfile, err := ioutil.TempFile("", "conf.")
|
||||||
|
if err != nil {
|
||||||
|
oslog.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(goodfile.Name())
|
||||||
|
goodfile.WriteString(`[storage]
|
||||||
|
inMemory = true
|
||||||
|
SQLHost = "localhost"`)
|
||||||
|
goodfile.Sync()
|
||||||
|
|
||||||
|
t.Run("No such file", func(t *testing.T) {
|
||||||
|
c, e := NewConfig("adfergdth")
|
||||||
|
require.Equal(t, Config{}, c)
|
||||||
|
require.Error(t, e)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Bad file", func(t *testing.T) {
|
||||||
|
c, e := NewConfig(badfile.Name())
|
||||||
|
require.Equal(t, Config{}, c)
|
||||||
|
require.Error(t, e)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TOML reading", func(t *testing.T) {
|
||||||
|
c, e := NewConfig(goodfile.Name())
|
||||||
|
require.Equal(t, true, c.Storage.InMemory)
|
||||||
|
require.Equal(t, "localhost", c.Storage.SQLHost)
|
||||||
|
require.NoError(t, e)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -1,11 +1,71 @@
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
amitralog "github.com/amitrai48/logger"
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
Infof(format string, args ...interface{})
|
||||||
|
Warnf(format string, args ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Fatalf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
// TODO
|
Logger amitralog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(level string) *Logger {
|
func New(conf config.Config) (Interface, error) {
|
||||||
return &Logger{}
|
if conf.Logger.File == "" || !validLevel(conf.Logger.Level) {
|
||||||
|
return nil, errors.New("invalid logger config")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := amitralog.Configuration{
|
||||||
|
EnableConsole: !conf.Logger.MuteStdout,
|
||||||
|
ConsoleLevel: amitralog.Fatal,
|
||||||
|
ConsoleJSONFormat: false,
|
||||||
|
EnableFile: true,
|
||||||
|
FileLevel: strings.ToLower(conf.Logger.Level),
|
||||||
|
FileJSONFormat: true,
|
||||||
|
FileLocation: conf.Logger.File,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := amitralog.NewLogger(c, amitralog.InstanceZapLogger); err != nil {
|
||||||
|
log.Fatalf("Could not instantiate log %s", err.Error())
|
||||||
|
}
|
||||||
|
l := amitralog.WithFields(amitralog.Fields{"hw": "12"})
|
||||||
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
func (l *Logger) Debugf(format string, args ...interface{}) {
|
||||||
|
l.Logger.Debugf(format, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Infof(format string, args ...interface{}) {
|
||||||
|
l.Logger.Infof(format, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Warnf(format string, args ...interface{}) {
|
||||||
|
l.Logger.Warnf(format, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Errorf(format string, args ...interface{}) {
|
||||||
|
l.Logger.Errorf(format, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Fatalf(format string, args ...interface{}) {
|
||||||
|
l.Logger.Fatalf(format, args)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validLevel(level string) bool {
|
||||||
|
var l = map[string]int{"debug": 1, "info": 1, "warn": 1, "error": 1, "fatal": 1}
|
||||||
|
return l[strings.ToLower(level)] == 1
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,63 @@
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/config"
|
||||||
|
"io/ioutil"
|
||||||
|
oslog "log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestLogger(t *testing.T) {
|
func TestLoggerLogic(t *testing.T) {
|
||||||
// TODO
|
tmpfile, err := ioutil.TempFile("", "log.")
|
||||||
|
if err != nil {
|
||||||
|
oslog.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
conf := config.Config{Logger: struct {
|
||||||
|
File string
|
||||||
|
Level string
|
||||||
|
MuteStdout bool
|
||||||
|
}{File: tmpfile.Name(), Level: "warn", MuteStdout: false}}
|
||||||
|
log, err := New(conf)
|
||||||
|
if err != nil {
|
||||||
|
oslog.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Messages arround the level", func(t *testing.T) {
|
||||||
|
log.Debugf("debug message")
|
||||||
|
log.Errorf("error message")
|
||||||
|
|
||||||
|
res, err := ioutil.ReadAll(tmpfile)
|
||||||
|
if err != nil {
|
||||||
|
oslog.Fatal(err)
|
||||||
|
}
|
||||||
|
require.Less(t, strings.Index(string(res), "debug message"), 0)
|
||||||
|
require.Greater(t, strings.Index(string(res), "error message"), 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerNegative(t *testing.T) {
|
||||||
|
t.Run("Bad file name", func(t *testing.T) {
|
||||||
|
conf := config.Config{Logger: struct {
|
||||||
|
File string
|
||||||
|
Level string
|
||||||
|
MuteStdout bool
|
||||||
|
}{File: "", Level: "debug", MuteStdout: true}}
|
||||||
|
_, err := New(conf)
|
||||||
|
require.Error(t, err, "invalid logger config")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Bad level", func(t *testing.T) {
|
||||||
|
conf := config.Config{Logger: struct {
|
||||||
|
File string
|
||||||
|
Level string
|
||||||
|
MuteStdout bool
|
||||||
|
}{File: "asdafad", Level: "wegretryjt", MuteStdout: true}}
|
||||||
|
_, err := New(conf)
|
||||||
|
require.Error(t, err, "invalid logger config")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package internalhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func loggingMiddleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// TODO
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,26 +1,33 @@
|
||||||
package internalhttp
|
package http
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
// TODO
|
server *http.Server
|
||||||
|
app app.App
|
||||||
}
|
}
|
||||||
|
|
||||||
type Application interface {
|
func NewServer(app *app.App, address string, port string) *Server {
|
||||||
// TODO
|
return &Server{server: &http.Server{Addr: net.JoinHostPort(address, port), Handler: app.LoggingMiddleware(app.Handler)}, app: *app}
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(app Application) *Server {
|
|
||||||
return &Server{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
// TODO
|
if err := s.server.ListenAndServe(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.app.Logger.Infof("Server starting")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
if err := s.server.Close(); err != nil {
|
||||||
// TODO
|
return err
|
||||||
|
}
|
||||||
|
s.app.Logger.Infof("Server stoped")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
ID string
|
|
||||||
Title string
|
|
||||||
// TODO
|
|
||||||
}
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ID int64
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Title string
|
||||||
|
Date time.Time
|
||||||
|
Latency time.Duration
|
||||||
|
Note string
|
||||||
|
UserID int64
|
||||||
|
NotifyTime time.Duration
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
Events map[event.ID]event.Event
|
||||||
|
lastID event.ID
|
||||||
|
Mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Storage {
|
||||||
|
return &Storage{Events: make(map[event.ID]event.Event)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Create(event event.Event) (event.ID, error) {
|
||||||
|
s.Mu.Lock()
|
||||||
|
s.lastID++
|
||||||
|
s.Events[s.lastID] = event
|
||||||
|
s.Mu.Unlock()
|
||||||
|
return s.lastID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Update(id event.ID, event event.Event) error {
|
||||||
|
s.Mu.Lock()
|
||||||
|
s.Events[id] = event
|
||||||
|
s.Mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Delete(id event.ID) error {
|
||||||
|
s.Mu.Lock()
|
||||||
|
delete(s.Events, id)
|
||||||
|
s.Mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) List() (map[event.ID]event.Event, error) {
|
||||||
|
return s.Events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetByID(id event.ID) (event.Event, bool) {
|
||||||
|
if s.Events[id].Title == "" {
|
||||||
|
return event.Event{}, false
|
||||||
|
}
|
||||||
|
return s.Events[id], true
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage/event"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dateTimeLayout = "2006-01-02 15:04:00 -0700"
|
||||||
|
|
||||||
|
func TestMemoryStorage(t *testing.T) {
|
||||||
|
s := New()
|
||||||
|
dateParced1, _ := time.Parse(dateTimeLayout, "11.11.1111")
|
||||||
|
dateParced2, _ := time.Parse(dateTimeLayout, "22.11.22222")
|
||||||
|
|
||||||
|
t.Run("Empty storage", func(t *testing.T) {
|
||||||
|
require.Equal(t, 0, len(s.Events))
|
||||||
|
})
|
||||||
|
id, err := s.Create(event.Event{Title: "event1", Date: dateParced1})
|
||||||
|
|
||||||
|
t.Run("Create events", func(t *testing.T) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(s.Events))
|
||||||
|
require.Equal(t, event.Event{Title: "event1", Date: dateParced1}, s.Events[id])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update event", func(t *testing.T) {
|
||||||
|
err := s.Update(id, event.Event{Title: "event1_modifyed", Date: dateParced2})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(s.Events))
|
||||||
|
require.Equal(t, event.Event{Title: "event1_modifyed", Date: dateParced2}, s.Events[id])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List event", func(t *testing.T) {
|
||||||
|
res, err := s.List()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(res))
|
||||||
|
require.Equal(t, event.Event{Title: "event1_modifyed", Date: dateParced2}, res[id])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get event by ID", func(t *testing.T) {
|
||||||
|
res, ok := s.GetByID(id)
|
||||||
|
require.Equal(t, ok, true)
|
||||||
|
require.Equal(t, event.Event{Title: "event1_modifyed", Date: dateParced2}, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get event by fake ID", func(t *testing.T) {
|
||||||
|
res, ok := s.GetByID(53663)
|
||||||
|
require.Equal(t, ok, false)
|
||||||
|
require.Equal(t, event.Event{}, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete event", func(t *testing.T) {
|
||||||
|
err := s.Delete(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, len(s.Events))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
package memorystorage
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
type Storage struct {
|
|
||||||
// TODO
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *Storage {
|
|
||||||
return &Storage{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
|
@ -1,7 +0,0 @@
|
||||||
package memorystorage
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestStorage(t *testing.T) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dateTimeLayout = "2006-01-02 15:04:00 -0700"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
User string
|
||||||
|
Pass string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Dbase string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(conf Config) *Storage {
|
||||||
|
return &Storage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Connect(config Config) error {
|
||||||
|
var err error
|
||||||
|
s.db, err = sql.Open("mysql", config.User+":"+config.Pass+"@tcp("+config.Host+":"+config.Port+")/"+config.Dbase)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Close() error {
|
||||||
|
return s.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Create(ev event.Event) (event.ID, error) {
|
||||||
|
res, err := s.db.Exec(
|
||||||
|
`INSERT INTO events
|
||||||
|
(title, date, latency, note, userID, notifyTime) VALUES
|
||||||
|
($1, $2, $3, $4, $5, $6)`,
|
||||||
|
ev.Title,
|
||||||
|
ev.Date.Format(dateTimeLayout),
|
||||||
|
ev.Latency,
|
||||||
|
ev.Note,
|
||||||
|
ev.UserID,
|
||||||
|
ev.NotifyTime,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
idint64, err := res.LastInsertId()
|
||||||
|
return event.ID(idint64), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Update(id event.ID, event event.Event) error {
|
||||||
|
_, err := s.db.Exec(
|
||||||
|
`UPDATE events set
|
||||||
|
title=$1, date=$2, latency=$3, note=$4, userID=$5, notifyTime=$6
|
||||||
|
where id=$7`,
|
||||||
|
event.Title,
|
||||||
|
event.Date.Format(dateTimeLayout),
|
||||||
|
event.Latency,
|
||||||
|
event.Note,
|
||||||
|
event.UserID,
|
||||||
|
event.NotifyTime,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Delete(id event.ID) error {
|
||||||
|
_, err := s.db.Exec(
|
||||||
|
`DELETE from events where id=$1`,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) List() (map[event.ID]event.Event, error) {
|
||||||
|
res := make(map[event.ID]event.Event)
|
||||||
|
results, err := s.db.Query(
|
||||||
|
`SELECT (id,title,date,latency,note,userID,notifyTime) from events ORDER BY id`)
|
||||||
|
if err != nil {
|
||||||
|
return map[event.ID]event.Event{}, err
|
||||||
|
}
|
||||||
|
defer results.Close()
|
||||||
|
for results.Next() {
|
||||||
|
var id event.ID
|
||||||
|
var evt event.Event
|
||||||
|
var dateRaw string
|
||||||
|
err = results.Scan(&id, &evt.Title, &dateRaw, &evt.Latency, &evt.Note, &evt.UserID, &evt.NotifyTime)
|
||||||
|
if err != nil {
|
||||||
|
return map[event.ID]event.Event{}, err
|
||||||
|
}
|
||||||
|
evt.Date, err = time.Parse(dateTimeLayout, dateRaw)
|
||||||
|
if err != nil {
|
||||||
|
return map[event.ID]event.Event{}, err
|
||||||
|
}
|
||||||
|
res[id] = evt
|
||||||
|
}
|
||||||
|
if results.Err() != nil {
|
||||||
|
return map[event.ID]event.Event{}, results.Err()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetByID(id event.ID) (event.Event, bool) {
|
||||||
|
var res event.Event
|
||||||
|
var dateRaw string
|
||||||
|
err := s.db.QueryRow(
|
||||||
|
`SELECT (id,title,date,latency,note,userID,notifyTime) from events where id=$1`, id).Scan(res.Title, dateRaw, res.Latency, res.Note, res.UserID, res.NotifyTime)
|
||||||
|
if err != nil {
|
||||||
|
return res, false
|
||||||
|
}
|
||||||
|
dateParced, err := time.Parse(dateTimeLayout, dateRaw)
|
||||||
|
if err != nil {
|
||||||
|
return res, false
|
||||||
|
}
|
||||||
|
res.Date = dateParced
|
||||||
|
return res, true
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
package sqlstorage
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type Storage struct {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *Storage {
|
|
||||||
return &Storage{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Connect(ctx context.Context) error {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Storage) Close(ctx context.Context) error {
|
|
||||||
// TODO
|
|
||||||
}
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage/event"
|
||||||
|
memorystorage "github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage/memory"
|
||||||
|
sqlstorage "github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/storage/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
InMemory bool
|
||||||
|
SQLHost string
|
||||||
|
SQLPort string
|
||||||
|
SQLDbase string
|
||||||
|
SQLUser string
|
||||||
|
SQLPass string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageInterface interface {
|
||||||
|
Create(event event.Event) (event.ID, error)
|
||||||
|
Update(id event.ID, event event.Event) error
|
||||||
|
Delete(id event.ID) error
|
||||||
|
List() (map[event.ID]event.Event, error)
|
||||||
|
GetByID(id event.ID) (event.Event, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(conf Config) StorageInterface {
|
||||||
|
if conf.InMemory {
|
||||||
|
st := memorystorage.New()
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
sqlConf := sqlstorage.Config{
|
||||||
|
Host: conf.SQLHost,
|
||||||
|
Port: conf.SQLPort,
|
||||||
|
Dbase: conf.SQLDbase,
|
||||||
|
User: conf.SQLUser,
|
||||||
|
Pass: conf.SQLPass,
|
||||||
|
}
|
||||||
|
return sqlstorage.New(sqlConf)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE events (
|
||||||
|
id int(16) NOT NULL AUTO_INCREMENT,
|
||||||
|
title varchar(255) NOT NULL,
|
||||||
|
date datetime NOT NULL,
|
||||||
|
latency int(16) NOT NULL,
|
||||||
|
note text,
|
||||||
|
userID int(16),
|
||||||
|
notifyTime int(16)
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE events;
|
||||||
|
-- +goose StatementEnd
|
Loading…
Reference in New Issue