commit
fc19ac2e89
|
@ -1,13 +1,16 @@
|
|||
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:
|
||||
go test -race ./internal/... ./pkg/...
|
||||
|
||||
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
|
||||
go test -race ./internal/...
|
||||
|
||||
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 (
|
||||
"flag"
|
||||
oslog "log"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/app"
|
||||
"github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/logger"
|
||||
internalhttp "github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/server/http"
|
||||
memorystorage "github.com/fixme_my_friend/hw12_13_14_15_calendar/internal/storage/memory"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/app"
|
||||
"github.com/tiburon-777/HW_OTUS/hw12_13_14_15_calendar/internal/config"
|
||||
"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
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&configFile, "config", "/etc/calendar/config.toml", "Path to configuration file")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := NewConfig()
|
||||
logg := logger.New(config.Logger.Level)
|
||||
conf, err := config.NewConfig(configFile)
|
||||
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(logg, storage)
|
||||
calendar := app.New(log, st)
|
||||
|
||||
server := internalhttp.NewServer(calendar)
|
||||
server := internalhttp.NewServer(calendar, conf.Server.Address, conf.Server.Port)
|
||||
|
||||
go func() {
|
||||
signals := make(chan os.Signal, 1)
|
||||
|
@ -34,12 +52,12 @@ func main() {
|
|||
signal.Stop(signals)
|
||||
|
||||
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 {
|
||||
logger.Error("failed to start http server: " + err.String())
|
||||
log.Errorf("failed to start http server: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
[logger]
|
||||
level = "INFO"
|
||||
[Server]
|
||||
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
|
||||
|
||||
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 (
|
||||
"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 {
|
||||
// TODO
|
||||
Storage store.StorageInterface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
// TODO
|
||||
func New(logger logger.Interface, storage store.StorageInterface) *App {
|
||||
return &App{Logger: logger, Storage: storage}
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
// TODO
|
||||
func (a *App) CreateEvent(ctx context.Context, title string) (err error) {
|
||||
_, 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 {
|
||||
return &App{}
|
||||
func (a *App) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
_, _ = w.Write([]byte("Hello! I'm calendar app!"))
|
||||
}
|
||||
|
||||
func (a *App) CreateEvent(ctx context.Context, id string, title string) error {
|
||||
return a.storage.CreateEvent(storage.Event{ID: id, Title: title})
|
||||
func (a *App) LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
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
|
||||
|
||||
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 {
|
||||
// TODO
|
||||
Logger amitralog.Logger
|
||||
}
|
||||
|
||||
func New(level string) *Logger {
|
||||
return &Logger{}
|
||||
func New(conf config.Config) (Interface, error) {
|
||||
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
|
||||
|
||||
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) {
|
||||
// TODO
|
||||
func TestLoggerLogic(t *testing.T) {
|
||||
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 {
|
||||
// TODO
|
||||
server *http.Server
|
||||
app app.App
|
||||
}
|
||||
|
||||
type Application interface {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func NewServer(app Application) *Server {
|
||||
return &Server{}
|
||||
func NewServer(app *app.App, address string, port string) *Server {
|
||||
return &Server{server: &http.Server{Addr: net.JoinHostPort(address, port), Handler: app.LoggingMiddleware(app.Handler)}, app: *app}
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
// TODO
|
||||
if err := s.server.Close(); err != nil {
|
||||
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