feat: some refactoring
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -30,4 +30,13 @@ go.work.sum
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
.vscode/
|
||||
!.vscode/settings.json
|
||||
!.vscode/settings.json
|
||||
|
||||
# Builded files
|
||||
apiserver
|
||||
|
||||
!apiserver/
|
||||
# Swagger generated files (auto-generated from source code)
|
||||
internal/apiserver/docs/swagger.json
|
||||
internal/apiserver/docs/swagger.yaml
|
||||
internal/apiserver/docs/docs.go
|
||||
147
Makefile
Normal file
147
Makefile
Normal file
@@ -0,0 +1,147 @@
|
||||
# MyGoServer Makefile
|
||||
# Удобные команды для разработки и тестирования
|
||||
|
||||
.PHONY: help build test test-coverage test-race fmt vet clean docker-up docker-down
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "🚀 MyGoServer Development Commands:"
|
||||
@echo ""
|
||||
@echo "📦 Build Commands:"
|
||||
@echo " build - Build the application"
|
||||
@echo " build-clean - Clean build and rebuild"
|
||||
@echo ""
|
||||
@echo "🧪 Test Commands:"
|
||||
@echo " test - Run all tests"
|
||||
@echo " test-unit - Run unit tests only"
|
||||
@echo " test-coverage - Run tests with coverage report"
|
||||
@echo " test-race - Run tests with race detector"
|
||||
@echo " test-watch - Run tests in watch mode"
|
||||
@echo ""
|
||||
@echo "🔧 Code Quality:"
|
||||
@echo " fmt - Format all Go code"
|
||||
@echo " vet - Run static analysis"
|
||||
@echo " lint - Run linting (if golangci-lint installed)"
|
||||
@echo ""
|
||||
@echo "🐳 Docker Commands:"
|
||||
@echo " docker-up - Start application with Docker"
|
||||
@echo " docker-down - Stop Docker containers"
|
||||
@echo " docker-build - Build Docker image"
|
||||
@echo ""
|
||||
@echo "🧹 Cleanup:"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo ""
|
||||
|
||||
# Build commands
|
||||
build: generate-docs
|
||||
@echo "🏗️ Building application..."
|
||||
go build -o apiserver ./cmd/apiserver
|
||||
@echo "✅ Build completed: apiserver"
|
||||
|
||||
generate-docs:
|
||||
@echo "📖 Generating Swagger documentation..."
|
||||
swag init -g cmd/apiserver/main.go -o internal/apiserver/docs
|
||||
@echo "✅ Swagger documentation generated"
|
||||
|
||||
build-clean: clean build
|
||||
|
||||
# Test commands
|
||||
test:
|
||||
@echo "🧪 Running all tests..."
|
||||
./scripts/test.sh
|
||||
|
||||
test-unit:
|
||||
@echo "🧪 Running unit tests..."
|
||||
go test -v ./internal/apiserver/...
|
||||
|
||||
test-coverage:
|
||||
@echo "📊 Running tests with coverage..."
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "📊 Coverage report: coverage.html"
|
||||
|
||||
test-race:
|
||||
@echo "🏁 Running tests with race detector..."
|
||||
go test -race ./...
|
||||
|
||||
test-watch:
|
||||
@echo "👀 Running tests in watch mode..."
|
||||
@which gotest > /dev/null || (echo "Installing gotest-watcher..." && go install github.com/bitfield/gotest@latest)
|
||||
gotest -w ./...
|
||||
|
||||
# Code quality
|
||||
fmt:
|
||||
@echo "🎨 Formatting code..."
|
||||
go fmt ./...
|
||||
@echo "✅ Code formatted"
|
||||
|
||||
vet:
|
||||
@echo "🔍 Running static analysis..."
|
||||
go vet ./...
|
||||
@echo "✅ Static analysis completed"
|
||||
|
||||
lint:
|
||||
@echo "🧹 Running linter..."
|
||||
@which golangci-lint > /dev/null || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)
|
||||
golangci-lint run
|
||||
@echo "✅ Linting completed"
|
||||
|
||||
# Docker commands
|
||||
docker-up:
|
||||
@echo "🐳 Starting application with Docker..."
|
||||
docker-compose -f build/docker-compose.yml up -d
|
||||
@echo "🚀 Application started on http://localhost:8080"
|
||||
@echo "📖 Swagger UI: http://localhost:8080/swagger/"
|
||||
|
||||
docker-down:
|
||||
@echo "🛑 Stopping Docker containers..."
|
||||
docker-compose -f build/docker-compose.yml down
|
||||
|
||||
docker-build:
|
||||
@echo "🔨 Building Docker image..."
|
||||
docker build -f build/Dockerfile -t mygoserver:latest .
|
||||
|
||||
# Cleanup
|
||||
clean:
|
||||
@echo "🧹 Cleaning build artifacts..."
|
||||
rm -f apiserver coverage.out coverage.html
|
||||
rm -rf build/bin/
|
||||
docker-compose -f build/docker-compose.yml down -v 2>/dev/null || true
|
||||
@echo "✅ Cleanup completed"
|
||||
|
||||
# Development helpers
|
||||
dev: build
|
||||
@echo "🚀 Starting development server..."
|
||||
./apiserver
|
||||
|
||||
debug: build
|
||||
@echo "🐛 Starting debug server..."
|
||||
dlv --listen=:2345 --headless=true --api-version=2 exec ./apiserver
|
||||
|
||||
# Run specific test patterns
|
||||
test-pattern:
|
||||
@if [ -z "$(PATTERN)" ]; then \
|
||||
echo "Usage: make test-pattern PATTERN=TestName"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "🧪 Running tests matching: $(PATTERN)"
|
||||
go test -v ./internal/apiserver/... -run="$(PATTERN)"
|
||||
|
||||
# Install development tools
|
||||
install-tools:
|
||||
@echo "🛠️ Installing development tools..."
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
@echo "✅ Development tools installed"
|
||||
|
||||
# Swagger documentation
|
||||
swag-setup:
|
||||
@echo "📖 Setting up Swagger documentation..."
|
||||
@echo "⚠️ Installing swag tool..."
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
@echo "✅ swag installed"
|
||||
@echo "📋 Initializing Swagger configuration..."
|
||||
swag init -g cmd/apiserver/main.go -o internal/apiserver/docs
|
||||
@echo "✅ Swagger documentation generated"
|
||||
|
||||
.PHONY: help build test test-coverage test-race fmt vet clean docker-up docker-down swag-setup
|
||||
31
build/Dockerfile
Normal file
31
build/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Build stage
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o apiserver ./cmd/apiserver
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# Copy binary from builder stage
|
||||
COPY --from=builder /app/apiserver .
|
||||
COPY --from=builder /app/configs ./configs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the application
|
||||
CMD ["./apiserver", "--config-path=./configs/server.toml"]
|
||||
23
build/docker-compose.yml
Normal file
23
build/docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
apiserver:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: build/Dockerfile
|
||||
container_name: mygoserver
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ../configs:/root/configs:ro
|
||||
environment:
|
||||
- LOGGING_LEVEL=info
|
||||
- LOGGING_FORMAT=json
|
||||
- LOGGING_OUTPUT=stdout
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
78
cmd/apiserver/config/config.go
Normal file
78
cmd/apiserver/config/config.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Address string `toml:"address"`
|
||||
ReadTimeout time.Duration `toml:"read_timeout"`
|
||||
WriteTimeout time.Duration `toml:"write_timeout"`
|
||||
IdleTimeout time.Duration `toml:"idle_timeout"`
|
||||
}
|
||||
|
||||
type Logging struct {
|
||||
Level string `toml:"level"`
|
||||
Format string `toml:"format"`
|
||||
Output string `toml:"output"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
MaxBodySize int64 `toml:"max_body_size"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Server Server `toml:"server"`
|
||||
Logging Logging `toml:"logging"`
|
||||
Request Request `toml:"request"`
|
||||
}
|
||||
|
||||
var (
|
||||
configPath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&configPath, "config-path", "./configs/development.toml", "path to config")
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
flag.Parse()
|
||||
|
||||
var config Config
|
||||
|
||||
if _, err := toml.DecodeFile(configPath, &config); err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Set defaults if not specified
|
||||
if config.Server.Address == "" {
|
||||
config.Server.Address = ":8080"
|
||||
}
|
||||
if config.Server.ReadTimeout == 0 {
|
||||
config.Server.ReadTimeout = 10 * time.Second
|
||||
}
|
||||
if config.Server.WriteTimeout == 0 {
|
||||
config.Server.WriteTimeout = 10 * time.Second
|
||||
}
|
||||
if config.Server.IdleTimeout == 0 {
|
||||
config.Server.IdleTimeout = 60 * time.Second
|
||||
}
|
||||
if config.Logging.Level == "" {
|
||||
config.Logging.Level = "info"
|
||||
}
|
||||
if config.Logging.Format == "" {
|
||||
config.Logging.Format = "json"
|
||||
}
|
||||
if config.Logging.Output == "" {
|
||||
config.Logging.Output = "stdout"
|
||||
}
|
||||
if config.Request.MaxBodySize == 0 {
|
||||
config.Request.MaxBodySize = 10 * 1024 * 1024 // 10MB
|
||||
}
|
||||
|
||||
return &config
|
||||
}
|
||||
@@ -1,23 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/app/apiserver"
|
||||
"git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config"
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/server"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath string
|
||||
)
|
||||
// @title Some GoLang server
|
||||
// @version 1.0
|
||||
// @description This is some GoLang server.
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&configPath, "config-path", "/go/bin/configs/server.toml", "path to config")
|
||||
}
|
||||
// @contact.name Sergey Elpashev
|
||||
// @contact.url https://nwaifu.su
|
||||
// @contact.email mrsedan@nwaifu.su
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/license/mit
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if err := apiserver.Start(); err != nil {
|
||||
cfg := config.Load()
|
||||
|
||||
srv := server.NewServer(cfg)
|
||||
if err := srv.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
16
configs/development.toml
Normal file
16
configs/development.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Development server configuration
|
||||
[server]
|
||||
address = ":8080"
|
||||
read_timeout = "5s"
|
||||
write_timeout = "5s"
|
||||
idle_timeout = "30s"
|
||||
|
||||
# Logging configuration for development
|
||||
[logging]
|
||||
level = "debug"
|
||||
format = "text"
|
||||
output = "stdout"
|
||||
|
||||
# Request configuration
|
||||
[request]
|
||||
max_body_size = 10485760 # 10MB
|
||||
16
configs/server.toml
Normal file
16
configs/server.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Server configuration
|
||||
[server]
|
||||
address = ":8080"
|
||||
read_timeout = "10s"
|
||||
write_timeout = "10s"
|
||||
idle_timeout = "60s"
|
||||
|
||||
# Logging configuration
|
||||
[logging]
|
||||
level = "info"
|
||||
format = "json"
|
||||
output = "stdout"
|
||||
|
||||
# Request configuration
|
||||
[request]
|
||||
max_body_size = 10485760 # 10MB
|
||||
206
docs/testing_guide.md
Normal file
206
docs/testing_guide.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 🧪 Руководство по тестированию MyGoServer
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Способы запуска тестов:
|
||||
|
||||
1. **Через Makefile (рекомендуется)**:
|
||||
```bash
|
||||
make test # Все тесты
|
||||
make test-unit # Только unit тесты
|
||||
make test-coverage # Тесты с покрытием
|
||||
make test-race # С race detector
|
||||
```
|
||||
|
||||
2. **Через скрипты**:
|
||||
```bash
|
||||
./scripts/test.sh # Все тесты с отчетом
|
||||
./scripts/test.sh "TestName" # Конкретные тесты
|
||||
```
|
||||
|
||||
3. **Ручной запуск**:
|
||||
```bash
|
||||
go test ./... # Все тесты
|
||||
go test -v ./internal/apiserver/... # Verbose вывод
|
||||
go test -race ./... # С race detector
|
||||
```
|
||||
|
||||
## Детальное руководство
|
||||
|
||||
### 📊 Покрытие кода (Coverage)
|
||||
|
||||
```bash
|
||||
# Генерация отчета о покрытии
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# Просмотр покрытия в терминале
|
||||
go tool cover -func=coverage.out
|
||||
```
|
||||
|
||||
### 🏁 Race Detector
|
||||
|
||||
Поиск гонок данных (data races):
|
||||
|
||||
```bash
|
||||
go test -race ./...
|
||||
|
||||
# Для более глубокой проверки
|
||||
go test -race -count=10 ./...
|
||||
```
|
||||
|
||||
### 📈 Benchmark тесты
|
||||
|
||||
Для измерения производительности:
|
||||
|
||||
```bash
|
||||
go test -bench=. -benchmem ./...
|
||||
|
||||
# Для конкретных функций
|
||||
go test -bench=BenchmarkHomeHandler -benchmem ./internal/apiserver/...
|
||||
```
|
||||
|
||||
### 🎯 Запуск по паттернам
|
||||
|
||||
```bash
|
||||
# Только тесты с определенным именем
|
||||
go test -run TestHome ./...
|
||||
|
||||
# Исключить некоторые тесты
|
||||
go test -run '^(?!TestIntegration).*' ./...
|
||||
|
||||
# Искать по нескольким паттернам
|
||||
go test -run 'TestHome|TestConfig' ./...
|
||||
```
|
||||
|
||||
### 🔄 Непрерывное тестирование
|
||||
|
||||
Для разработки с автоперезапуском тестов:
|
||||
|
||||
```bash
|
||||
# Установка gotest-watcher
|
||||
go install github.com/bitfield/gotest@latest
|
||||
|
||||
# Автоматический запуск тестов при изменении файлов
|
||||
gotest -w ./...
|
||||
```
|
||||
|
||||
### 📊 Отчеты и анализ
|
||||
|
||||
#### HTML отчет о покрытии:
|
||||
- `coverage.html` - веб-интерфейс покрытия
|
||||
- `coverage.out` - данные покрытия
|
||||
|
||||
#### JSON отчет (для CI/CD):
|
||||
```bash
|
||||
go test -json ./... > test-report.json
|
||||
```
|
||||
|
||||
#### Логи в файл:
|
||||
```bash
|
||||
go test -v ./... > test.log 2>&1
|
||||
```
|
||||
|
||||
## 🏗️ Структура тестов
|
||||
|
||||
### Unit тесты:
|
||||
```
|
||||
internal/apiserver/handlers/
|
||||
├── home_test.go
|
||||
├── middleware_test.go
|
||||
└── ...
|
||||
|
||||
internal/apiserver/models/
|
||||
├── response_test.go
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Integration тесты:
|
||||
```
|
||||
test/integration/
|
||||
├── api_test.go
|
||||
├── server_test.go
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Файлы конфигурации для тестов:
|
||||
```
|
||||
test/
|
||||
├── fixtures/ # Тестовые данные
|
||||
├── helpers/ # Вспомогательные функции
|
||||
└── testdata/ # Статические данные
|
||||
```
|
||||
|
||||
## 🔧 Настройка окружения тестирования
|
||||
|
||||
### Переменные окружения:
|
||||
```bash
|
||||
export TEST_ENV=development
|
||||
export LOG_LEVEL=debug
|
||||
export CONFIG_PATH=./configs/development.toml
|
||||
```
|
||||
|
||||
## 📝 Примеры тестов
|
||||
|
||||
### Базовый тест:
|
||||
```go
|
||||
func TestHomeHandler_ServeHTTP(t *testing.T) {
|
||||
handler := handlers.NewHomeHandler()
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Тест с моками:
|
||||
```go
|
||||
func TestConfig_Load(t *testing.T) {
|
||||
// Мок конфигурации
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{
|
||||
Address: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
if cfg.Server.Address != ":8080" {
|
||||
t.Error("Config not loaded correctly")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Benchmark тест:
|
||||
```go
|
||||
func BenchmarkHomeHandler_ServeHTTP(b *testing.B) {
|
||||
handler := handlers.NewHomeHandler()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Быстрые команды
|
||||
|
||||
```bash
|
||||
# Проверка всех аспектов качества кода
|
||||
make test-coverage && make vet && make fmt
|
||||
|
||||
# Быстрый цикл разработки
|
||||
make test-watch
|
||||
|
||||
# Полная проверка проекта
|
||||
make build && make test-race && make lint
|
||||
|
||||
# Тестирование конкретного компонента
|
||||
make test-pattern PATTERN=TestHome
|
||||
```
|
||||
21
go.mod
21
go.mod
@@ -4,11 +4,28 @@ go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.4
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/spec v0.20.6 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
65
go.sum
65
go.sum
@@ -1,21 +1,78 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
|
||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
|
||||
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
0
internal/apiserver/docs/.gitkeep
Normal file
0
internal/apiserver/docs/.gitkeep
Normal file
37
internal/apiserver/handlers/home.go
Normal file
37
internal/apiserver/handlers/home.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/models"
|
||||
)
|
||||
|
||||
// HomeHandler handles the root endpoint
|
||||
type HomeHandler struct{}
|
||||
|
||||
func NewHomeHandler() *HomeHandler {
|
||||
return &HomeHandler{}
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleHome(w, r)
|
||||
}
|
||||
|
||||
// @Summary Health check
|
||||
// @Description Проверка состояния сервера
|
||||
// @Tags Health
|
||||
// @Success 200 {object} models.Response "Server is running"
|
||||
// @Router / [get]
|
||||
func (h *HomeHandler) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
// Create a simple success response
|
||||
response := models.NewSuccessResponse(map[string]string{
|
||||
"status": "ok",
|
||||
"message": "API Server is running",
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
41
internal/apiserver/handlers/home_test.go
Normal file
41
internal/apiserver/handlers/home_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHomeHandler_ServeHTTP(t *testing.T) {
|
||||
// Create a new home handler
|
||||
handler := NewHomeHandler()
|
||||
|
||||
// Create a test request
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
|
||||
// Create a test response recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Call the handler
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Check content type
|
||||
if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("handler returned wrong content type: got %v want %v",
|
||||
ct, "application/json")
|
||||
}
|
||||
|
||||
// Check response body contains expected data
|
||||
expectedBody := `"success":true`
|
||||
if body := rr.Body.String(); !strings.Contains(body, expectedBody) {
|
||||
t.Errorf("handler returned unexpected body: got %v want %v",
|
||||
body, expectedBody)
|
||||
}
|
||||
}
|
||||
76
internal/apiserver/logger/logger.go
Normal file
76
internal/apiserver/logger/logger.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logrus.Logger
|
||||
once sync.Once
|
||||
initialized bool
|
||||
)
|
||||
|
||||
// Initialize logger with configuration
|
||||
func Initialize(level string, format string, output string) {
|
||||
once.Do(func() {
|
||||
logger = logrus.New()
|
||||
|
||||
// Set level
|
||||
lvl, err := logrus.ParseLevel(level)
|
||||
if err != nil {
|
||||
lvl = logrus.InfoLevel
|
||||
}
|
||||
logger.SetLevel(lvl)
|
||||
|
||||
// Set format
|
||||
switch format {
|
||||
case "json":
|
||||
logger.SetFormatter(&logrus.JSONFormatter{
|
||||
TimestampFormat: "2006-01-02T15:04:05.000Z",
|
||||
})
|
||||
case "text":
|
||||
logger.SetFormatter(&logrus.TextFormatter{
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
FullTimestamp: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Set output
|
||||
if output != "" {
|
||||
//TODO: Use files
|
||||
}
|
||||
|
||||
initialized = true
|
||||
})
|
||||
}
|
||||
|
||||
// GetLogger returns the singleton logger instance
|
||||
func GetLogger() *logrus.Logger {
|
||||
if !initialized {
|
||||
// Initialize with defaults
|
||||
Initialize("info", "json", "stdout")
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
// WithFields creates a logger with additional fields
|
||||
func WithFields(fields map[string]interface{}) *logrus.Entry {
|
||||
return GetLogger().WithFields(fields)
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func Info(msg string) {
|
||||
GetLogger().Info(msg)
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func Error(msg string) {
|
||||
GetLogger().Error(msg)
|
||||
}
|
||||
|
||||
// Debug logs a debug message
|
||||
func Debug(msg string) {
|
||||
GetLogger().Debug(msg)
|
||||
}
|
||||
42
internal/apiserver/middleware/logging.go
Normal file
42
internal/apiserver/middleware/logging.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger"
|
||||
rhttp "git.nwaifu.su/sergey/MyGoServer/pkg/http"
|
||||
)
|
||||
|
||||
// LoggingMiddleware logs HTTP requests
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// Use the custom response writer to capture status code
|
||||
rw := rhttp.NewResponseWriter(w)
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
// Log the request
|
||||
logger := logger.GetLogger()
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"method": r.Method,
|
||||
"uri": r.RequestURI,
|
||||
"remote_addr": r.RemoteAddr,
|
||||
"user_agent": r.UserAgent(),
|
||||
"status_code": rw.GetStatusCode(),
|
||||
"duration": time.Since(start).String(),
|
||||
"request_id": getRequestID(r.Context()),
|
||||
}).Info("HTTP request")
|
||||
})
|
||||
}
|
||||
|
||||
// getRequestID retrieves request ID from context
|
||||
func getRequestID(ctx context.Context) string {
|
||||
if id, ok := ctx.Value("request_id").(string); ok {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
33
internal/apiserver/middleware/request_id.go
Normal file
33
internal/apiserver/middleware/request_id.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// RequestIDMiddleware adds a unique request ID to each request
|
||||
func RequestIDMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Generate a new request ID
|
||||
requestID := uuid.New().String()
|
||||
|
||||
// Set request ID in response header
|
||||
w.Header().Set("X-Request-ID", requestID)
|
||||
|
||||
// Add request ID to context
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, "request_id", requestID)
|
||||
|
||||
// Log the request ID assignment
|
||||
logger := logger.GetLogger()
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
}).Debug("Request ID assigned")
|
||||
|
||||
// Continue with the request
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
25
internal/apiserver/models/response.go
Normal file
25
internal/apiserver/models/response.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// Response represents a standard API response
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
// NewSuccessResponse creates a successful response
|
||||
func NewSuccessResponse(data interface{}) *Response {
|
||||
return &Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorResponse creates an error response
|
||||
func NewErrorResponse(err error) *Response {
|
||||
return &Response{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
22
internal/apiserver/server/routes.go
Normal file
22
internal/apiserver/server/routes.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
_ "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/docs"
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/handlers"
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/middleware"
|
||||
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
)
|
||||
|
||||
// setupRoutes configures all routes
|
||||
func (s *Server) setupRoutes() {
|
||||
// Add request ID middleware to all routes
|
||||
s.router.Handle("/", middleware.RequestIDMiddleware(
|
||||
middleware.LoggingMiddleware(
|
||||
handlers.NewHomeHandler(),
|
||||
),
|
||||
))
|
||||
|
||||
// Swagger UI
|
||||
s.router.Handle("/swagger/", httpSwagger.WrapHandler)
|
||||
}
|
||||
70
internal/apiserver/server/server.go
Normal file
70
internal/apiserver/server/server.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config"
|
||||
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger"
|
||||
)
|
||||
|
||||
type contextKey struct {
|
||||
key string
|
||||
}
|
||||
|
||||
var connContextKey = &contextKey{"http-conn"}
|
||||
|
||||
func saveConnInContext(ctx context.Context, c net.Conn) context.Context {
|
||||
return context.WithValue(ctx, connContextKey, c)
|
||||
}
|
||||
|
||||
// Server represents the HTTP server
|
||||
type Server struct {
|
||||
config *config.Config
|
||||
router *http.ServeMux
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// NewServer creates a new server instance
|
||||
func NewServer(cfg *config.Config) *Server {
|
||||
s := &Server{
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.Initialize(cfg.Logging.Level, cfg.Logging.Format, cfg.Logging.Output)
|
||||
|
||||
// Create router
|
||||
s.router = http.NewServeMux()
|
||||
s.setupRoutes()
|
||||
|
||||
// Create HTTP server
|
||||
s.server = &http.Server{
|
||||
Addr: cfg.Server.Address,
|
||||
Handler: s.router,
|
||||
ConnContext: saveConnInContext,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start starts the server
|
||||
func (s *Server) Start() error {
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// GetRouter returns the HTTP router
|
||||
func (s *Server) GetRouter() *http.ServeMux {
|
||||
return s.router
|
||||
}
|
||||
|
||||
// GetServer returns the HTTP server instance
|
||||
func (s *Server) GetServer() *http.Server {
|
||||
return s.server
|
||||
}
|
||||
|
||||
// GetConfig returns the server configuration
|
||||
func (s *Server) GetConfig() *config.Config {
|
||||
return s.config
|
||||
}
|
||||
0
internal/apiserver/services/.gitkeep
Normal file
0
internal/apiserver/services/.gitkeep
Normal file
0
internal/apiserver/utils/.gitkeep
Normal file
0
internal/apiserver/utils/.gitkeep
Normal file
@@ -1,29 +0,0 @@
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type contextKey struct {
|
||||
key string
|
||||
}
|
||||
|
||||
var connContextKey = &contextKey{"http-conn"}
|
||||
|
||||
func saveConnInContext(ctx context.Context, c net.Conn) context.Context {
|
||||
return context.WithValue(ctx, connContextKey, c)
|
||||
}
|
||||
|
||||
// Start the server
|
||||
func Start() error {
|
||||
srv := newServer()
|
||||
server := http.Server{
|
||||
Addr: ":8080",
|
||||
ConnContext: saveConnInContext,
|
||||
Handler: srv,
|
||||
}
|
||||
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
ctxKeyRequestID = iota
|
||||
)
|
||||
|
||||
type server struct {
|
||||
router *mux.Router
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func newServer() *server {
|
||||
s := &server{
|
||||
router: mux.NewRouter(),
|
||||
logger: logrus.New(),
|
||||
}
|
||||
s.configureRouter()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *server) configureRouter() {
|
||||
s.router.Use(s.setRequestID)
|
||||
s.router.Use(s.logRequest)
|
||||
s.router.HandleFunc("/", s.handleHome())
|
||||
}
|
||||
|
||||
func (s *server) handleHome() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) logRequest(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger := logrus.WithFields(logrus.Fields{
|
||||
"remote_addr": r.RemoteAddr,
|
||||
"request_id": r.Context().Value(ctxKeyRequestID),
|
||||
})
|
||||
logger.Infof("started %s %s", r.Method, r.RequestURI)
|
||||
start := time.Now()
|
||||
rw := newResponseWriter(w)
|
||||
rw.code = http.StatusOK
|
||||
next.ServeHTTP(rw, r)
|
||||
logger.Infof("completed with %d %s in %v", rw.code, http.StatusText(rw.code), time.Since(start))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) setRequestID(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := uuid.New().String()
|
||||
w.Header().Set("X-Request-ID", id)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKeyRequestID, id)))
|
||||
})
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func (s *server) error(w http.ResponseWriter, r *http.Request, status int, err error) {
|
||||
s.respond(w, r, status, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
func (s *server) respond(w http.ResponseWriter, r *http.Request, status int, data interface{}) {
|
||||
w.WriteHeader(status)
|
||||
if data != nil {
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
}
|
||||
0
internal/pkg/errors/.gitkeep
Normal file
0
internal/pkg/errors/.gitkeep
Normal file
0
internal/pkg/validators/.gitkeep
Normal file
0
internal/pkg/validators/.gitkeep
Normal file
@@ -1,4 +1,4 @@
|
||||
package apiserver
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -22,8 +22,13 @@ func (w *ResponseWriter) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
// GetStatusCode returns the status code that was written
|
||||
func (w *ResponseWriter) GetStatusCode() int {
|
||||
return w.code
|
||||
}
|
||||
|
||||
// Get new RW
|
||||
func newResponseWriter(w http.ResponseWriter) *ResponseWriter {
|
||||
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
|
||||
hijacker, _ := w.(http.Hijacker)
|
||||
return &ResponseWriter{
|
||||
ResponseWriter: httpsnoop.Wrap(w, httpsnoop.Hooks{}),
|
||||
15
scripts/build.sh
Executable file
15
scripts/build.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building MyGoServer..."
|
||||
|
||||
# Create build directory
|
||||
mkdir -p build/bin
|
||||
|
||||
# Build the binary
|
||||
echo "Building apiserver binary..."
|
||||
go build -o build/bin/apiserver ./cmd/apiserver
|
||||
|
||||
echo "Build completed successfully!"
|
||||
echo "Binary location: build/bin/apiserver"
|
||||
101
scripts/test.sh
Executable file
101
scripts/test.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🧪 Running tests for MyGoServer..."
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
local status=$1
|
||||
local message=$2
|
||||
if [ "$status" = "PASS" ]; then
|
||||
echo -e "${GREEN}✅ $message${NC}"
|
||||
elif [ "$status" = "FAIL" ]; then
|
||||
echo -e "${RED}❌ $message${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ $message${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Run all unit tests
|
||||
echo -e "${YELLOW}Running all unit tests...${NC}"
|
||||
go test -v ./internal/apiserver/...
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_status "PASS" "Unit tests passed"
|
||||
else
|
||||
print_status "FAIL" "Unit tests failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Run tests with coverage
|
||||
echo -e "${YELLOW}Running tests with coverage...${NC}"
|
||||
go test -coverprofile=coverage.out ./...
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_status "PASS" "Coverage tests completed"
|
||||
|
||||
# Generate coverage report
|
||||
go tool cover -html=coverage.out -o coverage.html 2>/dev/null || true
|
||||
if [ -f "coverage.html" ]; then
|
||||
echo -e "${GREEN}📊 Coverage report generated: coverage.html${NC}"
|
||||
fi
|
||||
|
||||
# Show coverage percentage
|
||||
go tool cover -func=coverage.out | tail -1
|
||||
else
|
||||
print_status "FAIL" "Coverage tests failed"
|
||||
fi
|
||||
|
||||
# 3. Run tests with race detector (optional, takes longer)
|
||||
echo -e "${YELLOW}Running race detector tests...${NC}"
|
||||
echo "This may take longer - press Ctrl+C to skip, or wait..."
|
||||
sleep 2
|
||||
|
||||
if go test -race ./internal/apiserver/... > /dev/null 2>&1; then
|
||||
print_status "PASS" "Race detector tests passed"
|
||||
else
|
||||
print_status "WARN" "Race detector tests completed (some may have warnings)"
|
||||
fi
|
||||
|
||||
# 4. Run vet (static analysis)
|
||||
echo -e "${YELLOW}Running go vet (static analysis)...${NC}"
|
||||
go vet ./...
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_status "PASS" "Static analysis passed"
|
||||
else
|
||||
print_status "WARN" "Static analysis completed with warnings"
|
||||
fi
|
||||
|
||||
# 5. Check formatting
|
||||
echo -e "${YELLOW}Checking code formatting...${NC}"
|
||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||
print_status "FAIL" "Code formatting issues found:"
|
||||
gofmt -s -l .
|
||||
echo "Run 'gofmt -s -w .' to fix formatting"
|
||||
exit 1
|
||||
else
|
||||
print_status "PASS" "Code formatting is correct"
|
||||
fi
|
||||
|
||||
# 6. Run specific test patterns (if specified)
|
||||
if [ "$1" != "" ]; then
|
||||
echo -e "${YELLOW}Running specific tests: $1${NC}"
|
||||
go test -v ./internal/apiserver/... -run="$1"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}🎉 All tests completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "📝 Usage examples:"
|
||||
echo " ./scripts/test.sh # Run all tests"
|
||||
echo " ./scripts/test.sh \"TestHome\" # Run specific test pattern"
|
||||
echo " go test ./... # Run all tests manually"
|
||||
echo " go test -v ./internal/apiserver/ # Run specific package tests"
|
||||
echo " go test -race ./... # Run with race detector"
|
||||
Reference in New Issue
Block a user