From d4e7211d1d7ccdf2d0de287a593c1cc12ae788b8 Mon Sep 17 00:00:00 2001 From: Stanislav Chernyshev Date: Sun, 22 Jun 2025 21:04:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Допричесать игру --- part_8/8.1/udp/client/client.go | 46 ++ part_8/8.1/udp/go.mod | 3 + part_8/8.1/udp/main.go | 38 ++ part_8/8.1/udp/server/server.go | 44 ++ part_8/8.2/tcp/client/client.go | 53 +++ part_8/8.2/tcp/go.mod | 3 + part_8/8.2/tcp/main.go | 38 ++ part_8/8.2/tcp/server/server.go | 62 +++ part_8/8.3/tcp-chat/client/client.go | 77 ++++ part_8/8.3/tcp-chat/go.mod | 3 + part_8/8.3/tcp-chat/main.go | 38 ++ part_8/8.3/tcp-chat/server/server.go | 85 ++++ .../golang/todo-service/.vscode/settings.json | 5 + part_8/8.4/golang/todo-service/db/db.go | 98 ++++ .../golang/todo-service/db/db_definition.go | 12 + part_8/8.4/golang/todo-service/db/models.go | 17 + .../golang/todo-service/db/projects_crud.go | 49 ++ .../8.4/golang/todo-service/db/tasks_crud.go | 73 +++ part_8/8.4/golang/todo-service/go.mod | 16 + part_8/8.4/golang/todo-service/go.sum | 16 + part_8/8.4/golang/todo-service/main.go | 20 + .../golang/todo-service/service/api_models.go | 22 + .../todo-service/service/api_project.go | 87 ++++ .../golang/todo-service/service/api_task.go | 153 +++++++ .../8.4/golang/todo-service/service/logger.go | 23 + .../golang/todo-service/service/routers.go | 102 +++++ .../golang/todo-service/todo-service-api.yml | 429 ++++++++++++++++++ part_8/8.4/golang/todo-service/todo.db | Bin 0 -> 16384 bytes part_8/tic_tac_toe_v7/board/board.go | 12 - .../tic_tac_toe_v7/board/board_cell_type.go | 13 - part_8/tic_tac_toe_v7/database/crud.go | 8 +- part_8/tic_tac_toe_v7/database/models.go | 48 +- part_8/tic_tac_toe_v7/database/utils.go | 31 -- .../model/finish_game_shapshot.go | 18 +- .../network/client_to_server.go | 20 +- part_8/tic_tac_toe_v7/network/protocol.go | 5 + .../network/server_to_client.go | 29 +- .../tic_tac_toe_v7/player/computer_player.go | 23 +- part_8/tic_tac_toe_v7/player/i_player.go | 7 +- part_8/tic_tac_toe_v7/player/player.go | 36 +- part_8/tic_tac_toe_v7/server/server.go | 5 +- 41 files changed, 1724 insertions(+), 143 deletions(-) create mode 100644 part_8/8.1/udp/client/client.go create mode 100644 part_8/8.1/udp/go.mod create mode 100644 part_8/8.1/udp/main.go create mode 100644 part_8/8.1/udp/server/server.go create mode 100644 part_8/8.2/tcp/client/client.go create mode 100644 part_8/8.2/tcp/go.mod create mode 100644 part_8/8.2/tcp/main.go create mode 100644 part_8/8.2/tcp/server/server.go create mode 100644 part_8/8.3/tcp-chat/client/client.go create mode 100644 part_8/8.3/tcp-chat/go.mod create mode 100644 part_8/8.3/tcp-chat/main.go create mode 100644 part_8/8.3/tcp-chat/server/server.go create mode 100644 part_8/8.4/golang/todo-service/.vscode/settings.json create mode 100644 part_8/8.4/golang/todo-service/db/db.go create mode 100644 part_8/8.4/golang/todo-service/db/db_definition.go create mode 100644 part_8/8.4/golang/todo-service/db/models.go create mode 100644 part_8/8.4/golang/todo-service/db/projects_crud.go create mode 100644 part_8/8.4/golang/todo-service/db/tasks_crud.go create mode 100644 part_8/8.4/golang/todo-service/go.mod create mode 100644 part_8/8.4/golang/todo-service/go.sum create mode 100644 part_8/8.4/golang/todo-service/main.go create mode 100644 part_8/8.4/golang/todo-service/service/api_models.go create mode 100644 part_8/8.4/golang/todo-service/service/api_project.go create mode 100644 part_8/8.4/golang/todo-service/service/api_task.go create mode 100644 part_8/8.4/golang/todo-service/service/logger.go create mode 100644 part_8/8.4/golang/todo-service/service/routers.go create mode 100644 part_8/8.4/golang/todo-service/todo-service-api.yml create mode 100644 part_8/8.4/golang/todo-service/todo.db delete mode 100644 part_8/tic_tac_toe_v7/database/utils.go diff --git a/part_8/8.1/udp/client/client.go b/part_8/8.1/udp/client/client.go new file mode 100644 index 0000000..16a4838 --- /dev/null +++ b/part_8/8.1/udp/client/client.go @@ -0,0 +1,46 @@ +package client + +import ( + "fmt" + "log" + "net" +) + +func checkErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func RunClient(port int) { + s, _ := net.ResolveUDPAddr("udp4", fmt.Sprintf("localhost:%d", port)) + connection, err := net.DialUDP("udp4", nil, s) + checkErr(err) + + fmt.Printf("The UDP server is %s\n", connection.RemoteAddr().String()) + defer connection.Close() + count := 0 + for { + data := []byte(fmt.Sprintf("%d)Hello!\n", count)) + fmt.Print("->", string(data)) + _, err = connection.Write(data) + + checkErr(err) + + buffer := make([]byte, 1024) + n, _, err := connection.ReadFromUDP(buffer) + checkErr(err) + + fmt.Printf("<-: %s", string(buffer[0:n])) + count++ + if count >= 5 { + data := []byte("STOP") + fmt.Println("->", string(data)) + _, err = connection.Write(data) + checkErr(err) + + fmt.Println("Finished!") + return + } + } +} diff --git a/part_8/8.1/udp/go.mod b/part_8/8.1/udp/go.mod new file mode 100644 index 0000000..c224feb --- /dev/null +++ b/part_8/8.1/udp/go.mod @@ -0,0 +1,3 @@ +module udp + +go 1.24 diff --git a/part_8/8.1/udp/main.go b/part_8/8.1/udp/main.go new file mode 100644 index 0000000..76f68b4 --- /dev/null +++ b/part_8/8.1/udp/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "log" + "os" + "strconv" + "udp/client" + "udp/server" +) + +const PORT = 8081 + +func checkErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func main() { + if len(os.Args) != 2 { + fmt.Println("You didn't select a launch option!!!") + return + } + numGR, err := strconv.Atoi(os.Args[1]) + checkErr(err) + + switch numGR { + case 1: + fmt.Println("Server is running") + server.RunServer(PORT) + case 2: + fmt.Println("Client is running") + client.RunClient(PORT) + default: + log.Fatal("What pokemon is this?") + } +} diff --git a/part_8/8.1/udp/server/server.go b/part_8/8.1/udp/server/server.go new file mode 100644 index 0000000..09eef1e --- /dev/null +++ b/part_8/8.1/udp/server/server.go @@ -0,0 +1,44 @@ +package server + +import ( + "fmt" + "log" + "net" + "strings" + "time" +) + +func checkErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func RunServer(port int) { + s, err := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", port)) + checkErr(err) + + connection, err := net.ListenUDP("udp4", s) + checkErr(err) + + defer connection.Close() + buffer := make([]byte, 1024) + fmt.Println("Oo") + for { + n, addr, err := connection.ReadFromUDP(buffer) // ждем подсоединение клиента + checkErr(err) + fmt.Printf("<- %s", string(buffer[0:n])) + + if strings.TrimSpace(string(buffer[0:n])) == "STOP" { + fmt.Println() + fmt.Println("Exiting UDP server!") + return + } + + time.Sleep(5 * time.Second) + + fmt.Printf("->: %s", string(buffer)) + _, err = connection.WriteToUDP(buffer, addr) + checkErr(err) + } +} diff --git a/part_8/8.2/tcp/client/client.go b/part_8/8.2/tcp/client/client.go new file mode 100644 index 0000000..9261a66 --- /dev/null +++ b/part_8/8.2/tcp/client/client.go @@ -0,0 +1,53 @@ +package client + +import ( + "bufio" + "fmt" + "log" + "net" +) + +func checkErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func RunClient(port int) { + // запускает реализацию клиента TCP и соединяет вас с нужным TCP-сервером + connection, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + checkErr(err) + + fmt.Printf("The TCP server is %s\n", connection.RemoteAddr().String()) + defer connection.Close() + count := 0 + for { + // отправка сообщения на сервер + data := []byte(fmt.Sprintf("%d)Hello!\n", count)) + fmt.Print("->", string(data)) + fmt.Fprint(connection, fmt.Sprintf("%d)Hello!\n", count)) + // или + //_, err = connection.Write(data) + // checkErr(err) + + // считываем ответ TCP-сервера + message, err := bufio.NewReader(connection).ReadString('\n') + // или + // buffer := make([]byte, 1024) + // _, err := connection.Read(buffer) + // message := string(buffer) + checkErr(err) + + fmt.Printf("<-: %s", message) + count++ + if count >= 5 { + data := []byte("STOP") + fmt.Println("->", string(data)) + _, err = connection.Write(data) + checkErr(err) + + fmt.Println("Finished!") + return + } + } +} diff --git a/part_8/8.2/tcp/go.mod b/part_8/8.2/tcp/go.mod new file mode 100644 index 0000000..540e7dc --- /dev/null +++ b/part_8/8.2/tcp/go.mod @@ -0,0 +1,3 @@ +module udp/server + +go 1.24 diff --git a/part_8/8.2/tcp/main.go b/part_8/8.2/tcp/main.go new file mode 100644 index 0000000..8f40b03 --- /dev/null +++ b/part_8/8.2/tcp/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "log" + "os" + "strconv" + "udp/server/client" + "udp/server/server" +) + +const PORT = 8081 + +func checkErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func main() { + if len(os.Args) != 2 { + fmt.Println("You didn't select a launch option!!!") + return + } + numGR, err := strconv.Atoi(os.Args[1]) + checkErr(err) + + switch numGR { + case 1: + fmt.Println("Server is running") + server.RunServer(PORT) + case 2: + fmt.Println("Client is running") + client.RunClient(PORT) + default: + log.Fatal("What pokemon is this?") + } +} diff --git a/part_8/8.2/tcp/server/server.go b/part_8/8.2/tcp/server/server.go new file mode 100644 index 0000000..ecf2c84 --- /dev/null +++ b/part_8/8.2/tcp/server/server.go @@ -0,0 +1,62 @@ +package server + +import ( + "fmt" + "log" + "net" + "strings" + "time" +) + +func checkErr(err error) { + if err != nil { + log.Print(err) + } +} + +func handleConnection(connection net.Conn) { + defer connection.Close() + + buffer := make([]byte, 1024) + for { + n, err := connection.Read(buffer) // считывание данных + checkErr(err) + fmt.Printf("Receive: %s from %s\n", + string(buffer[0:n-1]), connection.RemoteAddr().String()) + + if strings.TrimSpace(string(buffer[0:n])) == "STOP" { + // завершение работы сервера + fmt.Println() + fmt.Printf("Close connection with client: %s\n", + connection.RemoteAddr().String()) + break + } + + time.Sleep(2 * time.Second) + + fmt.Printf("Send : %s to %s\n", + string(buffer[0:n-1]), connection.RemoteAddr().String()) + _, err = connection.Write(buffer) // отправка сообщения клиенту + checkErr(err) + } +} + +func RunServer(port int) { + // net.Listen возвращает Listener переменную, + // которая является общим сетевым прослушивателем для потоковых протоколов + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + checkErr(err) + + defer listener.Close() + + for { + connection, err := listener.Accept() // ожидание подключения клиента к серверу + // Только после успешного вызова Accept()TCP-сервер может начать + // взаимодействовать с TCP-клиентами + if err != nil { + log.Print(err) + return + } + go handleConnection(connection) + } +} diff --git a/part_8/8.3/tcp-chat/client/client.go b/part_8/8.3/tcp-chat/client/client.go new file mode 100644 index 0000000..43089cb --- /dev/null +++ b/part_8/8.3/tcp-chat/client/client.go @@ -0,0 +1,77 @@ +package client + +import ( + "bufio" + "fmt" + "io" + "log" + "net" + "os" + "strings" + "sync" +) + +var waitingGr sync.WaitGroup + +func checkErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func sendMessage(connection net.Conn, waitingGr *sync.WaitGroup) { + defer waitingGr.Done() + for { + reader := bufio.NewReader(os.Stdin) // считываем введенное сообщение + text, _ := reader.ReadString('\n') + + if strings.TrimSuffix(strings.TrimSpace(text), "\r\n") == "/stop" { + // завершение работы клиента + break + } else { + _, err := connection.Write([]byte(text)) + checkErr(err) + } + } +} + +func receiveMessage(connection net.Conn, waitingGr *sync.WaitGroup) { + defer waitingGr.Done() + for { + // ждем сообщение от сервера и считываем его + message, err := bufio.NewReader(connection).ReadString('\n') + + if err == io.EOF { + fmt.Println("Connection close!") + break + } else if err != nil { + fmt.Println(err.Error()) + break + } + + message = message[:len(message)-1] // обрезаем символ перевода на следующую строку + fmt.Println(string(message)) + } +} + +func RunClient(port int) { + // запускает реализацию клиента TCP и соединяет вас с нужным TCP-сервером + connection, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + checkErr(err) + + fmt.Printf("The TCP server is %s\n", connection.RemoteAddr().String()) + defer connection.Close() + + waitingGr.Add(1) + + fmt.Println("Enter name: ") + temp := bufio.NewReader(os.Stdin) + userName, _ := temp.ReadString('\n') + _, err = connection.Write([]byte(userName)) + checkErr(err) + + go sendMessage(connection, &waitingGr) + go receiveMessage(connection, &waitingGr) + + waitingGr.Wait() +} diff --git a/part_8/8.3/tcp-chat/go.mod b/part_8/8.3/tcp-chat/go.mod new file mode 100644 index 0000000..aecef7d --- /dev/null +++ b/part_8/8.3/tcp-chat/go.mod @@ -0,0 +1,3 @@ +module tcp/server + +go 1.24 diff --git a/part_8/8.3/tcp-chat/main.go b/part_8/8.3/tcp-chat/main.go new file mode 100644 index 0000000..cbfe962 --- /dev/null +++ b/part_8/8.3/tcp-chat/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "log" + "os" + "strconv" + "tcp/server/client" + "tcp/server/server" +) + +const PORT = 8081 + +func checkErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func main() { + if len(os.Args) != 2 { + fmt.Println("You didn't select a launch option!!!") + return + } + numGR, err := strconv.Atoi(os.Args[1]) + checkErr(err) + + switch numGR { + case 1: + fmt.Println("Server is running") + server.RunServer(PORT) + case 2: + fmt.Println("Client is running") + client.RunClient(PORT) + default: + log.Fatal("What pokemon is this?") + } +} diff --git a/part_8/8.3/tcp-chat/server/server.go b/part_8/8.3/tcp-chat/server/server.go new file mode 100644 index 0000000..9116898 --- /dev/null +++ b/part_8/8.3/tcp-chat/server/server.go @@ -0,0 +1,85 @@ +package server + +import ( + "bufio" + "fmt" + "log" + "net" +) + +var connections []net.Conn + +func checkErr(err error) { + if err != nil { + log.Print(err) + } +} + +func handleConnection(connection net.Conn) { + connections = append(connections, connection) + userName, _ := bufio.NewReader(connection).ReadString('\n') + userName = userName[:len(userName)-2] + _, err := connection.Write([]byte("Hi " + userName + "\n")) + checkErr(err) + + for { + text, err := bufio.NewReader(connection).ReadString('\n') + if err != nil { + connection.Close() + removeConnection(connection) + broadCastMessage(userName+" is offline\n", connection) + break + } + + broadCastMessage(userName+":"+text, connection) + } +} + +func removeConnection(connection net.Conn) { + var i int + + for i = range connections { + if connections[i] == connection { + break + } + } + + if len(connections) > 1 { + connections = append(connections[:i], connections[i+1:]...) + } else { + connections = nil + } +} + +func broadCastMessage(msg string, connection net.Conn) { + // отправка сообщения всем клиентам + for _, c := range connections { + if connection != c { + _, err := c.Write([]byte(msg)) + checkErr(err) + } + } + + msg = msg[:len(msg)-1] + fmt.Println(msg) +} + +func RunServer(port int) { + // net.Listen возвращает Listener переменную, + // которая является общим сетевым прослушивателем для потоковых протоколов + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + checkErr(err) + + defer listener.Close() + + for { + connection, err := listener.Accept() // ожидание подключения клиента к серверу + // Только после успешного вызова Accept()TCP-сервер может начать + // взаимодействовать с TCP-клиентами + if err != nil { + log.Print(err) + return + } + go handleConnection(connection) + } +} diff --git a/part_8/8.4/golang/todo-service/.vscode/settings.json b/part_8/8.4/golang/todo-service/.vscode/settings.json new file mode 100644 index 0000000..f465c5c --- /dev/null +++ b/part_8/8.4/golang/todo-service/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "yaml.schemas": { + "swaggerviewer:openapi": "file:///e%3A/code/golang/todo-service/todo-service-api.yml" + } +} \ No newline at end of file diff --git a/part_8/8.4/golang/todo-service/db/db.go b/part_8/8.4/golang/todo-service/db/db.go new file mode 100644 index 0000000..e098e42 --- /dev/null +++ b/part_8/8.4/golang/todo-service/db/db.go @@ -0,0 +1,98 @@ +package db + +import ( + _ "database/sql" + "fmt" + "log" + "os" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type SQLiteRepository struct { + db *gorm.DB +} + +func NewSQLiteRepository() *SQLiteRepository { + var db *gorm.DB + + if _, err := os.Stat(dbName); os.IsNotExist(err) { + db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{}) + if err != nil { + log.Fatal(err) + } + fmt.Println("DB isn't exist") + db.AutoMigrate(&Project{}, &Task{}) + putDefaultValuesToDB(db) + } else { + db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{}) + if err != nil { + log.Fatal(err) + } + fmt.Println("DB already exists") + } + + return &SQLiteRepository{ + db: db, + } +} + +func putDefaultValuesToDB(db *gorm.DB) { + firstProject := Project{ + Name: "Go", + Description: "Roadmap for learning Go", + } + secondProject := Project{ + Name: "One Year", + Description: "Tasks for the year", + } + db.Create(&firstProject) + db.Create(&secondProject) + db.Create(&Task{ + Name: "Variable", + Description: "Learning Go build-in variables", + Priority: 1, + Project: &firstProject, + }) + db.Create(&Task{ + Name: "Struct", + Description: "Learning use struct in OOP code", + Priority: 3, + Project: &firstProject, + }) + db.Create(&Task{ + Name: "Goroutine", + Description: "Learning concurrent programming", + Priority: 5, + Project: &firstProject, + }) + db.Create(&Task{ + Name: "DataBase", + Description: "How write app with db", + Priority: 1, + Project: &firstProject, + }) + db.Create(&Task{ + Name: "PhD", + Description: "Ph.D. in Technical Sciences", + Priority: 5, + Project: &secondProject, + }) + db.Create(&Task{ + Name: "Losing weight", + Description: "Exercise and eat less chocolate", + Priority: 2, + Project: &secondProject, + }) + db.Create(&Task{ + Name: "Пафос и превозмогание", + Description: "10к подписчиков на канале", + Priority: 2, + Project: &secondProject, + }) +} + +func (r *SQLiteRepository) Close() { + +} diff --git a/part_8/8.4/golang/todo-service/db/db_definition.go b/part_8/8.4/golang/todo-service/db/db_definition.go new file mode 100644 index 0000000..d6a42a3 --- /dev/null +++ b/part_8/8.4/golang/todo-service/db/db_definition.go @@ -0,0 +1,12 @@ +package db + +import "errors" + +const dbName = "todo.db" + +var ( + ErrDuplicate = errors.New("record already exists") + ErrNotExists = errors.New("row not exists") + ErrUpdateFailed = errors.New("update failed") + ErrDeleteFailed = errors.New("delete failed") +) diff --git a/part_8/8.4/golang/todo-service/db/models.go b/part_8/8.4/golang/todo-service/db/models.go new file mode 100644 index 0000000..e2f306f --- /dev/null +++ b/part_8/8.4/golang/todo-service/db/models.go @@ -0,0 +1,17 @@ +package db + +type Project struct { + ID int `json:"id" gorm:"primary_key;autoIncrement:true;not null"` + Name string `json:"name" gorm:"unique;not null"` + Description string `json:"description"` +} + +type Task struct { + ID int `json:"id" gorm:"primary_key;autoIncrement;not null"` + Name string `json:"name" gorm:"not null"` + Description string `json:"description" gorm:"not null"` + Priority uint8 `json:"priority" gorm:"not null"` + IsDone bool `json:"isDone" gorm:"not null"` + ProjectID int `json:"projectID" gorm:"not null"` + Project *Project `gorm:"foreignKey:ProjectID;references:ID"` +} diff --git a/part_8/8.4/golang/todo-service/db/projects_crud.go b/part_8/8.4/golang/todo-service/db/projects_crud.go new file mode 100644 index 0000000..5546660 --- /dev/null +++ b/part_8/8.4/golang/todo-service/db/projects_crud.go @@ -0,0 +1,49 @@ +package db + +import ( + "errors" + + "github.com/mattn/go-sqlite3" +) + +func (r *SQLiteRepository) AddProject(project Project) (*Project, error) { + tx := r.db.Create(&project) + if tx.Error != nil { + var sqliteErr sqlite3.Error + if errors.As(tx.Error, &sqliteErr) { + if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique) { + return nil, ErrDuplicate + } + } + return nil, tx.Error + } + + return &project, nil +} + +func (r *SQLiteRepository) DeleteProject(projectID int) error { + tx := r.db.Delete(&Project{ID: projectID}) + if tx.Error != nil { + return tx.Error + } + + rowsAffected := tx.RowsAffected + if rowsAffected == 0 { + return ErrDeleteFailed + } + + return nil +} + +func (r *SQLiteRepository) GetAllProjects() ([]Project, error) { + var projects []Project + tx := r.db.Find(&projects) + if tx.Error != nil { + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, ErrNotExists + } + + return projects, nil +} diff --git a/part_8/8.4/golang/todo-service/db/tasks_crud.go b/part_8/8.4/golang/todo-service/db/tasks_crud.go new file mode 100644 index 0000000..9f8dc1d --- /dev/null +++ b/part_8/8.4/golang/todo-service/db/tasks_crud.go @@ -0,0 +1,73 @@ +package db + +import "errors" + +func (r *SQLiteRepository) AddTask(task Task) (*Task, error) { + tx := r.db.Create(&task) + if tx.Error != nil { + return nil, tx.Error + } + + return &task, nil +} + +func (r *SQLiteRepository) DeleteTask(taskID int) error { + tx := r.db.Delete(&Task{ID: taskID}) + if tx.Error != nil { + return tx.Error + } + + rowsAffected := tx.RowsAffected + if rowsAffected == 0 { + return ErrDeleteFailed + } + + return nil +} + +func (r *SQLiteRepository) GetAllTasks() (tasks []Task, err error) { + tx := r.db.Find(&tasks) + if tx.Error != nil { + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, ErrNotExists + } + + return +} + +func (r *SQLiteRepository) GetProjectTasks(projectID int) (tasks []Task, err error) { + if projectID == 0 { + return nil, errors.New("invalid updated ID") + } + + tx := r.db.Where("project_id", projectID).Find(&tasks) + if tx.Error != nil { + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, ErrNotExists + } + + return +} + +func (r *SQLiteRepository) TaskDone(taskId int) error { + if taskId == 0 { + return errors.New("invalid updated ID") + } + pjTask := &Task{ID: taskId} + tx := r.db.Find(&pjTask) + if tx.Error != nil { + return tx.Error + } + pjTask.IsDone = true + r.db.Save(&pjTask) + rowsAffected := tx.RowsAffected + if rowsAffected == 0 { + return ErrUpdateFailed + } + + return nil +} diff --git a/part_8/8.4/golang/todo-service/go.mod b/part_8/8.4/golang/todo-service/go.mod new file mode 100644 index 0000000..4445f4d --- /dev/null +++ b/part_8/8.4/golang/todo-service/go.mod @@ -0,0 +1,16 @@ +module golang/todo-service + +go 1.24 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/mattn/go-sqlite3 v1.14.24 + gorm.io/driver/sqlite v1.5.7 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + golang.org/x/text v0.14.0 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/part_8/8.4/golang/todo-service/go.sum b/part_8/8.4/golang/todo-service/go.sum new file mode 100644 index 0000000..3e6353e --- /dev/null +++ b/part_8/8.4/golang/todo-service/go.sum @@ -0,0 +1,16 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/part_8/8.4/golang/todo-service/main.go b/part_8/8.4/golang/todo-service/main.go new file mode 100644 index 0000000..43717b7 --- /dev/null +++ b/part_8/8.4/golang/todo-service/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "golang/todo-service/db" + "golang/todo-service/service" + "log" + "net/http" +) + +const PORT = "8080" + +func main() { + + rep := db.NewSQLiteRepository() + defer rep.Close() + + router := service.NewRouter(rep) + log.Printf("Server started") + log.Fatal(http.ListenAndServe(":"+PORT, router)) +} diff --git a/part_8/8.4/golang/todo-service/service/api_models.go b/part_8/8.4/golang/todo-service/service/api_models.go new file mode 100644 index 0000000..241c312 --- /dev/null +++ b/part_8/8.4/golang/todo-service/service/api_models.go @@ -0,0 +1,22 @@ +package service + +import "golang/todo-service/db" + +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type GoodResponse struct { + Code int `json:"code"` + Message string `json:"message"` + ID int `json:"id"` +} + +type ProjectsList struct { + Items []db.Project `json:"items,omitempty"` +} + +type TasksList struct { + Items []db.Task `json:"items,omitempty"` +} diff --git a/part_8/8.4/golang/todo-service/service/api_project.go b/part_8/8.4/golang/todo-service/service/api_project.go new file mode 100644 index 0000000..404b101 --- /dev/null +++ b/part_8/8.4/golang/todo-service/service/api_project.go @@ -0,0 +1,87 @@ +package service + +import ( + "encoding/json" + "golang/todo-service/db" + "io" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +func deleteProject(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + params := mux.Vars(r) + + idStr, ok := params["id"] + if !ok { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + id, err := strconv.Atoi(idStr) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + err = repository.DeleteProject(id) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusNotFound, + "Project not found"}) + return + } + w.WriteHeader(http.StatusOK) +} + +func addProject(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + var project db.Project + reqBody, err := io.ReadAll(r.Body) + if err == nil { + json.Unmarshal(reqBody, &project) + if project.Name == "" { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + project.ID = 0 + project, err := repository.AddProject(project) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Project with that name already exists"}) + return + } + + goodResponse := GoodResponse{ + Code: 201, + Message: "Проект создан", + ID: project.ID, + } + jsonGoodResponse, _ := json.Marshal(goodResponse) + w.WriteHeader(http.StatusCreated) + w.Write(jsonGoodResponse) + return + } + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) +} + +func getProjects(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + progects, err := repository.GetAllProjects() + if err != nil || len(progects) == 0 { + errorResponse(w, ErrorResponse{http.StatusNotFound, + "Projects not found"}) + return + } + + projectsList := ProjectsList{progects} + jsonGoodResponse, _ := json.Marshal(projectsList) + w.WriteHeader(http.StatusOK) + w.Write(jsonGoodResponse) +} diff --git a/part_8/8.4/golang/todo-service/service/api_task.go b/part_8/8.4/golang/todo-service/service/api_task.go new file mode 100644 index 0000000..6e1b594 --- /dev/null +++ b/part_8/8.4/golang/todo-service/service/api_task.go @@ -0,0 +1,153 @@ +package service + +import ( + "encoding/json" + "golang/todo-service/db" + "io" + "net/http" + "strconv" +) + +func deleteTask(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + queryStr := r.URL.Query() + + idStr, ok := queryStr["id"] + if !ok { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + if len(idStr) == 0 { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + id, err := strconv.Atoi(idStr[0]) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + err = repository.DeleteTask(id) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusNotFound, + "Task not found"}) + return + } + w.WriteHeader(http.StatusOK) +} + +func getTask(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + queryStr := r.URL.Query() + + idStr, ok := queryStr["projectID"] + if !ok { + tasks, err := repository.GetAllTasks() + if err != nil || len(tasks) == 0 { + errorResponse(w, ErrorResponse{http.StatusNotFound, + "Tasks not found"}) + return + } + // возвращаем все имеющиеся задачи + projectsList := TasksList{tasks} + jsonGoodResponse, _ := json.Marshal(projectsList) + w.WriteHeader(http.StatusOK) + w.Write(jsonGoodResponse) + return + } + + if len(idStr) == 0 { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + id, err := strconv.Atoi(idStr[0]) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + tasks, err := repository.GetProjectTasks(id) + if err != nil || len(tasks) == 0 { + errorResponse(w, ErrorResponse{http.StatusNotFound, + "Tasks not found"}) + return + } + // возвращаем все задачи конкретного проекта + projectsList := TasksList{tasks} + jsonGoodResponse, _ := json.Marshal(projectsList) + w.WriteHeader(http.StatusOK) + w.Write(jsonGoodResponse) +} + +func addTask(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + var task db.Task + reqBody, err := io.ReadAll(r.Body) + if err == nil { + json.Unmarshal(reqBody, &task) + if task.Name == "" || task.ProjectID == 0 { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + task.ID = 0 + task, err := repository.AddTask(task) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + goodResponse := GoodResponse{ + Code: 201, + Message: "Задача создана", + ID: task.ID, + } + jsonGoodResponse, _ := json.Marshal(goodResponse) + w.WriteHeader(http.StatusCreated) + w.Write(jsonGoodResponse) + return + } + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) +} + +func doneTask(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + queryStr := r.URL.Query() + + idStr, ok := queryStr["id"] + if !ok { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + if len(idStr) == 0 { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + id, err := strconv.Atoi(idStr[0]) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusBadRequest, + "Validation Failed"}) + return + } + + err = repository.TaskDone(id) + if err != nil { + errorResponse(w, ErrorResponse{http.StatusNotFound, + "Tasks not found"}) + return + } + w.WriteHeader(http.StatusOK) +} diff --git a/part_8/8.4/golang/todo-service/service/logger.go b/part_8/8.4/golang/todo-service/service/logger.go new file mode 100644 index 0000000..a1f80c9 --- /dev/null +++ b/part_8/8.4/golang/todo-service/service/logger.go @@ -0,0 +1,23 @@ +package service + +import ( + "log" + "net/http" + "time" +) + +func Logger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s %s %s %s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} diff --git a/part_8/8.4/golang/todo-service/service/routers.go b/part_8/8.4/golang/todo-service/service/routers.go new file mode 100644 index 0000000..c9513a5 --- /dev/null +++ b/part_8/8.4/golang/todo-service/service/routers.go @@ -0,0 +1,102 @@ +package service + +import ( + "encoding/json" + "fmt" + "golang/todo-service/db" + "net/http" + + "github.com/gorilla/mux" +) + +type Route struct { + Name string // имя функции обработчика + Method string // тип HTTP-сообщения + Pattern string // шаблон пути + HandlerFunc http.HandlerFunc // ссылка на функцию обработчик + // сигнатура функции должна быть func(ResponseWriter, *Request) +} + +var repository *db.SQLiteRepository + +func NewRouter(rep *db.SQLiteRepository) *mux.Router { + repository = rep + router := mux.NewRouter().StrictSlash(true) + for _, route := range routes { + var handler http.Handler + handler = route.HandlerFunc + //оборачиваем функцию-обработчик в логгер + handler = Logger(route.HandlerFunc, route.Name) + // handler := route.HandlerFunc + // // // добавляем новый обработчик + router. //HandleFunc(route.Pattern, handler).Methods(route.Method) + Methods(route.Method). // тип HTTP-сообщения + Path(route.Pattern). // шаблон пути + Name(route.Name). // имя функции обработчика + Handler(handler) // ссылка на функцию обработчик + } + router.Use(mux.CORSMethodMiddleware(router)) + + return router +} + +func home(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello Wold in REST API style!!!") +} + +var routes = []Route{ + { // домашняя страница + "home", + http.MethodGet, + "/api/v1/todo", + home, + }, + { // удаление проекта по id + "deleteProject", + http.MethodDelete, + "/api/v1/todo/project/del/{id}", + deleteProject, + }, + { // добавление проекта + "addProject", + http.MethodPost, + "/api/v1/todo/project", + addProject, + }, + { // получить все проекты + "getProjects", + http.MethodGet, + "/api/v1/todo/projects", + getProjects, + }, + { // удаление задачи + "deleteTask", + http.MethodDelete, + "/api/v1/todo/task", + deleteTask, + }, + { // получение всех задач или конкретного проекта + "getTask", + http.MethodGet, + "/api/v1/todo/task", + getTask, + }, + { // добавить задачу + "addTask", + http.MethodPost, + "/api/v1/todo/task", + addTask, + }, + { // изменение статуса задачи на «Выполнено» + "doneTask", + http.MethodPut, + "/api/v1/todo/task", + doneTask, + }, +} + +func errorResponse(w http.ResponseWriter, err ErrorResponse) { + jsonResponse, _ := json.Marshal(err) + w.WriteHeader(err.Code) + w.Write(jsonResponse) +} diff --git a/part_8/8.4/golang/todo-service/todo-service-api.yml b/part_8/8.4/golang/todo-service/todo-service-api.yml new file mode 100644 index 0000000..b256da3 --- /dev/null +++ b/part_8/8.4/golang/todo-service/todo-service-api.yml @@ -0,0 +1,429 @@ +openapi: 3.0.2 +info: + title: ToDo client-server api + description: | + Документация по описанию конечных точек сервера, посредством которых + происходит доступ к ресурсам + version: 1.0.0 +servers: +- url: http://127.0.0.1:8080/api/v1/todo + +tags: +- name: task +- name: project + +paths: + /project: + post: + tags: + - project + description: | + Добавление проекта + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + responses: + "201": + description: Добавление прошло успешно. + content: + application/json: + schema: + $ref: '#/components/schemas/GoodResponse' + examples: + response: + value: |- + { + "code": 201, + "message": "Project created", + "id": 1 + } + "400": + description: Невалидная схема проекта или входные данные не верны. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + response: + value: |- + { + "code": 400, + "message": "Validation Failed" + } + + /project/del/{id}: + delete: + tags: + - project + description: | + Удалить проект + parameters: + - description: Идентификатор + in: path + name: id + required: true + schema: + type: integer + example: 1 + responses: + "200": + description: Удаление прошло успешно. + "400": + description: Невалидная схема проекта или входные данные не верны. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 400, + "message": "Validation Failed" + } + "404": + description: Проект не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 404, + "message": "Project not found" + } + + /projects: + get: + tags: + - project + description: | + Получить список проектов + responses: + "200": + description: Запрос прошел успешно + content: + application/json: + schema: + $ref: "#/components/schemas/ProgectsList" + "404": + description: Проект не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 404, + "message": "Projects not found" + } + + /task: + put: + tags: + - task + description: | + Изменение статуса задачи + parameters: + - in: query + required: true + name: id + schema: + type: integer + example: 1 + responses: + "201": + description: Запрос прошел успешно + "400": + description: Невалидная схема задачи или входные данные не верны. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 400, + "message": "Validation Failed" + } + "404": + description: Проект не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 404, + "message": "Tasks not found" + } + post: + tags: + - task + description: | + Добавление задачи + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Task" + responses: + "201": + description: Добавление прошло успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/GoodResponse" + examples: + response: + value: |- + { + "code": 201, + "message": "Task created", + "id": 1 + } + "400": + description: Невалидная схема задачи или входные данные не верны. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 400, + "message": "Validation Failed" + } + get: + tags: + - task + description: | + Получение списка всех задач или конкретного проекта + parameters: + - in: query + name: projectID + schema: + type: integer + example: 1 + responses: + "200": + description: Запрос прошел успешно + content: + application/json: + schema: + $ref: "#/components/schemas/TasksList" + "400": + description: Невалидная схема задачи или входные данные не верны. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 400, + "message": "Validation Failed" + } + "404": + description: Проект не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 404, + "message": "Tasks not found" + } + delete: + tags: + - task + description: | + Удаление задачи с заданным ID + parameters: + - in: query + required: true + name: id + schema: + type: integer + example: 1 + responses: + "200": + description: Запрос прошел успешно + "400": + description: Невалидная схема задачи или входные данные не верны. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 400, + "message": "Validation Failed" + } + "404": + description: Задача не найдена. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + response: + value: |- + { + "code": 404, + "message": "Task not found" + } + +components: + schemas: + Error: + required: + - code + - message + properties: + code: + type: integer + nullable: false + message: + type: string + nullable: false + + GoodResponse: + required: + - code + - id + - message + properties: + code: + type: integer + nullable: false + message: + type: string + nullable: false + id: + type: integer + nullable: false + example: + code: 0 + id: 6 + message: message + + Project: + required: + - description + - id + - name + type: object + properties: + id: + type: integer + description: id project + name: + type: string + description: Имя проекта + nullable: false + description: + type: string + description: Описание проекта + nullable: false + example: + id: 1 + name: Пафос и Превозмогание + description: Прожать батоны и вперед!!! + + ProgectsList: + type: object + properties: + items: + type: array + description: Список существующих проектов + nullable: false + items: + $ref: '#/components/schemas/Project' + example: + items: + - id: 1 + name: Пафос и Превозмогание + description: Прожать батоны и вперед!!! + - id: 1 + name: Пафос и Превозмогание + description: Прожать батоны и вперед!!! + + Task: + required: + - description + - id + - isDone + - name + - projectID + - priority + type: object + properties: + id: + type: integer + description: id task + name: + type: string + description: Имя задачи + nullable: false + description: + type: string + description: Описание задачи + nullable: false + priority: + type: integer + description: приоритет задачи + nullable: false + isDone: + type: boolean + description: Флаг о выполнении задачи + nullable: false + projectID: + type: integer + description: id task + nullable: false + example: + id: 1 + name: 10к подписчиков + description: Прожать батоны и вперед!!! + isDone: false + projectID: 1 + priority: 3 + + TasksList: + type: object + properties: + items: + type: array + description: Список существующих проектов + nullable: false + items: + $ref: '#/components/schemas/Task' + example: + items: + - id: 1 + name: 10к подписчиков + description: Прожать батоны и вперед!!! + isDone: false + projectID: 1 + - id: 1 + name: 10к подписчиков + description: Прожать батоны и вперед!!! + isDone: false + projectID: 1 \ No newline at end of file diff --git a/part_8/8.4/golang/todo-service/todo.db b/part_8/8.4/golang/todo-service/todo.db new file mode 100644 index 0000000000000000000000000000000000000000..2915c0adb9d46b0664ea5b996ef042e8a0c87cdc GIT binary patch literal 16384 zcmeI(Pj4GV6aetqwbP9e^|s<*aWK3{NX1eT8ll303$C*@rp8Wf2PuNqyV;%EL+u^2 zJ0|IkRF#l`?@)^@KT zN>eBf*JU*J6ui#PFvDPP12D#lc;xZOiitPJ>qge_57#1F{@}uC`&X7Ln#_K~{s}Ks zNPq-LfCNZ@1W14cNPq-LfCT;*fm_ zhB#h09rH-|$A+9l@+0o6{u5`h)@i&~?QFnhXXB5FRu*gRR=3xwHd;M!uixmWtLPsM z;=-kN$7$4CM;(rhU4f2s$>}()n$rdMs3CUK)mJJzMZ+qU%f^mO&9dg~YW`U+3;!oP zSZ_71tUHVD3B$#ctE{|co+{PK1`}bxx8sjT_)g#Vrb?#uY`JlNC7VhPe`@o|&+RlG zru~rFKidy&or*F-^1pRe#gMs_AFVc>C`hnh0JKtScSa`|I zv+_zi`Q?;mpfai%OwAA z6+dV8%>K%r*zej4#b2;Wg#<`|1W14cNPq-LfCNZ@1W14co{_*c%VJjt#=P;D*VzYu zmnUC2ukQio!VmRbJRi(kfcHsCeOG_4_f7M{DT|%EjN>T;sA0FY;p*COd3720K0uHA!%+C% z2)e%Dq0i$v^Od~C%5N6t3#*>;E_yM4M{dCu?r^}dCllBbY6!vR`JBaG##!L2cO<7u zguIz0<9!+W( board.Size || - col > board.Size { - fmt.Println("Invalid input. Please try again.") - return -1, -1, false - } - - // Преобразуем введенные координаты (начиная с 1) - // в индексы массива (начиная с 0) - return row - 1, col - 1, true -} - func (p *HumanPlayer) IsComputer() bool { return false } diff --git a/part_8/tic_tac_toe_v7/server/server.go b/part_8/tic_tac_toe_v7/server/server.go index 07f50b3..86e5e26 100644 --- a/part_8/tic_tac_toe_v7/server/server.go +++ b/part_8/tic_tac_toe_v7/server/server.go @@ -76,7 +76,10 @@ func (s *Server) handleConnection(conn net.Conn) { for { var msg network.Message if err := decoder.Decode(&msg); err != nil { - log.Printf("Client %s disconnected: %v", conn.RemoteAddr(), err) + log.Printf( + "Client %s disconnected: %v", conn.RemoteAddr(), + err, + ) s.disconnectedClientHandler(conn) return }