first commit

This commit is contained in:
i.smyshlyaev 2025-10-16 12:46:23 +03:00
commit 9c813d4754
37 changed files with 1671 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/awesome_cli.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/awesome_cli.iml" filepath="$PROJECT_DIR$/.idea/awesome_cli.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

60
back/cmd/main.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"awesome-back/internal/datastore"
"awesome-back/internal/handler"
"awesome-back/internal/service"
"github.com/gin-gonic/gin"
)
func main() {
// Инициализация репозиториев
coffeeRepo := datastore.NewMockCoffeeRepository()
pastryRepo := datastore.NewMockPastryRepository()
orderRepo := datastore.NewMockOrderRepository()
// Инициализация сервисов
coffeeService := service.NewCoffeeService(coffeeRepo)
pastryService := service.NewPastryService(pastryRepo)
orderService := service.NewOrderService(orderRepo)
// Инициализация обработчиков
coffeeHandler := handler.NewCoffeeHandler(coffeeService)
pastryHandler := handler.NewPastryHandler(pastryService)
orderHandler := handler.NewOrderHandler(orderService)
// Настройка маршрутов
router := gin.Default()
setupRoutes(router, coffeeHandler, pastryHandler, orderHandler)
router.Run(":8080")
}
func setupRoutes(router *gin.Engine, coffeeHandler *handler.CoffeeHandler, pastryHandler *handler.PastryHandler, orderHandler *handler.OrderHandler) {
api := router.Group("/api/v1")
{
// Кофе
coffeeRoutes := api.Group("/coffees")
{
coffeeRoutes.GET("/", coffeeHandler.GetCoffees)
coffeeRoutes.GET("/:id", coffeeHandler.GetCoffeeByID)
}
// Выпечка
pastryRoutes := api.Group("/pastries")
{
pastryRoutes.GET("/", pastryHandler.GetPastries)
pastryRoutes.GET("/:id", pastryHandler.GetPastryByID)
pastryRoutes.GET("/category/:category", pastryHandler.GetPastriesByCategory)
}
// Заказы
orderRoutes := api.Group("/orders")
{
orderRoutes.GET("/", orderHandler.GetOrders)
orderRoutes.GET("/:id", orderHandler.GetOrderByID)
orderRoutes.POST("/", orderHandler.CreateOrder)
}
}
}

39
back/go.mod Normal file
View File

@ -0,0 +1,39 @@
module awesome-back
go 1.25.1
require github.com/gin-gonic/gin v1.11.0
require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

88
back/go.sum Normal file
View File

@ -0,0 +1,88 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,34 @@
package datastore
import (
"awesome-back/internal/domain/entities"
"awesome-back/internal/domain/repositories"
"awesome-back/pkg/errors"
)
type MockCoffeeRepository struct {
coffees []entities.Coffee
}
func NewMockCoffeeRepository() repositories.CoffeeRepository {
return &MockCoffeeRepository{
coffees: []entities.Coffee{
{ID: 1, Name: "Эспрессо", Description: "Крепкий черный кофе", Price: 120.0, Size: "S"},
{ID: 2, Name: "Капучино", Description: "Кофе с молочной пенкой", Price: 180.0, Size: "M"},
// ... остальные данные
},
}
}
func (r *MockCoffeeRepository) FindAll() ([]entities.Coffee, error) {
return r.coffees, nil
}
func (r *MockCoffeeRepository) FindByID(id int) (*entities.Coffee, error) {
for _, coffee := range r.coffees {
if coffee.ID == id {
return &coffee, nil
}
}
return nil, errors.NewNotFoundError("coffee not found")
}

View File

