Merge pull request #13 from tiburon-777/hw12_13_14_15_calendar

HW12 completed
master
Andrey Ivanov 2020-09-27 09:41:33 +03:00 committed by GitHub
commit fc19ac2e89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 660 additions and 128 deletions

View File

@ -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

View File

@ -1,14 +0,0 @@
package main
// При желании конфигурацию можно вынести в internal/config.
// Организация конфига в main принуждает нас сужать API компонентов, использовать
// при их конструировании только необходимые параметры, а также уменьшает вероятность циклической зависимости.
type Config struct {
// TODO
}
func NewConfig() Config {
return Config{}
}
// TODO

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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
)

View File

@ -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

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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")
})
}

View File

@ -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
})
}

View File

@ -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

View File

@ -1,7 +0,0 @@
package storage
type Event struct {
ID string
Title string
// TODO
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
})
}

View File

@ -1,14 +0,0 @@
package memorystorage
import "sync"
type Storage struct {
// TODO
mu sync.RWMutex
}
func New() *Storage {
return &Storage{}
}
// TODO

View File

@ -1,7 +0,0 @@
package memorystorage
import "testing"
func TestStorage(t *testing.T) {
// TODO
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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