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 0000000..2915c0a Binary files /dev/null and b/part_8/8.4/golang/todo-service/todo.db differ diff --git a/part_8/tic_tac_toe_v7/board/board.go b/part_8/tic_tac_toe_v7/board/board.go index 5097b15..a5bc053 100644 --- a/part_8/tic_tac_toe_v7/board/board.go +++ b/part_8/tic_tac_toe_v7/board/board.go @@ -35,18 +35,6 @@ func (b *Board) IsEmpty() bool { return true } -// Отображение игрового поля -// ToStringSlice converts the board to a slice of strings for serialization. -func (b *Board) ToStringSlice() []string { - slice := make([]string, 0, b.Size*b.Size) - for i := 0; i < b.Size; i++ { - for j := 0; j < b.Size; j++ { - slice = append(slice, b.Board[i][j].String()) - } - } - return slice -} - // Отображение игрового поля func (b *Board) PrintBoard() { fmt.Print(" ") diff --git a/part_8/tic_tac_toe_v7/board/board_cell_type.go b/part_8/tic_tac_toe_v7/board/board_cell_type.go index fcc17ec..1beb472 100644 --- a/part_8/tic_tac_toe_v7/board/board_cell_type.go +++ b/part_8/tic_tac_toe_v7/board/board_cell_type.go @@ -8,16 +8,3 @@ const ( Cross Nought ) - -func (bf BoardField) String() string { - switch bf { - case Empty: - return "." - case Cross: - return "X" - case Nought: - return "O" - default: - return "?" - } -} diff --git a/part_8/tic_tac_toe_v7/database/crud.go b/part_8/tic_tac_toe_v7/database/crud.go index 6a7fdef..ea1b9af 100644 --- a/part_8/tic_tac_toe_v7/database/crud.go +++ b/part_8/tic_tac_toe_v7/database/crud.go @@ -38,9 +38,13 @@ func (r *SQLiteRepository) GetAllFinishedGames() (*[]m.FinishGameSnapshot, error return &finishGameSnapshots, nil } -func (r *SQLiteRepository) GetFinishedGameById(id int) (*m.FinishGameSnapshot, error) { +func (r *SQLiteRepository) GetFinishedGameById( + id int, +) (*m.FinishGameSnapshot, error) { var playerFinishGame PlayerFinishGame - if err := r.db.Where("id = ?", id).First(&playerFinishGame).Error; err != nil { + if err := r.db.Where("id = ?", id).First( + &playerFinishGame, + ).Error; err != nil { return nil, err } return playerFinishGame.ToModel() diff --git a/part_8/tic_tac_toe_v7/database/models.go b/part_8/tic_tac_toe_v7/database/models.go index 901f260..77e1509 100644 --- a/part_8/tic_tac_toe_v7/database/models.go +++ b/part_8/tic_tac_toe_v7/database/models.go @@ -1,14 +1,48 @@ package database -import "time" +import ( + "encoding/json" + b "tic-tac-toe/board" + m "tic-tac-toe/model" + "time" +) // PlayerFinishGame представляет модель таблицы // для хранения завершенной игры в БД type PlayerFinishGame struct { - ID int `gorm:"primary_key;autoIncrement;not null"` - WinnerName string `gorm:"not null"` - AnotherPlayerName string `gorm:"not null"` - BoardJSON []byte `gorm:"type:json;not null"` - PlayerFigure int `gorm:"not null"` - Time time.Time `gorm:"not null"` + // ID игры + ID int `gorm:"primary_key;autoIncrement;not null"` + // Имя победителя + WinnerName string `gorm:"not null"` + // Имя противника + AnotherPlayerName string `gorm:"not null"` + // JSON-представление доски + BoardJSON []byte `gorm:"type:json;not null"` + // Символ победившего игрока + PlayerFigure int `gorm:"not null"` + // Время завершения игры + Time time.Time `gorm:"not null"` +} + +// Задаем имя таблицы для структуры PlayerFinishGame +func (pfg *PlayerFinishGame) TableName() string { + return "player_finish_games" +} + +// Преобразуем таблицу PlayerFinishGame в модель PlayerFinishGame +// из пакета model +func (f *PlayerFinishGame) ToModel() (*m.FinishGameSnapshot, error) { + var board b.Board + if err := json.Unmarshal(f.BoardJSON, &board); err != nil { + return nil, err + } + + return &m.FinishGameSnapshot{ + ID: f.ID, + Board: &board, + PlayerFigure: b.BoardField(f.PlayerFigure), + WinnerName: f.WinnerName, + AnotherPlayerName: f.AnotherPlayerName, + Time: f.Time, + }, nil } diff --git a/part_8/tic_tac_toe_v7/database/utils.go b/part_8/tic_tac_toe_v7/database/utils.go deleted file mode 100644 index 7c4af0b..0000000 --- a/part_8/tic_tac_toe_v7/database/utils.go +++ /dev/null @@ -1,31 +0,0 @@ -package database - -import ( - "encoding/json" - - b "tic-tac-toe/board" - m "tic-tac-toe/model" -) - -// Задаем имя таблицы для структуры PlayerFinishGame -func (pfg *PlayerFinishGame) TableName() string { - return "player_finish_games" -} - -// Преобразуем таблицу PlayerFinishGame в модель PlayerFinishGame -// из пакета model -func (f *PlayerFinishGame) ToModel() (*m.FinishGameSnapshot, error) { - var board b.Board - if err := json.Unmarshal(f.BoardJSON, &board); err != nil { - return nil, err - } - - return &m.FinishGameSnapshot{ - ID: f.ID, - Board: &board, - PlayerFigure: b.BoardField(f.PlayerFigure), - WinnerName: f.WinnerName, - AnotherPlayerName: f.AnotherPlayerName, - Time: f.Time, - }, nil -} diff --git a/part_8/tic_tac_toe_v7/model/finish_game_shapshot.go b/part_8/tic_tac_toe_v7/model/finish_game_shapshot.go index ac739b1..5194864 100644 --- a/part_8/tic_tac_toe_v7/model/finish_game_shapshot.go +++ b/part_8/tic_tac_toe_v7/model/finish_game_shapshot.go @@ -6,10 +6,16 @@ import ( ) type FinishGameSnapshot struct { - ID int `json:"id"` - Board *board.Board `json:"board"` - PlayerFigure board.BoardField `json:"player_figure"` - WinnerName string `json:"winner_name"` - AnotherPlayerName string `json:"another_player_name"` - Time time.Time `json:"time"` + // Идентификатор игры + ID int `json:"id"` + // Состояние доски в момент завершения игры + Board *board.Board `json:"board"` + // Символ (Х/О) победившего игрока + PlayerFigure board.BoardField `json:"player_figure"` + // Имя победителя + WinnerName string `json:"winner_name"` + // Имя противника + AnotherPlayerName string `json:"another_player_name"` + // Время завершения игры + Time time.Time `json:"time"` } diff --git a/part_8/tic_tac_toe_v7/network/client_to_server.go b/part_8/tic_tac_toe_v7/network/client_to_server.go index 42595e6..d349089 100644 --- a/part_8/tic_tac_toe_v7/network/client_to_server.go +++ b/part_8/tic_tac_toe_v7/network/client_to_server.go @@ -1,7 +1,8 @@ package network +// Сообщения от клиента к серверу + const ( - // Client to Server Commands CmdNickname Command = "nickname" CmdJoinRoomRequest Command = "join_room" CmdLeaveRoomRequest Command = "leave_room" @@ -11,29 +12,29 @@ const ( CmdFinishedGameByIdRequest Command = "get_finished_game_by_id" ) -// LoginRequest отправляется клиентом для входа в систему. +// Запрос от клиента для входа в систему type NicknameRequest struct { Nickname string `json:"nickname"` } -// JoinRoomRequest отправляется клиентом для подключения к существующей комнате. +// Запрос от клиента для подключения к существующей комнате type JoinRoomRequest struct { RoomName string `json:"room_name"` PlayerName string `json:"player_name"` } -// LeaveRoomRequest отправляется клиентом для выхода из текущей комнаты. +// Запрос от клиента для выхода из текущей комнаты type LeaveRoomRequest struct { RoomName string `json:"room_name"` PlayerName string `json:"player_name"` } -// ListRoomsRequest отправляется клиентом для получения списка доступных комнат. -// Обычно для этого запроса не требуется специальная полезная нагрузка. +// Запрос от клиента для получения списка доступных комнат +// Обычно для этого запроса не требуется специальная полезная нагрузка type ListRoomsRequest struct { } -// MakeMoveRequest отправляется клиентом для совершения хода в игре. +// Сообщение с данными о ходе игрока type MakeMoveRequest struct { RoomName string `json:"room_name"` PlayerName string `json:"player_name"` @@ -41,12 +42,11 @@ type MakeMoveRequest struct { PositionCol int `json:"position_col"` } -// GetFinishedGamesRequest отправляется клиентом для получения списка завершенных игр. -// Обычно для этого запроса не требуется специальная полезная нагрузка, если запрашиваются все игры для пользователя. +// Запрос от клиента для получения списка завершенных игр type GetFinishedGamesRequest struct { } -// GetFinishedGameByIdRequest отправляется клиентом для получения конкретной завершенной игры. +// Запрос от клиента для получения конкретной завершенной игры type GetFinishedGameByIdRequest struct { GameID int `json:"game_id"` } diff --git a/part_8/tic_tac_toe_v7/network/protocol.go b/part_8/tic_tac_toe_v7/network/protocol.go index d6e7888..fc39023 100644 --- a/part_8/tic_tac_toe_v7/network/protocol.go +++ b/part_8/tic_tac_toe_v7/network/protocol.go @@ -2,8 +2,13 @@ package network import "encoding/json" +// Объявляем тип команды type Command string +// Базовая структура сообщения +// Cmd - тип команды +// Payload - полезная нагрузка, которая должна быть +// сериализована в JSON ([]byte). Может быть nil type Message struct { Cmd Command `json:"command"` Payload json.RawMessage `json:"payload,omitempty"` diff --git a/part_8/tic_tac_toe_v7/network/server_to_client.go b/part_8/tic_tac_toe_v7/network/server_to_client.go index e4e113e..b4e0a57 100644 --- a/part_8/tic_tac_toe_v7/network/server_to_client.go +++ b/part_8/tic_tac_toe_v7/network/server_to_client.go @@ -6,8 +6,8 @@ import ( "tic-tac-toe/model" ) +// Сообщения от сервера к клиенту const ( - // Server to Client Commands CmdUpdateState Command = "update_state" CmdError Command = "error" CmdNickNameResponse Command = "nick_name_response" @@ -21,26 +21,25 @@ const ( CmdFinishedGameResponse Command = "finished_game_response" ) -// сообщение о том, что противник покинул игру -// инициализирующее сообщение в начале партии - -// InitGameResponse отправляется сервером при инициализации игры. +// Отправляется сервером при инициализации игры type InitGameResponse struct { Board b.Board `json:"board"` CurrentPlayer b.BoardField `json:"current_player"` } -// EndGameResponse отправляется сервером при завершении игры. +// Отправляется сервером при завершении игры type EndGameResponse struct { Board b.Board `json:"board"` CurrentPlayer b.BoardField `json:"current_player"` } +// Сообщение о том, что противник покинул игру +// инициализирующее сообщение в начале партии type OpponentLeft struct { Nickname string `json:"nickname"` } -// RoomInfo содержит информацию о комнате. +// Содержит информацию о состоянии комнаты type RoomInfo struct { Name string `json:"name"` BoardSize int `json:"board_size"` @@ -49,46 +48,46 @@ type RoomInfo struct { Difficult g.Difficulty `json:"difficult"` } -// RoomListсодержит список доступных комнат. +// Отправляем клиенту при запросе списка доступных комнат type RoomListResponse struct { Rooms []RoomInfo `json:"rooms"` } -// GameStateUpdate содержит информацию об обновлении состояния игры. +// Отправляем клиенту при обновлении состояния игры type GameStateUpdate struct { Board b.Board `json:"board"` CurrentPlayer b.BoardField `json:"current_player"` } -// ErrorResponse отправляется сервером при возникновении ошибки. +// Отправляется сервером при возникновении ошибки type ErrorResponse struct { Message string `json:"message"` } -// NickNameResponse отправляется сервером при успешном входе клиента. +// Отправляется сервером при успешном входе клиента type NickNameResponse struct { Nickname string `json:"nickname"` } -// RoomCreatedResponse отправляется сервером после успешного создания комнаты. +// Отправляется сервером после успешного создания комнаты type RoomCreatedResponse struct { RoomID string `json:"room_id"` RoomName string `json:"room_name"` } -// RoomJoinResponse отправляется сервером, когда клиент успешно присоединился к комнате. +// Отправляется сервером, когда клиент успешно присоединился к комнате type RoomJoinResponse struct { RoomName string `json:"room_name"` PlayerSymbol b.BoardField `json:"player_symbol"` Board b.Board `json:"board"` } -// FinishedGamesResponse отправляется сервером со списком завершенных игр. +// Отправляется сервером за запрос о списке завершенных игр type FinishedGamesResponse struct { Games *[]model.FinishGameSnapshot `json:"games"` } -// FinishedGameResponse отправляется сервером с информацией о конкретной завершенной игре. +// Отправляется сервером с информацией о конкретной завершенной игре type FinishedGameResponse struct { Game *model.FinishGameSnapshot `json:"game"` } diff --git a/part_8/tic_tac_toe_v7/player/computer_player.go b/part_8/tic_tac_toe_v7/player/computer_player.go index 66f14bb..e2a33bb 100644 --- a/part_8/tic_tac_toe_v7/player/computer_player.go +++ b/part_8/tic_tac_toe_v7/player/computer_player.go @@ -19,8 +19,7 @@ type ComputerPlayer struct { // Создаем нового игрока-компьютера с заданным уровнем сложности func NewComputerPlayer( - figure b.BoardField, - difficulty g.Difficulty, + figure b.BoardField, difficulty g.Difficulty, ) *ComputerPlayer { source := rand.NewSource(time.Now().UnixNano()) return &ComputerPlayer{ @@ -45,12 +44,8 @@ func (p *ComputerPlayer) GetNickname() string { return "Computer" } -func (p *ComputerPlayer) SwitchPlayer() { - if p.Figure == b.Cross { - p.Figure = b.Nought - } else { - p.Figure = b.Cross - } +func (p *ComputerPlayer) CheckSocket(conn net.Conn) bool { + return false } func (p *ComputerPlayer) GetFigure() b.BoardField { @@ -61,6 +56,14 @@ func (p *ComputerPlayer) IsComputer() bool { return true } +func (p *ComputerPlayer) SwitchPlayer() { + if p.Figure == b.Cross { + p.Figure = b.Nought + } else { + p.Figure = b.Cross + } +} + // Реализуем ход компьютера в зависимости от выбранной сложности func (p *ComputerPlayer) MakeMove(board *b.Board) (int, int, bool) { fmt.Printf("%s (Computer) making move... ", p.GetSymbol()) @@ -91,10 +94,6 @@ func (p *ComputerPlayer) makeEasyMove(board *b.Board) (int, int) { return emptyCells[randomIndex][0], emptyCells[randomIndex][1] } -func (p *ComputerPlayer) CheckSocket(conn net.Conn) bool { - return false -} - // Средний уровень: проверяет возможность выигрыша // или блокировки выигрыша противника func (p *ComputerPlayer) makeMediumMove(board *b.Board) (int, int) { diff --git a/part_8/tic_tac_toe_v7/player/i_player.go b/part_8/tic_tac_toe_v7/player/i_player.go index a98111e..68826ef 100644 --- a/part_8/tic_tac_toe_v7/player/i_player.go +++ b/part_8/tic_tac_toe_v7/player/i_player.go @@ -11,22 +11,25 @@ type IPlayer interface { // Получение символа игрока (X или O) GetSymbol() string - // Переключение хода на другого игрока + // Переключение хода на другой символ SwitchPlayer() + // Отправка сообщения игроку-клиенту SendMessage(msg *network.Message) + // Получение никнейма игрока GetNickname() string // Получение текущей фигуры игрока GetFigure() b.BoardField - // Выполнение хода игрока + // Метод для выполнения хода компьютером // Возвращает координаты хода (x, y) и признак успешности MakeMove(board *b.Board) (int, int, bool) // Проверка, является ли игрок компьютером IsComputer() bool + // Проверка владения сокетом CheckSocket(conn net.Conn) bool } diff --git a/part_8/tic_tac_toe_v7/player/player.go b/part_8/tic_tac_toe_v7/player/player.go index 8b7934d..e2b225e 100644 --- a/part_8/tic_tac_toe_v7/player/player.go +++ b/part_8/tic_tac_toe_v7/player/player.go @@ -2,10 +2,7 @@ package player import ( "encoding/json" - "fmt" "net" - "strconv" - "strings" b "tic-tac-toe/board" "tic-tac-toe/network" ) @@ -20,7 +17,9 @@ type HumanPlayer struct { func NewHumanPlayer( nickname string, conn *net.Conn, ) *HumanPlayer { - return &HumanPlayer{Figure: b.Cross, Nickname: nickname, Conn: conn} + return &HumanPlayer{ + Figure: b.Cross, Nickname: nickname, Conn: conn, + } } func (p *HumanPlayer) CheckSocket(conn net.Conn) bool { @@ -58,38 +57,11 @@ func (p *HumanPlayer) GetFigure() b.BoardField { } // Метод-заглушка, т.к. ввод игрока осуществляется на -// уровне пакета game, где нужно еще отрабатывать -// команду на выход и сохранение игровой сессии +// клиентской стороне func (p *HumanPlayer) MakeMove(board *b.Board) (int, int, bool) { return -1, -1, false } -// Обрабатываем строку ввода и -// преобразуем ее в координаты хода -func (p *HumanPlayer) ParseMove( - input string, - board *b.Board, -) (int, int, bool) { - parts := strings.Fields(input) - if len(parts) != 2 { - fmt.Println("Invalid input. Please try again.") - return -1, -1, false - } - - row, err1 := strconv.Atoi(parts[0]) - col, err2 := strconv.Atoi(parts[1]) - if err1 != nil || err2 != nil || - row < 1 || col < 1 || row > 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 }