@ -0,0 +1,82 @@
package datastore
import (
"time"
"awesome-back/internal/domain/entities"
"awesome-back/internal/domain/repositories"
"awesome-back/pkg/errors"
)
type MockOrderRepository struct {
orders []entities.Order
nextID int
}
func NewMockOrderRepository() repositories.OrderRepository {
return &MockOrderRepository{
orders: []entities.Order{
{
ID: 1,
Items: []entities.Item{
{ProductID: 1, Quantity: 1},
{ProductID: 1, Quantity: 2},
},
Total: 320.0,
Status: "completed",
Timestamp: "2024-01-15 10:30:00",
},
{
ID: 2,
Items: []entities.Item{
{ProductID: 2, Quantity: 1},
{ProductID: 3, Quantity: 1},
},
Total: 380.0,
Status: "preparing",
Timestamp: "2024-01-15 11:15:00",
},
},
nextID: 3,
}
}
func (r *MockOrderRepository) FindAll() ([]entities.Order, error) {
return r.orders, nil
}
func (r *MockOrderRepository) FindByID(id int) (*entities.Order, error) {
for _, order := range r.orders {
if order.ID == id {
return &order, nil
}
}
return nil, errors.NewNotFoundError("order not found")
}
func (r *MockOrderRepository) Save(order *entities.Order) error {
if order == nil {
return errors.NewInvalidError("order cannot be nil")
}
// Если это новый заказ (ID = 0), присваиваем следующий ID
if order.ID == 0 {
order.ID = r.nextID
order.Timestamp = time.Now().Format("2006-01-02 15:04:05")
if order.Status == "" {
order.Status = "created"
}
r.orders = append(r.orders, *order)
r.nextID++
} else {
// Обновление существующего заказа
for i, existingOrder := range r.orders {
if existingOrder.ID == order.ID {
r.orders[i] = *order
break
}
}
}
return nil
}

View File

@ -0,0 +1,51 @@
package datastore
import (
"awesome-back/internal/domain/entities"
"awesome-back/internal/domain/repositories"
"awesome-back/pkg/errors"
)
type MockPastryRepository struct {
pastries []entities.Pastry
}
func NewMockPastryRepository() repositories.PastryRepository {
return &MockPastryRepository{
pastries: []entities.Pastry{
{ID: 1, Name: "Круассан", Description: "Слоеная выпечка с маслом", Price: 80.0, Category: "Выпечка"},
{ID: 2, Name: "Тирамису", Description: "Итальянский десерт", Price: 150.0, Category: "Десерты"},
{ID: 3, Name: "Чизкейк", Description: "Сырный торт", Price: 130.0, Category: "Десерты"},
{ID: 4, Name: "Маффин", Description: "Шоколадный кекс", Price: 70.0, Category: "Выпечка"},
{ID: 5, Name: "Печенье", Description: "Домашнее овсяное печенье", Price: 50.0, Category: "Печенье"},
},
}
}
func (r *MockPastryRepository) FindAll() ([]entities.Pastry, error) {
return r.pastries, nil
}
func (r *MockPastryRepository) FindByID(id int) (*entities.Pastry, error) {
for _, pastry := range r.pastries {
if pastry.ID == id {
return &pastry, nil
}
}
return nil, errors.NewNotFoundError("pastry not found")
}
func (r *MockPastryRepository) FindByCategory(category string) ([]entities.Pastry, error) {
var filtered []entities.Pastry
for _, pastry := range r.pastries {
if pastry.Category == category {
filtered = append(filtered, pastry)
}
}
if len(filtered) == 0 {
return nil, errors.NewNotFoundError("no pastries found for category")
}
return filtered, nil
}

View File

@ -0,0 +1,9 @@
package entities
type Coffee struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Size string `json:"size"`
}

View File

