commit 9c813d4754c4099c4e13d4dd58e5ece7c8aedabf Author: i.smyshlyaev Date: Thu Oct 16 12:46:23 2025 +0300 first commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/awesome_cli.iml b/.idea/awesome_cli.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/awesome_cli.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6cc931a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/back/cmd/main.go b/back/cmd/main.go new file mode 100644 index 0000000..0d407bc --- /dev/null +++ b/back/cmd/main.go @@ -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) + } + } +} diff --git a/back/go.mod b/back/go.mod new file mode 100644 index 0000000..8905f35 --- /dev/null +++ b/back/go.mod @@ -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 +) diff --git a/back/go.sum b/back/go.sum new file mode 100644 index 0000000..a25fff5 --- /dev/null +++ b/back/go.sum @@ -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= diff --git a/back/internal/datastore/mock_coffee_repository.go b/back/internal/datastore/mock_coffee_repository.go new file mode 100644 index 0000000..36ece2b --- /dev/null +++ b/back/internal/datastore/mock_coffee_repository.go @@ -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") +} diff --git a/back/internal/datastore/mock_order_repository.go b/back/internal/datastore/mock_order_repository.go new file mode 100644 index 0000000..e5dbeb6 --- /dev/null +++ b/back/internal/datastore/mock_order_repository.go @@ -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 +} diff --git a/back/internal/datastore/mock_pastry_repository.go b/back/internal/datastore/mock_pastry_repository.go new file mode 100644 index 0000000..a89fcc5 --- /dev/null +++ b/back/internal/datastore/mock_pastry_repository.go @@ -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 +} diff --git a/back/internal/domain/entities/coffee.go b/back/internal/domain/entities/coffee.go new file mode 100644 index 0000000..fb1f8f0 --- /dev/null +++ b/back/internal/domain/entities/coffee.go @@ -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"` +} diff --git a/back/internal/domain/entities/order.go b/back/internal/domain/entities/order.go new file mode 100644 index 0000000..68e6888 --- /dev/null +++ b/back/internal/domain/entities/order.go @@ -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"` +} diff --git a/back/internal/domain/entities/pastry.go b/back/internal/domain/entities/pastry.go new file mode 100644 index 0000000..0a2f4b4 --- /dev/null +++ b/back/internal/domain/entities/pastry.go @@ -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"` +} diff --git a/back/internal/domain/repositories/coffee_repository.go b/back/internal/domain/repositories/coffee_repository.go new file mode 100644 index 0000000..4b32193 --- /dev/null +++ b/back/internal/domain/repositories/coffee_repository.go @@ -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) +} diff --git a/back/internal/domain/repositories/order_repository.go b/back/internal/domain/repositories/order_repository.go new file mode 100644 index 0000000..e997467 --- /dev/null +++ b/back/internal/domain/repositories/order_repository.go @@ -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 +} diff --git a/back/internal/domain/repositories/pastry_repository.go b/back/internal/domain/repositories/pastry_repository.go new file mode 100644 index 0000000..555f259 --- /dev/null +++ b/back/internal/domain/repositories/pastry_repository.go @@ -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) +} diff --git a/back/internal/handler/coffee_handler.go b/back/internal/handler/coffee_handler.go new file mode 100644 index 0000000..89818d0 --- /dev/null +++ b/back/internal/handler/coffee_handler.go @@ -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) +} diff --git a/back/internal/handler/order_handler.go b/back/internal/handler/order_handler.go new file mode 100644 index 0000000..0a3e83a --- /dev/null +++ b/back/internal/handler/order_handler.go @@ -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) +} diff --git a/back/internal/handler/pastry_handler.go b/back/internal/handler/pastry_handler.go new file mode 100644 index 0000000..1fa3879 --- /dev/null +++ b/back/internal/handler/pastry_handler.go @@ -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) +} diff --git a/back/internal/handler/response.go b/back/internal/handler/response.go new file mode 100644 index 0000000..c7772e8 --- /dev/null +++ b/back/internal/handler/response.go @@ -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... diff --git a/back/internal/service/coffee_service.go b/back/internal/service/coffee_service.go new file mode 100644 index 0000000..d721117 --- /dev/null +++ b/back/internal/service/coffee_service.go @@ -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 +} diff --git a/back/internal/service/order_service.go b/back/internal/service/order_service.go new file mode 100644 index 0000000..16b929e --- /dev/null +++ b/back/internal/service/order_service.go @@ -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 +} diff --git a/back/internal/service/pastry_service.go b/back/internal/service/pastry_service.go new file mode 100644 index 0000000..9d1605c --- /dev/null +++ b/back/internal/service/pastry_service.go @@ -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 +} diff --git a/back/pkg/errors/errors.go b/back/pkg/errors/errors.go new file mode 100644 index 0000000..1d27f64 --- /dev/null +++ b/back/pkg/errors/errors.go @@ -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) +} diff --git a/cli/awesome_cli b/cli/awesome_cli new file mode 100755 index 0000000..6967e53 Binary files /dev/null and b/cli/awesome_cli differ diff --git a/cli/cmd/cli.go b/cli/cmd/cli.go new file mode 100644 index 0000000..c2c7fc6 --- /dev/null +++ b/cli/cmd/cli.go @@ -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) + } +} diff --git a/cli/cmd/list.go b/cli/cmd/list.go new file mode 100644 index 0000000..26504cc --- /dev/null +++ b/cli/cmd/list.go @@ -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 +} diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 0000000..d2d9bae --- /dev/null +++ b/cli/go.mod @@ -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 +) diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 0000000..b76622e --- /dev/null +++ b/cli/go.sum @@ -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= diff --git a/cli/internal/entity/vm.go b/cli/internal/entity/vm.go new file mode 100644 index 0000000..a536d40 --- /dev/null +++ b/cli/internal/entity/vm.go @@ -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" + } +} diff --git a/cli/internal/service/vm.go b/cli/internal/service/vm.go new file mode 100644 index 0000000..a9f4722 --- /dev/null +++ b/cli/internal/service/vm.go @@ -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, + }, + } +} diff --git a/cli/internal/ui/interactive.go b/cli/internal/ui/interactive.go new file mode 100644 index 0000000..78d78bc --- /dev/null +++ b/cli/internal/ui/interactive.go @@ -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 +} diff --git a/cli/internal/ui/question.go b/cli/internal/ui/question.go new file mode 100644 index 0000000..704c2f5 --- /dev/null +++ b/cli/internal/ui/question.go @@ -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") +} diff --git a/cli/internal/ui/render.go b/cli/internal/ui/render.go new file mode 100644 index 0000000..6b2d4f2 --- /dev/null +++ b/cli/internal/ui/render.go @@ -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) + } + } + } +} diff --git a/cli/internal/ui/validate.go b/cli/internal/ui/validate.go new file mode 100644 index 0000000..6861e2d --- /dev/null +++ b/cli/internal/ui/validate.go @@ -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 +} diff --git a/cli/internal/views/vm.go b/cli/internal/views/vm.go new file mode 100644 index 0000000..1a23c58 --- /dev/null +++ b/cli/internal/views/vm.go @@ -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()) +} diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..12bfc63 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,7 @@ +package main + +import "awesome_cli/cmd" + +func main() { + cmd.Execute() +}