@ -0,0 +1,14 @@
package entities
type Order struct {
ID int `json:"id"`
Items []Item `json:"items"`
Total float64 `json:"total"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
}
type Item struct {
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
}

View File

@ -0,0 +1,9 @@
package entities
type Pastry struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Category string `json:"category"`
}

View File

@ -0,0 +1,8 @@
package repositories
import "awesome-back/internal/domain/entities"
type CoffeeRepository interface {
FindAll() ([]entities.Coffee, error)
FindByID(id int) (*entities.Coffee, error)
}

View File

@ -0,0 +1,9 @@
package repositories
import "awesome-back/internal/domain/entities"
type OrderRepository interface {
FindAll() ([]entities.Order, error)
FindByID(id int) (*entities.Order, error)
Save(order *entities.Order) error
}

View File

@ -0,0 +1,9 @@
package repositories
import "awesome-back/internal/domain/entities"
type PastryRepository interface {
FindAll() ([]entities.Pastry, error)
FindByID(id int) (*entities.Pastry, error)
FindByCategory(category string) ([]entities.Pastry, error)
}

View File

@ -0,0 +1,45 @@
package handler
import (
"strconv"
"awesome-back/internal/service"
"awesome-back/pkg/errors"
"github.com/gin-gonic/gin"
)
type CoffeeHandler struct {
coffeeService *service.CoffeeService
}
func NewCoffeeHandler(coffeeService *service.CoffeeService) *CoffeeHandler {
return &CoffeeHandler{coffeeService: coffeeService}
}
func (h *CoffeeHandler) GetCoffees(c *gin.Context) {
coffees, err := h.coffeeService.GetAllCoffees()
if err != nil {
Error(c, err)
return
}
Success(c, coffees)
}
func (h *CoffeeHandler) GetCoffeeByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
Error(c, errors.NewInvalidError("invalid coffee ID format"))
return
}
coffee, err := h.coffeeService.GetCoffeeByID(id)
if err != nil {
Error(c, err)
return
}
Success(c, coffee)
}

View File

@ -0,0 +1,62 @@
package handler
import (
"strconv"
"awesome-back/internal/domain/entities"
"awesome-back/internal/service"
"awesome-back/pkg/errors"
"github.com/gin-gonic/gin"
)
type OrderHandler struct {
orderService *service.OrderService
}
func NewOrderHandler(orderService *service.OrderService) *OrderHandler {
return &OrderHandler{orderService: orderService}
}
func (h *OrderHandler) GetOrders(c *gin.Context) {
orders, err := h.orderService.GetAllOrders()
if err != nil {
Error(c, err)
return
}
Success(c, orders)
}
func (h *OrderHandler) GetOrderByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
Error(c, errors.NewInvalidError("invalid order ID format"))
return
}
order, err := h.orderService.GetOrderByID(id)
if err != nil {
Error(c, err)
return
}
Success(c, order)
}
func (h *OrderHandler) CreateOrder(c *gin.Context) {
var order entities.Order
if err := c.ShouldBindJSON(&order); err != nil {
Error(c, errors.NewInvalidError("invalid order data"))
return
}
err := h.orderService.CreateOrder(&order)
if err != nil {
Error(c, err)
return
}
Created(c, order)
}

View File

@ -0,0 +1,61 @@
package handler
import (
"strconv"
"awesome-back/internal/service"
"awesome-back/pkg/errors"
"github.com/gin-gonic/gin"
)
type PastryHandler struct {
pastryService *service.PastryService
}
func NewPastryHandler(pastryService *service.PastryService) *PastryHandler {
return &PastryHandler{pastryService: pastryService}
}
func (h *PastryHandler) GetPastries(c *gin.Context) {
pastries, err := h.pastryService.GetAllPastries()
if err != nil {
Error(c, err)
return
}
Success(c, pastries)
}
func (h *PastryHandler) GetPastryByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
Error(c, errors.NewInvalidError("invalid pastry ID format"))
return
}
pastry, err := h.pastryService.GetPastryByID(id)
if err != nil {
Error(c, err)
return
}
Success(c, pastry)
}
func (h *PastryHandler) GetPastriesByCategory(c *gin.Context) {
category := c.Param("category")
if category == "" {
Error(c, errors.NewInvalidError("category parameter is required"))
return
}
pastries, err := h.pastryService.GetPastriesByCategory(category)
if err != nil {
Error(c, err)
return
}
Success(c, pastries)
}

View File

@ -0,0 +1,55 @@
package handler
import (
"errors"
"net/http"
appErr "awesome-back/pkg/errors"
"github.com/gin-gonic/gin"
)
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Success: true,
Data: data,
})
}
func Created(c *gin.Context, data interface{}) {
c.JSON(http.StatusCreated, Response{
Success: true,
Data: data,
})
}
func Error(c *gin.Context, err error) {
var appErr *appErr.AppError
if errors.As(err, &appErr) {
statusCode := appErr.Code
if statusCode == 0 {
statusCode = http.StatusInternalServerError
}
c.JSON(statusCode, Response{
Success: false,
Error: appErr.Message,
})
return
}
// Ошибка по умолчанию
c.JSON(http.StatusInternalServerError, Response{
Success: false,
Error: "Internal server error",
})
}
// Аналогично для PastryHandler и OrderHandler...

View File

@ -0,0 +1,39 @@
package service
import (
"awesome-back/internal/domain/entities"
"awesome-back/internal/domain/repositories"
"awesome-back/pkg/errors"
)
type CoffeeService struct {
repo repositories.CoffeeRepository
}
func NewCoffeeService(repo repositories.CoffeeRepository) *CoffeeService {
return &CoffeeService{repo: repo}
}
func (s *CoffeeService) GetAllCoffees() ([]entities.Coffee, error) {
coffees, err := s.repo.FindAll()
if err != nil {
return nil, errors.Wrap(err, "failed to get coffees")
}
return coffees, nil
}
func (s *CoffeeService) GetCoffeeByID(id int) (*entities.Coffee, error) {
if id <= 0 {
return nil, errors.NewInvalidError("invalid coffee ID")
}
coffee, err := s.repo.FindByID(id)
if err != nil {
if errors.IsNotFound(err) {
return nil, errors.NewNotFoundError("coffee not found")
}
return nil, errors.Wrap(err, "failed to get coffee")
}
return coffee, nil
}

View File

@ -0,0 +1,57 @@
// internal/service/order_service.go
package service
import (
"awesome-back/internal/domain/entities"
"awesome-back/internal/domain/repositories"
"awesome-back/pkg/errors"
)
type OrderService struct {
orderRepo repositories.OrderRepository
}
func NewOrderService(orderRepo repositories.OrderRepository) *OrderService {
return &OrderService{orderRepo: orderRepo}
}
func (s *OrderService) GetAllOrders() ([]entities.Order, error) {
orders, err := s.orderRepo.FindAll()
if err != nil {
return nil, errors.Wrap(err, "failed to get orders")
}
return orders, nil
}
func (s *OrderService) GetOrderByID(id int) (*entities.Order, error) {
if id <= 0 {
return nil, errors.NewInvalidError("invalid order ID")
}
order, err := s.orderRepo.FindByID(id)
if err != nil {
if errors.IsNotFound(err) {
return nil, errors.NewNotFoundError("order not found")
}
return nil, errors.Wrap(err, "failed to get order")
}
return order, nil
}
func (s *OrderService) CreateOrder(order *entities.Order) error {
if order == nil {
return errors.NewInvalidError("order cannot be nil")
}
if len(order.Items) == 0 {
return errors.NewInvalidError("order must contain at least one item")
}
err := s.orderRepo.Save(order)
if err != nil {
return errors.Wrap(err, "failed to create order")
}
return nil
}

View File

@ -0,0 +1,52 @@
package service
import (
"awesome-back/internal/domain/entities"
"awesome-back/internal/domain/repositories"
"awesome-back/pkg/errors"
)
type PastryService struct {
repo repositories.PastryRepository
}
func NewPastryService(repo repositories.PastryRepository) *PastryService {
return &PastryService{repo: repo}
}
func (s *PastryService) GetAllPastries() ([]entities.Pastry, error) {
pastries, err := s.repo.FindAll()
if err != nil {
return nil, errors.Wrap(err, "failed to get pastries")
}
return pastries, nil
}
func (s *PastryService) GetPastryByID(id int) (*entities.Pastry, error) {
if id <= 0 {
return nil, errors.NewInvalidError("invalid pastry ID")
}
pastry, err := s.repo.FindByID(id)
if err != nil {
if errors.IsNotFound(err) {
return nil, errors.NewNotFoundError("pastry not found")
}
return nil, errors.Wrap(err, "failed to get pastry")
}
return pastry, nil
}
func (s *PastryService) GetPastriesByCategory(category string) ([]entities.Pastry, error) {
if category == "" {
return nil, errors.NewInvalidError("category cannot be empty")
}
pastries, err := s.repo.FindByCategory(category)
if err != nil {
return nil, errors.Wrap(err, "failed to get pastries by category")
}
return pastries, nil
}

118
back/pkg/errors/errors.go Normal file
View File

@ -0,0 +1,118 @@
package errors
import (
"errors"
"fmt"
)
// ErrorType представляет тип ошибки
type ErrorType string
const (
ErrorTypeInvalid ErrorType = "INVALID"
ErrorTypeNotFound ErrorType = "NOT_FOUND"
ErrorTypeConflict ErrorType = "CONFLICT"
ErrorTypeInternal ErrorType = "INTERNAL"
ErrorTypeUnauthorized ErrorType = "UNAUTHORIZED"
ErrorTypeForbidden ErrorType = "FORBIDDEN"
)
// AppError - кастомная ошибка приложения
type AppError struct {
Type ErrorType `json:"type"`
Message string `json:"message"`
Code int `json:"code,omitempty"`
Err error `json:"-"`
}
// Error реализует интерфейс error
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
// Unwrap для поддержки errors.Unwrap
func (e *AppError) Unwrap() error {
return e.Err
}
// WithCode добавляет HTTP код к ошибке
func (e *AppError) WithCode(code int) *AppError {
e.Code = code
return e
}
// Wrap оборачивает существующую ошибку
func (e *AppError) Wrap(err error) *AppError {
e.Err = err
return e
}
// Конструкторы ошибок
func NewInvalidError(message string) *AppError {
return &AppError{
Type: ErrorTypeInvalid,
Message: message,
Code: 400,
}
}
func NewNotFoundError(message string) *AppError {
return &AppError{
Type: ErrorTypeNotFound,
Message: message,
Code: 404,
}
}
func NewInternalError(message string) *AppError {
return &AppError{
Type: ErrorTypeInternal,
Message: message,
Code: 500,
}
}
func NewConflictError(message string) *AppError {
return &AppError{
Type: ErrorTypeConflict,
Message: message,
Code: 409,
}
}
// Вспомогательные функции для проверки типов ошибок
func IsNotFound(err error) bool {
var appErr *AppError
if errors.As(err, &appErr) {
return appErr.Type == ErrorTypeNotFound
}
return false
}
func IsInvalid(err error) bool {
var appErr *AppError
if errors.As(err, &appErr) {
return appErr.Type == ErrorTypeInvalid
}
return false
}
func IsConflict(err error) bool {
var appErr *AppError
if errors.As(err, &appErr) {
return appErr.Type == ErrorTypeConflict
}
return false
}
// Wrap оборачивает ошибку с сообщением
func Wrap(err error, message string) error {
return fmt.Errorf("%s: %w", message, err)
}
func Wrapf(err error, format string, args ...interface{}) error {
return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

BIN
cli/awesome_cli Executable file

Binary file not shown.

29
cli/cmd/cli.go Normal file
View File

@ -0,0 +1,29 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var (
RootCmd = &cobra.Command{
Use: "acli",
Short: "Интерфейс командной строки",
CompletionOptions: cobra.CompletionOptions{
HiddenDefaultCmd: true,
},
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
cmd.Help()
os.Exit(0)
}
},
}
)
func Execute() {
if err := RootCmd.Execute(); err != nil {
panic(err)
}
}

26
cli/cmd/list.go Normal file
View File

@ -0,0 +1,26 @@
package cmd
import (
"awesome_cli/internal/service"
"awesome_cli/internal/views"
"github.com/spf13/cobra"
)
func init() {
RootCmd.AddCommand(list())
}
func list() *cobra.Command {
c := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "Вывод списка",
Run: func(cmd *cobra.Command, args []string) {
vms := service.VMService().List()
views.List(vms)
},
}
return c
}

42
cli/go.mod Normal file
View File

@ -0,0 +1,42 @@
module awesome_cli
go 1.25
require (
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
golang.org/x/term v0.36.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.28.0 // indirect
)

102
cli/go.sum Normal file
View File

@ -0,0 +1,102 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

42
cli/internal/entity/vm.go Normal file
View File

@ -0,0 +1,42 @@
package entity
import (
"awesome_cli/internal/ui"
)
type VirtualMachineOutput struct {
UUID string `json:"vm_uuid,omitempty"`
Status VMStatus `json:"status,omitempty"`
Name string `json:"name,omitempty"`
Autostart bool `json:"autostart,omitempty"`
CPU int `json:"cpu,omitempty"`
DatastoreName string `json:"datastore_name,omitempty"`
Memory int `json:"memory,omitempty"`
}
type VMStatus int
const (
Creating VMStatus = iota + 1
Running
Stopped
Failed
Paused
)
func (s VMStatus) String() string {
switch s {
case Creating:
return ui.SetColor(ui.Yellow, "Creating")
case Running:
return ui.SetColor(ui.Green, "Running")
case Stopped:
return ui.SetColor(ui.Red, "Stopped")
case Failed:
return ui.SetColor(ui.Red, "Failed")
case Paused:
return ui.SetColor(ui.Yellow, "Paused")
default:
return "Unknown"
}
}

View File

@ -0,0 +1,37 @@
package service
import (
"awesome_cli/internal/entity"
"github.com/gofrs/uuid"
)
type VM struct {
}
func VMService() *VM {
return &VM{}
}
func (vm *VM) List() []entity.VirtualMachineOutput {
u, _ := uuid.NewV4()
return []entity.VirtualMachineOutput{
{
UUID: u.String(),
Status: 1,
Name: "Awesome VM",
Autostart: false,
CPU: 8,
DatastoreName: "datastore",
Memory: 8,
},
{
UUID: u.String(),
Status: 2,
Name: "Awesome VM2",
Autostart: true,
CPU: 18,
DatastoreName: "datastore2",
Memory: 8,
},
}
}

View File

@ -0,0 +1,166 @@
package ui
import (
"fmt"
"strconv"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/huh"
log "github.com/sirupsen/logrus"
)
type FormBuilder struct {
widgets []huh.Field
form huh.Form
}
func NewForm() *FormBuilder {
return &FormBuilder{}
}
func (f *FormBuilder) AddWidget(w huh.Field) {
f.widgets = append(f.widgets, w)
}
func (f *FormBuilder) Group(w ...huh.Field) *huh.Group {
group := huh.NewGroup(w...)
return group
}
func (f *FormBuilder) BuildGroups(g ...*huh.Group) *huh.Form {
form := huh.NewForm(g...)
return form
}
func (f *FormBuilder) Build() *huh.Form {
form := huh.NewForm(huh.NewGroup(f.widgets...)).WithTheme(ThemeCustom())
return form
}
func (f *FormBuilder) GetString(key string) string {
fmt.Println(f.form.GetString(key))
return f.form.GetString(key)
}
func (f *FormBuilder) GetBool(key string) bool {
fmt.Println(f.form.GetString(key))
return f.form.GetBool(key)
}
func (f *FormBuilder) GetInt(key string) int64 {
v := f.form.GetString(key)
fmt.Println(v)
value, err := strconv.Atoi(v)
if err != nil {
log.WithError(err).Fatalf(fmt.Sprintf("failed to convert value %s to integer", v))
return 0
}
return int64(value)
}
func MultiSelect(title, key string, options []string) *huh.MultiSelect[string] {
s := huh.NewMultiSelect[string]().
Title(title).
Options(huh.NewOptions[string](options...)...).
Key(key)
return s
}
func InteractiveList(title, key string, opts map[string]string) *huh.Select[string] {
var pick string
var options []huh.Option[string]
s := huh.NewSelect[string]().
Title(title).
Key(key).
Value(&pick)
if opts != nil {
for k, v := range opts {
options = append(options, huh.Option[string]{Key: k, Value: v})
}
s.Options(options...)
}
return s
}
func Input(question string, key string) *huh.Input {
var answer string
input := huh.NewInput().
Title(fmt.Sprintf("%s", question)).
Prompt("> ").
Validate(ValidateString).
Key(key).
Value(&answer)
return input
}
func Confirm(question string, key string) *huh.Confirm {
var happy bool
confirm := huh.NewConfirm().
Title(question).
Affirmative("Yes").
Negative("No").
Value(&happy).
Key(key)
return confirm
}
func Text(title, desc string) *huh.Note {
t := huh.NewNote().
Title(title).
Description(desc)
return t
}
func TextField(title, key string) *huh.Text {
t := huh.NewText().
Title(title).Key(key).ShowLineNumbers(true).Editor("vim")
return t
}
func ThemeCustom() *huh.Theme {
t := huh.ThemeBase()
var (
normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"}
// indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
indigo = lipgloss.AdaptiveColor{Light: "32", Dark: "33"}
cream = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}
// fuchsia = lipgloss.Color("#F780E2")
green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
red = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"}
)
t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238"))
t.Focused.Title = t.Focused.Title.Foreground(White).Bold(true)
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(White).Bold(true).MarginBottom(1)
t.Focused.Directory = t.Focused.Directory.Foreground(indigo)
t.Focused.Description = t.Focused.Description.Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"})
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red)
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red)
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(green)
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(indigo)
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(indigo)
t.Focused.Option = t.Focused.Option.Foreground(normalFg)
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(green)
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green)
t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(green).SetString("✓ ")
t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ")
t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg)
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(cream).Background(indigo)
t.Focused.Next = t.Focused.FocusedButton
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "252", Dark: "237"})
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(Gray)
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"})
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(green)
t.Blurred = t.Focused
t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
t.Blurred.NextIndicator = lipgloss.NewStyle()
t.Blurred.PrevIndicator = lipgloss.NewStyle()
return t
}

View File

@ -0,0 +1,84 @@
package ui
import (
"fmt"
"strconv"
"github.com/charmbracelet/huh"
log "github.com/sirupsen/logrus"
)
const (
proceedTemplate = "%s (y/n): "
defaultTemplate = "%s: "
chooseTemplate = "%d : %s\n"
)
func AskToConfirm(question, affirmative, negative string) bool {
confirm := Confirm(question, "res")
confirm.Affirmative(affirmative)
confirm.Negative(negative)
form := huh.NewForm(huh.NewGroup(confirm))
form.Run()
if form.State != huh.StateCompleted {
log.Fatal("canceled")
}
return form.GetBool("res")
}
func AskToValue(question string) (string, error) {
input := Input(question, "res")
form := huh.NewForm(huh.NewGroup(input))
form.Run()
if form.State != huh.StateCompleted {
log.Fatal("canceled")
}
return form.GetString("res"), nil
}
func AskToInt(question string) (int, error) {
v, err := AskToValue(question)
if err != nil {
return 0, err
}
value, err := strconv.Atoi(v)
if err != nil {
return 0, err
}
return value, nil
}
func AskToChoose(question string, list map[int]string) string {
for i, item := range list {
fmt.Printf(chooseTemplate, i, item)
}
answer, err := AskToInt(question)
if err != nil {
log.WithError(err).Error("failed to read the answer")
}
return list[answer]
}
func AskToChooseInteractive(question string, list []string) string {
values := make(map[string]string)
if len(list) == 0 {
log.Fatal("no items found")
}
for _, item := range list {
values[item] = item
}
widget := InteractiveList(question, "res", values)
form := huh.NewForm(huh.NewGroup(widget)).WithShowHelp(false)
form.Run()
if form.State != huh.StateCompleted {
log.Fatal("canceled")
}
return form.GetString("res")
}

155
cli/internal/ui/render.go Normal file
View File

@ -0,0 +1,155 @@
package ui
import (
"fmt"
"os"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
terminal "golang.org/x/term"
)
var (
// ANSI256 256 colors, 8-bit
Blue = lipgloss.Color("21")
Magenta = lipgloss.Color("13")
Gray = lipgloss.Color("238")
White = lipgloss.Color("15")
Red = lipgloss.Color("9")
Green = lipgloss.Color("10")
Yellow = lipgloss.Color("11")
// styling
headerStyle = lipgloss.NewStyle().
Background(Blue).
Foreground(White).
Padding(0, 1).
Italic(true)
infoStyle = headerStyle.Copy().
Foreground(White).
Background(Gray)
subHeaderStyle = headerStyle.Copy().
Background(lipgloss.NoColor{})
errorStyle = lipgloss.NewStyle().Foreground(Red)
)
type TableBuilder struct {
tableHeader []string
rows [][]string
Table *table.Table
padding int
}
func NewTableBuilder() *TableBuilder {
return &TableBuilder{
Table: table.New(),
padding: 1,
}
}
func (t *TableBuilder) Reset() {
t.tableHeader = nil
t.rows = nil
t.Table = table.New()
}
func (t *TableBuilder) AddTableHeader(strs ...string) {
t.tableHeader = append(t.tableHeader, strs...)
}
func (t *TableBuilder) AddRow(strs ...string) {
t.rows = append(t.rows, strs)
}
func (t *TableBuilder) Width(w int) {
t.Table.Width(w)
}
func (t *TableBuilder) colWidth(colNumber int) int {
var maxWidth int
for _, row := range t.rows {
for col, v := range row {
if col == colNumber && maxWidth < len(v)+t.padding*2 {
maxWidth = len(v) + t.padding*2
}
}
}
return maxWidth
}
func (t *TableBuilder) CreateTable() *table.Table {
width, _, _ := terminal.GetSize(0)
t.Table.
BorderRow(false).BorderColumn(false).
BorderLeft(false).BorderRight(false).BorderTop(false).BorderBottom(false).
//Border(lipgloss.RoundedBorder()).
//BorderRow(true).
//BorderStyle(BorderStyle).
StyleFunc(func(row, col int) lipgloss.Style {
re := lipgloss.NewRenderer(os.Stdout)
if row == -1 {
// Align(lipgloss.Center)
return re.NewStyle().Bold(true).Align(lipgloss.Left).Padding(0, 2, 0, 0).Foreground(Green)
}
return re.NewStyle().Padding(0, 2, 0, 0).Align(lipgloss.Left)
}).
Headers(t.tableHeader...).
Rows(t.rows...)
switch {
case width < 100:
t.Table.Width(width)
case len(t.tableHeader) > 8:
t.Table.Width(width)
}
return t.Table
}
// Justify Выравнивание по ширине терминала
func (t *TableBuilder) Justify() {
width, _, _ := terminal.GetSize(0)
t.Table.Width(width)
}
func Header(s string) string {
return headerStyle.Render(s)
}
func SubHeader(s string) string {
return subHeaderStyle.Render(s)
}
func Info(s string) string {
return infoStyle.Render(s)
}
func StringBar(strs ...string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, strs...)
}
func ErrorBar(s string) string {
return errorStyle.Render(s)
}
func SetColor(color lipgloss.Color, str string) string {
return lipgloss.NewStyle().Foreground(color).Render(str)
}
func Spinner(stop chan bool, description string) {
spinner := []rune{'|', '\\', '-', '/'}
for {
for _, r := range spinner {
select {
case <-stop:
fmt.Printf("\r")
return
default:
fmt.Printf("\r%c %s", r, description)
time.Sleep(100 * time.Millisecond)
}
}
}
}

View File

@ -0,0 +1,14 @@
package ui
import (
"errors"
"regexp"
)
func ValidateString(s string) error {
validRegex := regexp.MustCompile(`^[a-zA-Z0-9_.]+$`)
if !validRegex.MatchString(s) {
return errors.New("invalid input")
}
return nil
}

35
cli/internal/views/vm.go Normal file
View File

@ -0,0 +1,35 @@
package views
import (
"fmt"
"strconv"
"awesome_cli/internal/entity"
"awesome_cli/internal/ui"
)
func List(vms []entity.VirtualMachineOutput) {
var runCount int
t := ui.NewTableBuilder()
t.AddTableHeader("Status", "Name", "vCPU", "Memory", "Datastore")
for _, vm := range vms {
t.AddRow(
vm.Status.String(),
vm.Name,
strconv.FormatInt(int64(vm.CPU), 10),
strconv.FormatInt(int64(vm.Memory), 10),
vm.DatastoreName,
)
if vm.Status == entity.Running {
runCount++
}
}
header := ui.Header("Virtual machines")
count := ui.Info(fmt.Sprintf("Total: %d", len(vms)))
running := ui.Info(fmt.Sprintf("Run: %d", runCount))
fmt.Printf("%s\n\n", ui.StringBar(header, count, running))
fmt.Println(t.CreateTable())
}

7
cli/main.go Normal file
View File

@ -0,0 +1,7 @@
package main
import "awesome_cli/cmd"
func main() {
cmd.Execute()
}