diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..65a40855 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,9 @@ package main -func addUp() {} +func addUp(n int) int { + sum := 0 + for i := 1; i <= n; i++ { + sum += i + } + return sum +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..8100ecbc 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,20 @@ package main -func sum() {} +import ( + "fmt" + "strconv" +) + +func sum(a, b string) (string, error) { + num1, err := strconv.Atoi(a) + if err != nil { + return "", fmt.Errorf("string: %s cannot be converted", a) + } + num2, err := strconv.Atoi(b) + if err != nil { + return "", fmt.Errorf("string: %s cannot be converted", b) + } + + res := num1 + num2 + return strconv.Itoa(res), nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..906b9586 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,16 @@ package main -func binary() {} +import ( + "fmt" + "strconv" +) + +func binary(a string) (string, error) { + num, err := strconv.Atoi(a) + if err != nil { + return "", fmt.Errorf("invalid decimal number: %s", a) + } + + binary := strconv.FormatInt(int64(num), 2) + return binary, nil +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..9921ecf5 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,9 @@ package main -func numberSquares() {} +func numberSquares(n int) int { + total := 0 + for i := 1; i <= n; i++ { + total += (n - i + 1) * (n - i + 1) + } + return total +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..d93c31f4 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,15 @@ package main -func detectWord() {} +import ( + "unicode" +) + +func detectWord(word string) string { + var result []rune + for _, ch := range word { + if unicode.IsLower(ch) { + result = append(result, ch) + } + } + return string(result) +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..33954719 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,9 @@ package main -func potatoes() {} +import ( + "strings" +) + +func potatoes(word string) int { + return strings.Count(word, "potato") +} diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..de5d0d2e 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,18 @@ package main -func emojify() {} +import ( + "strings" +) + +func emojify(res string) string { + replacer := map[string]string{ + "smile": "🙂", + "grin": "😀", + "sad": "😥", + "mad": "😠", + } + for word, emoji := range replacer { + res = strings.ReplaceAll(res, word, emoji) + } + return res +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..81434636 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,18 @@ package main -func highestDigit() {} +import ( + "strconv" +) + +func highestDigit(num int) int { + numToStr := strconv.Itoa(num) + highest := 0 + + for _, val := range numToStr { + digit, _ := strconv.Atoi(string(val)) + if digit > highest { + highest = digit + } + } + return highest +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..752fbc0c 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,18 @@ package main -func countVowels() {} +import ( + "strings" +) + +func countVowels(str string) int { + str = strings.ToLower(str) + vowels := "aoiue" + count := 0 + + for _, ch := range str { + if strings.ContainsRune(vowels, ch) { + count++ + } + } + return count +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..55e5f713 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,18 @@ package main -func bitwiseAND() {} +import "fmt" -func bitwiseOR() {} +func bitwiseAND(a, b int) string { + res := a & b + return fmt.Sprintf("%08b", res) +} -func bitwiseXOR() {} +func bitwiseOR(a, b int) string { + res := a | b + return fmt.Sprintf("%08b", res) +} + +func bitwiseXOR(a, b int) string { + res := a ^ b + return fmt.Sprintf("%08b", res) +} diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..cd3fca1f 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,11 @@ package problem1 -func isChangeEnough() { +func IsChangeEnough(change []int, amount float64) bool { + coinValues := []float64{0.25, 0.10, 0.05, 0.01} + + totalChange := 0.0 + for i := 0; i < len(change); i++ { + totalChange += float64(change[i]) * coinValues[i] + } + return totalChange >= amount } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..14fd6b5a 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,13 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(string) func(int)) { + brands := make(map[string]int) + + makeB := func(brand string) func(int) { + return func(count int) { + brands[brand] += count + } + } + + return brands, makeB +} diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..883ad1b1 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,14 @@ package problem11 -func removeDups() {} +func removeDups[T comparable](items []T) []T { + seen := make(map[T]bool) + var result []T + + for _, item := range items { + if !seen[item] { + seen[item] = true + result = append(result, item) + } + } + return result +} diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..dd615a8c 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,5 @@ package problem11 func keysAndValues() {} + +//idk(( diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..b8765933 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,14 @@ package problem2 -func capitalize() { +import ( + "strings" +) + +func capitalize(names []string) []string { + result := make([]string, len(names)) + + for i, name := range names { + result[i] = strings.Title(strings.ToLower(name)) + } + return result } diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..9599419e 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,18 @@ package problem4 -func mapping() { +import ( + "fmt" + "strings" +) + +func mapping(lowerCase []string) map[string]string { + result := make(map[string]string) + + for _, s := range lowerCase { + result[s] = strings.ToUpper(s) + } + return result +} +func main() { + fmt.Println(mapping([]string{"p", "s"})) } diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..53731611 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,20 @@ package problem5 -func products() { +import "sort" + +func products(prices map[string]int, minimum int) []string { + var result []string + + for product, price := range prices { + if price >= minimum { + result = append(result, product) + } + } + + sort.Slice(result, func(i, j int) bool { + return prices[result[i]] < prices[result[j]] || + (prices[result[i]] == prices[result[j]] && result[i] < result[j]) + }) + + return result } diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..7b4a7ca7 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,19 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(a []int, b []int, target int) bool { + + bMap := make(map[int]struct{}) + + for _, num := range b { + bMap[num] = struct{}{} + } + + for _, num := range a { + complement := target - num + if _, exists := bMap[complement]; exists { + return true + } + } + + return false } diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..6c9884d7 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,7 @@ package problem7 -func swap() { +func swap(x *int, y *int) { + temp := *x + *x = *y + *y = temp } diff --git a/exercise2/problem8/problem8.go b/exercise2/problem8/problem8.go index 9389d3b0..15e97e1a 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -1,16 +1,14 @@ package problem8 func simplify(list []string) map[string]int { - var indMap map[string]int - - indMap = make(map[string]int) - load(&indMap, &list) + indMap := make(map[string]int) + load(&indMap, list) return indMap } -func load(m *map[string]int, students *[]string) { - for i, name := range *students { +func load(m *map[string]int, students []string) { + for i, name := range students { (*m)[name] = i } } diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..5cca036a 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,11 @@ package problem9 -func factory() {} +func factory(multiplier int) func(...int) []int { + return func(nums ...int) []int { + result := make([]int, len(nums)) + for i, num := range nums { + result[i] = num * multiplier + } + return result + } +} diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..ecdd8321 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,35 @@ package problem1 -type Queue struct{} +import "errors" + +type Queue struct { + elements []interface{} +} + +func (q *Queue) Enqueue(element interface{}) { + q.elements = append(q.elements, element) +} + +func (q Queue) IsEmpty() bool { + return len(q.elements) == 0 +} + +func (q *Queue) Dequeue() (interface{}, error) { + if q.IsEmpty() { + return nil, errors.New("Queue is empty") + } + element := q.elements[0] + q.elements = q.elements[1:] + return element, nil +} + +func (q *Queue) Peek() (interface{}, error) { + if q.IsEmpty() { + return nil, errors.New("Queue is empty") + } + return q.elements[0], nil +} + +func (q *Queue) Size() int { + return len(q.elements) +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..25f52fbb 100644 --- a/exercise3/problem2/problem2.go +++ b/exercise3/problem2/problem2.go @@ -1,3 +1,31 @@ package problem2 -type Stack struct{} +type Stack struct { + elements []interface{} +} + +func (s *Stack) Push(v interface{}) { + s.elements = append(s.elements, v) +} +func (s *Stack) Pop() interface{} { + if s.IsEmpty() { + return nil + } + topIndex := len(s.elements) - 1 + element := s.elements[topIndex] + s.elements = s.elements[:topIndex] + return element +} + +func (s *Stack) IsEmpty() bool { + return len(s.elements) == 0 +} +func (s *Stack) Peek() interface{} { + if s.IsEmpty() { + return nil + } + return s.elements[len(s.elements)-1] +} +func (s *Stack) Size() int { + return len(s.elements) +} diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..bb6dd730 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,96 @@ package problem3 -type Set struct{} +type Set struct { + elements map[interface{}]struct{} +} + +func NewSet() *Set { + return &Set{elements: make(map[interface{}]struct{})} +} + +func (s *Set) Add(element interface{}) { + s.elements[element] = struct{}{} + +} + +func (s *Set) Remove(element interface{}) { + delete(s.elements, element) + +} + +func (s *Set) IsEmpty() bool { + return len(s.elements) == 0 +} + +func (s *Set) Size() int { + return len(s.elements) +} + +func (s *Set) List() []interface{} { + list := make([]interface{}, 0, s.Size()) + for element := range s.elements { + list = append(list, element) + } + return list +} + +func (s *Set) Has(element interface{}) bool { + _, ok := s.elements[element] + return ok +} +func (s *Set) Copy() *Set { + newSet := NewSet() + for element := range s.elements { + newSet.Add(element) + } + return newSet +} + +func (s *Set) Difference(other *Set) *Set { + diffSet := NewSet() + for element := range s.elements { + if !other.Has(element) { + diffSet.Add(element) + } + } + return diffSet +} + +//func (s *Set) Union(other *Set) *Set { +// unionSet := NewSet() +// for element := range s.elements { +// unionSet.Add(element) +// } +// for element := range other.elements { +// unionSet.Add(element) +// } +//} + +func (s *Set) IsSubset(other *Set) bool { + for element := range s.elements { + if !other.Has(element) { + return false + } + } + return true +} + +func (s *Set) Union(other ...*Set) *Set { + if s.IsEmpty() { + return NewSet() + } + unionSet := NewSet() + for element := range s.elements { + isInAll := true + for _, otherElement := range other { + if otherElement.Has(element) { + isInAll = false + break + } + } + if isInAll { + unionSet.Add(element) + } + } + return unionSet +} diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..50e21b3e 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,4 @@ package problem4 -type LinkedList struct{} +type LinkedList struct { +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..d9eb9836 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,17 @@ package problem5 -type Person struct{} +import "fmt" + +type Person struct { + Name string + Age int +} + +func (p *Person) compareAge(other *Person) string { + if p.Age < other.Age { + return fmt.Sprintf("%s is older than me", other.Name) + } else if p.Age > other.Age { + return fmt.Sprintf("%s is younger than me", other.Name) + } + return fmt.Sprintf("%s is same age as me", other.Name) +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..121b5c34 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,24 @@ package problem6 -type Animal struct{} +type Animal struct { + name string + legsNum int +} -type Insect struct{} +type Insect struct { + name string + legsNum int +} -func sumOfAllLegsNum() {} +func sumOfAllLegsNum(entities ...interface{}) int { + total := 0 + for _, entity := range entities { + switch e := entity.(type) { + case Animal: + total += e.legsNum + case Insect: + total += e.legsNum + } + } + return total +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..2cee8e60 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,53 @@ package problem7 +import "fmt" + type BankAccount struct { + name string + balance float64 } type FedexAccount struct { + name string + packages []string } type KazPostAccount struct { + name string + balance float64 + packages []string +} + +func withdrawMoney(amount float64, accounts ...interface{}) { + for _, account := range accounts { + switch acc := account.(type) { + case *BankAccount: + if acc.balance >= amount { + acc.balance -= amount + fmt.Printf("Withdrew %.2f from %s's bank account. New balance: %.2f\n", amount, acc.name, acc.balance) + } else { + fmt.Printf("Insufficient funds in %s's bank account to withdraw %.2f\n", acc.name, amount) + } + case *KazPostAccount: + if acc.balance >= amount { + acc.balance -= amount + fmt.Printf("Withdrew %.2f from %s's KazPost account. New balance: %.2f\n", amount, acc.name, acc.balance) + } else { + fmt.Printf("Insufficient funds in %s's KazPost account to withdraw %.2f\n", acc.name, amount) + } + } + } +} +func sendPackagesTo(recipient string, accounts ...interface{}) { + for _, account := range accounts { + switch acc := account.(type) { + case *FedexAccount: + acc.packages = append(acc.packages, recipient) + fmt.Printf("Sent a package to %s from %s's FedEx account. Total packages sent: %v\n", recipient, acc.name, acc.packages) + + case *KazPostAccount: + acc.packages = append(acc.packages, recipient) + fmt.Printf("Sent a package to %s from %s's KazPost account. Total packages sent: %v\n", recipient, acc.name, acc.packages) + } + } } diff --git a/exercise4/bot/game/gameplay/gameplay.go b/exercise4/bot/game/gameplay/gameplay.go new file mode 100644 index 00000000..f5e5891f --- /dev/null +++ b/exercise4/bot/game/gameplay/gameplay.go @@ -0,0 +1,79 @@ +package gameplay + +import "math" + +func GetBestMove(board []string, token string) int { + bestScore := math.Inf(-1) + bestMove := -1 + + for i := 0; i < 9; i++ { + if board[i] == " " { + board[i] = token + score := minimax(board, 0, false, token, math.Inf(-1), math.Inf(1)) + board[i] = " " + if score > bestScore { + bestScore = score + bestMove = i + } + } + } + return bestMove +} + +func minimax(board []string, depth int, isMaximizing bool, token string, alpha, beta float64) float64 { + opponentToken := "o" + if token == "o" { + opponentToken = "x" + } + + if isWinning(board, token) { + return 1 + } + if isWinning(board, opponentToken) { + return -1 + } + if isBoardFull(board) { + return 0 + } + + if isMaximizing { + bestScore := math.Inf(-1) + for i := 0; i < 9; i++ { + if board[i] == " " { + board[i] = token + score := minimax(board, depth+1, false, token, alpha, beta) + board[i] = " " + bestScore = math.Max(bestScore, score) + alpha = math.Max(alpha, bestScore) + if beta <= alpha { + break + } + } + } + return bestScore + } else { + bestScore := math.Inf(1) + for i := 0; i < 9; i++ { + if board[i] == " " { + board[i] = opponentToken + score := minimax(board, depth+1, true, token, alpha, beta) + board[i] = " " + bestScore = math.Min(bestScore, score) + beta = math.Min(beta, bestScore) + if beta <= alpha { + break + } + } + } + return bestScore + } +} + +func isBoardFull(board []string) bool { + for _, cell := range board { + if cell == " " { + return false + } + } + return true +} diff --git a/exercise4/bot/game/gameplay/win.go b/exercise4/bot/game/gameplay/win.go new file mode 100644 index 00000000..c0ef1623 --- /dev/null +++ b/exercise4/bot/game/gameplay/win.go @@ -0,0 +1,21 @@ +package gameplay + +func isWinning(board []string, token string) bool { + // Задаем индексы выигрышных позиций + winningPositions := [][]int{ + {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // Горизонтальные + {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // Вертикальные + {0, 4, 8}, {2, 4, 6}, // Диагонали + } + + // Проверяем каждую выигрышную позицию + for _, positions := range winningPositions { + // Если все три клетки в позиции заняты нужным токеном + if board[positions[0]] == token && board[positions[1]] == token && board[positions[2]] == token { + return true + } + } + + // Если ни одна позиция не подошла + return false +} diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..90b10b6b 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -1,20 +1,65 @@ package main import ( + "bytes" "context" + "encoding/json" + "fmt" + "net/http" "os" "os/signal" "syscall" + "time" ) +func sendRequest(ctx context.Context) { + port := os.Getenv("PORT") + name := os.Getenv("NAME") + + // Если имя не задано в переменной окружения, установить значение по умолчанию + if name == "" { + name = "Unknown Player" + } + + js, err := json.Marshal(map[string]interface{}{ + "name": name, // Используем имя из переменной окружения + "url": fmt.Sprintf("http://127.0.0.1:%s", port), + }) + if err != nil { + fmt.Println("Error during marshaling:", err) + return + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + "http://127.0.0.1:4444/join", + bytes.NewBuffer(js), + ) + if err != nil { + fmt.Println("Error creating request:", err) + return + } + + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + fmt.Println("Error sending join request:", err) + return + } + defer res.Body.Close() + + fmt.Println("Response status:", res.StatusCode) +} + func main() { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() - ready := startServer() + ready := startServer(ctx) + sendRequest(ctx) <-ready - // TODO after server start - stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop // Wait for SIGINT or SIGTERM diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go index e6760ec5..dcd8b6c5 100644 --- a/exercise4/bot/server.go +++ b/exercise4/bot/server.go @@ -1,13 +1,21 @@ package main import ( + "bytes" + "context" + "encoding/json" "errors" "fmt" + "io" + "log/slog" "net" "net/http" "os" + "strings" "sync" "time" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/game/gameplay" ) type readyListener struct { @@ -21,21 +29,111 @@ func (l *readyListener) Accept() (net.Conn, error) { return l.Listener.Accept() } -func startServer() <-chan struct{} { - ready := make(chan struct{}) +type RequestMove struct { + Board []string `json:"board"` + Token string `json:"token"` +} - listener, err := net.Listen("tcp", fmt.Sprintf(":%s", os.Getenv("PORT"))) +func pingHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + w.WriteHeader(http.StatusOK) +} + +func moveHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + fmt.Println("Received request:", r.Method, r.URL.Path) + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Can't read body", http.StatusBadRequest) + return + } + fmt.Println("Request Body:", string(bodyBytes)) + + // Reset the request body + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + var reqMove RequestMove + if err := json.Unmarshal(bodyBytes, &reqMove); err != nil { + http.Error(w, "Bad request: "+err.Error(), http.StatusBadRequest) + return + } + + board := reqMove.Board + token := strings.ToLower(reqMove.Token) // tokens are lowercase "x" or "o" + + if len(board) != 9 { + http.Error(w, "Invalid board size", http.StatusBadRequest) + return + } + + if token != "x" && token != "o" { + http.Error(w, "Invalid token", http.StatusBadRequest) + return + } + + index := gameplay.GetBestMove(board, token) + if index == -1 { + http.Error(w, "No moves left", http.StatusBadRequest) + return + } + + response := map[string]int{"index": index} + js, err := json.Marshal(response) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(js) + if err != nil { + fmt.Printf("Failed to write response: %v\n", err) + + } +} + +func startServer(ctx context.Context) <-chan struct{} { + port := os.Getenv("PORT") + if port == "" { + port = "4081" + } + mux := http.NewServeMux() + mux.HandleFunc("/ping", pingHandler) + mux.HandleFunc("/move", moveHandler) + + server := &http.Server{ + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 15 * time.Second, + BaseContext: func(_ net.Listener) context.Context { + return ctx + }, + } + + listener, err := net.Listen("tcp", fmt.Sprintf(":%s", port)) if err != nil { panic(err) } + ready := make(chan struct{}) list := &readyListener{Listener: listener, ready: ready} - srv := &http.Server{ - IdleTimeout: 2 * time.Minute, - } + + slog.InfoContext( + ctx, + "starting service", + "port", port, + ) go func() { - err := srv.Serve(list) + err := server.Serve(list) if !errors.Is(err, http.ErrServerClosed) { panic(err) } diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..9e6c9419 100644 --- a/exercise5/problem1/problem1.go +++ b/exercise5/problem1/problem1.go @@ -1,9 +1,15 @@ package problem1 +import "sync" + func incrementConcurrently(num int) int { + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() num++ }() + wg.Wait() return num } diff --git a/exercise5/problem2/problem2.go b/exercise5/problem2/problem2.go index 16d38e1d..32a8e157 100644 --- a/exercise5/problem2/problem2.go +++ b/exercise5/problem2/problem2.go @@ -1,5 +1,10 @@ package problem2 +import ( + "runtime" + "sync" +) + // add - sequential code to add numbers, don't update it, just to illustrate concept func add(numbers []int) int64 { var sum int64 @@ -11,6 +16,32 @@ func add(numbers []int) int64 { func addConcurrently(numbers []int) int64 { var sum int64 + numCores := runtime.NumCPU() + chunkSize := (len(numbers) + numCores - 1) / numCores + + var wg sync.WaitGroup + var mu sync.Mutex + + for i := 0; i < len(numbers); i += chunkSize { + wg.Add(1) + end := i + chunkSize + if end > len(numbers) { + end = len(numbers) + } + go func(chunk []int) { + defer wg.Done() + + localSum := int64(0) + for _, n := range chunk { + localSum += int64(n) + } + + mu.Lock() + sum += localSum + mu.Unlock() + }(numbers[i:end]) + } + wg.Wait() return sum } diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..ec7d8a50 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -1,11 +1,11 @@ package problem3 func sum(a, b int) int { - var c int + c := make(chan int) go func(a, b int) { - c = a + b + c <- a + b }(a, b) - return c + return <-c } diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index b5899ddf..6c05ac2e 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -4,6 +4,7 @@ func iter(ch chan<- int, nums []int) { for _, n := range nums { ch <- n } + close(ch) } func sum(nums []int) int { diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..ca902da8 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,8 +1,22 @@ package problem5 -func producer() {} +func producer(words []string, ch chan<- string) { + for _, word := range words { + ch <- word + } + close(ch) +} -func consumer() {} +func consumer(ch <-chan string) string { + var res string + for word := range ch { + if res != "" { + res += " " // Add a space between words + } + res += word + } + return res +} func send( words []string, diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..c3042aca 100644 --- a/exercise5/problem7/problem7.go +++ b/exercise5/problem7/problem7.go @@ -1,3 +1,26 @@ package problem7 -func multiplex(ch1 <-chan string, ch2 <-chan string) []string {} +func multiplex(ch1 <-chan string, ch2 <-chan string) []string { + var res []string + done := make(chan struct{}) + + go func() { + for v := range ch1 { + res = append(res, v) + } + done <- struct{}{} + }() + + go func() { + for v := range ch2 { + res = append(res, v) + } + done <- struct{}{} + }() + + <-done + <-done + + return res + +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..c9e5d102 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -4,4 +4,11 @@ import ( "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + select { + case message := <-ch: + return message + case <-time.After(ttl): + return "timeout" + } +} diff --git a/exercise6/problem1/problem1.go b/exercise6/problem1/problem1.go index ee453b24..e62c52fc 100644 --- a/exercise6/problem1/problem1.go +++ b/exercise6/problem1/problem1.go @@ -1,9 +1,33 @@ package problem1 +import "sync" + type bankAccount struct { blnc int + mu sync.Mutex } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + return &bankAccount{blnc: blnc} +} + +func (a *bankAccount) Deposit(amount int) { + a.mu.Lock() + defer a.mu.Unlock() + a.blnc += amount +} +func (a *bankAccount) Balance() int { + a.mu.Lock() + defer a.mu.Unlock() + return a.blnc +} + +func (a *bankAccount) Withdraw(amount int) bool { + a.mu.Lock() + defer a.mu.Unlock() + if a.blnc >= amount { + a.blnc -= amount + return true + } + return false } diff --git a/exercise6/problem2/problem2.go b/exercise6/problem2/problem2.go index 97e02368..3cea4f5e 100644 --- a/exercise6/problem2/problem2.go +++ b/exercise6/problem2/problem2.go @@ -1,6 +1,7 @@ package problem2 import ( + "sync" "time" ) @@ -8,13 +9,17 @@ var readDelay = 10 * time.Millisecond type bankAccount struct { blnc int + mu sync.Mutex } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + return &bankAccount{blnc: blnc} } func (b *bankAccount) balance() int { + b.mu.Lock() + defer b.mu.Unlock() + time.Sleep(readDelay) - return 0 + return b.blnc } diff --git a/exercise6/problem3/problem3.go b/exercise6/problem3/problem3.go index b34b90bb..7f2c1a9d 100644 --- a/exercise6/problem3/problem3.go +++ b/exercise6/problem3/problem3.go @@ -1,5 +1,7 @@ package problem3 +import "sync/atomic" + type counter struct { val int64 } @@ -9,3 +11,15 @@ func newCounter() *counter { val: 0, } } + +func (c *counter) Increment() { + atomic.AddInt64(&c.val, 1) +} + +func (c *counter) Decrement() { + atomic.AddInt64(&c.val, -1) +} + +func (c *counter) Value() int64 { + return atomic.LoadInt64(&c.val) +} diff --git a/exercise6/problem4/problem4.go b/exercise6/problem4/problem4.go index 793449c9..77d165bf 100644 --- a/exercise6/problem4/problem4.go +++ b/exercise6/problem4/problem4.go @@ -1,28 +1,40 @@ package problem4 import ( + "sync" "time" ) -func worker(id int, _ *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +var mu sync.Mutex +var cond = sync.NewCond(&mu) + +func worker(id int, shoppingList *[]string, ch chan<- int) { + mu.Lock() + for len(*shoppingList) == 0 { + cond.Wait() + } + mu.Unlock() + ch <- id } func updateShopList(shoppingList *[]string) { time.Sleep(10 * time.Millisecond) + mu.Lock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + cond.Broadcast() + mu.Unlock() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - for i := range numWorkers { + for i := 0; i < numWorkers; i++ { go worker(i+1, shoppingList, notifier) - time.Sleep(time.Millisecond) // order matters + time.Sleep(time.Millisecond) } go updateShopList(shoppingList) diff --git a/exercise6/problem5/problem5.go b/exercise6/problem5/problem5.go index 8e4a1703..5e313035 100644 --- a/exercise6/problem5/problem5.go +++ b/exercise6/problem5/problem5.go @@ -1,26 +1,36 @@ package problem5 import ( + "sync" "time" ) +var mu sync.Mutex +var cond = sync.NewCond(&mu) + func worker(id int, shoppingList *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed + mu.Lock() + for len(*shoppingList) == 0 { + cond.Wait() + } + mu.Unlock() ch <- id } func updateShopList(shoppingList *[]string) { time.Sleep(10 * time.Millisecond) - + mu.Lock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + cond.Broadcast() + mu.Unlock() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - for i := range numWorkers { + for i := 0; i < numWorkers; i++ { go worker(i+1, shoppingList, notifier) time.Sleep(time.Millisecond) // order matters } diff --git a/exercise6/problem7/problem7.go b/exercise6/problem7/problem7.go index ef49497b..b87466d0 100644 --- a/exercise6/problem7/problem7.go +++ b/exercise6/problem7/problem7.go @@ -3,15 +3,21 @@ package problem7 import ( "fmt" "math/rand" + "sync" "time" ) func task() { start := time.Now() var t *time.Timer + var mu sync.Mutex + t = time.AfterFunc( randomDuration(), func() { + mu.Lock() + defer mu.Unlock() + fmt.Println(time.Now().Sub(start)) t.Reset(randomDuration()) }, diff --git a/exercise6/problem8/problem8.go b/exercise6/problem8/problem8.go index 949eb2d2..7137b407 100644 --- a/exercise6/problem8/problem8.go +++ b/exercise6/problem8/problem8.go @@ -1,3 +1,20 @@ package problem8 -func multiplex(chs []<-chan string) []string {} +func multiplex(chs []<-chan string) []string { + var result []string + d := make(chan bool) + + for _, ch := range chs { + go func(ch <-chan string) { + for v := range ch { + result = append(result, v) + } + d <- true + }(ch) + } + + for range chs { + <-d + } + return result +} diff --git a/exercise7/blogging-platform/Dockerfile b/exercise7/blogging-platform/Dockerfile new file mode 100644 index 00000000..c28803ca --- /dev/null +++ b/exercise7/blogging-platform/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.23 + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o blog-crud ./cmd + +EXPOSE 8080 + +CMD ["./blog-crud"] diff --git a/exercise7/blogging-platform/cmd/main.go b/exercise7/blogging-platform/cmd/main.go new file mode 100644 index 00000000..b9b28248 --- /dev/null +++ b/exercise7/blogging-platform/cmd/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/config" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/handler" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/repository" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/service" +) + +func main() { + db := config.ConnectDB() + defer db.Close() + + postRepo := repository.NewPostRepository(db) + postService := service.NewPostService(postRepo) + postHandler := handler.NewPostHandler(postService) + + r := mux.NewRouter() + + // CRUD роуты для постов + r.HandleFunc("/posts", postHandler.CreatePost).Methods("POST") // создание + r.HandleFunc("/posts", postHandler.GetAllPosts).Methods("GET") // получить все + r.HandleFunc("/posts/{id:[0-9]+}", postHandler.GetPostByID).Methods("GET") // получить по ID + r.HandleFunc("/posts/{id:[0-9]+}", postHandler.UpdatePost).Methods("PUT") // обновить + r.HandleFunc("/posts/{id:[0-9]+}", postHandler.DeletePost).Methods("DELETE") // удалить + + // простой хелсчек + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Welcome to the blogging platform!")) + }) + + fmt.Println("Сервер запущен на порту 8080") + log.Fatal(http.ListenAndServe(":8080", r)) +} diff --git a/exercise7/blogging-platform/config/config.go b/exercise7/blogging-platform/config/config.go new file mode 100644 index 00000000..ad470a6b --- /dev/null +++ b/exercise7/blogging-platform/config/config.go @@ -0,0 +1,25 @@ +package config + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/go-sql-driver/mysql" +) + +func ConnectDB() *sql.DB { + dsn := "user:root@tcp(127.0.0.1:3307)/blog_platform" + + db, err := sql.Open("mysql", dsn) + if err != nil { + log.Fatalf("Error connecting to the database: %v", err) + } + + if err = db.Ping(); err != nil { + log.Fatalf("DB is not responding: %v", err) + } + + fmt.Println("Successful connection to the database") + return db +} diff --git a/exercise7/blogging-platform/docker-compose.yml b/exercise7/blogging-platform/docker-compose.yml new file mode 100644 index 00000000..0f417700 --- /dev/null +++ b/exercise7/blogging-platform/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: blog_mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: blogdb + MYSQL_USER: user + MYSQL_PASSWORD: password + ports: + - "3307:3306" + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..0ae19bff 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -2,4 +2,9 @@ module github.com/talgat-ruby/exercises-go/exercise7/blogging-platform go 1.23.3 -require github.com/lib/pq v1.10.9 +require ( + github.com/go-sql-driver/mysql v1.9.0 + github.com/gorilla/mux v1.8.1 +) + +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..e5d89554 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,2 +1,6 @@ -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/exercise7/blogging-platform/internal/handler/post_handler.go b/exercise7/blogging-platform/internal/handler/post_handler.go new file mode 100644 index 00000000..4f2febc6 --- /dev/null +++ b/exercise7/blogging-platform/internal/handler/post_handler.go @@ -0,0 +1,120 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/gorilla/mux" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/service" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/models" +) + +type PostHandler struct { + service *service.PostService +} + +func NewPostHandler(service *service.PostService) *PostHandler { + return &PostHandler{ + service: service, + } +} + +// CreatePost +func (h *PostHandler) CreatePost(w http.ResponseWriter, r *http.Request) { + var post models.Post + + if err := json.NewDecoder(r.Body).Decode(&post); err != nil { + http.Error(w, "Invalid request format", http.StatusBadRequest) + return + } + + if err := h.service.CreatePost(&post); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(w).Encode(post); err != nil { + http.Error(w, "Error when sending the response", http.StatusInternalServerError) + } +} + +// GetAllPosts +func (h *PostHandler) GetAllPosts(w http.ResponseWriter, r *http.Request) { + posts, err := h.service.GetAllPosts() + if err != nil { + http.Error(w, "Error receiving posts", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(posts); err != nil { + http.Error(w, "Error when sending the response", http.StatusInternalServerError) + } +} + +// GetPostByID +func (h *PostHandler) GetPostByID(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid ID format", http.StatusBadRequest) + return + } + + post, err := h.service.GetPostByID(int64(id)) + if err != nil { + http.Error(w, "The post was not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(post); err != nil { + http.Error(w, "Error when sending the response", http.StatusInternalServerError) + } +} + +// UpdatePost +func (h *PostHandler) UpdatePost(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid request format", http.StatusBadRequest) + return + } + + var post models.Post + if err := json.NewDecoder(r.Body).Decode(&post); err != nil { + http.Error(w, "Invalid request format", http.StatusBadRequest) + return + } + + post.ID = int64(id) + + if err := h.service.UpdatePost(&post); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("The post has been updated successfully")) +} + +// DeletePost — удаление поста по ID +func (h *PostHandler) DeletePost(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid ID format", http.StatusBadRequest) + return + } + + if err := h.service.DeletePost(int64(id)); err != nil { + http.Error(w, "Error when deleting a post", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("deleted successfully")) +} diff --git a/exercise7/blogging-platform/internal/repository/post_repository.go b/exercise7/blogging-platform/internal/repository/post_repository.go new file mode 100644 index 00000000..2570730c --- /dev/null +++ b/exercise7/blogging-platform/internal/repository/post_repository.go @@ -0,0 +1,151 @@ +package repository + +import ( + "database/sql" + "log" + "strings" + "time" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/models" +) + +type PostRepository struct { + db *sql.DB +} + +func NewPostRepository(db *sql.DB) *PostRepository { + return &PostRepository{db: db} +} + +// Create +func (r *PostRepository) Create(post *models.Post) error { + tagsString := strings.Join(post.Tags, ",") + query := `INSERT INTO posts (title, content, category, tags) VALUES (?, ?, ?, ?)` + _, err := r.db.Exec(query, post.Title, post.Content, post.Category, tagsString) + if err != nil { + log.Printf("Error when creating a post: %v", err) + } + return err +} + +// GetPostByID +func (r *PostRepository) GetPostByID(id int) (*models.Post, error) { + query := `SELECT id, title, content, created_at, category, tags, updated_at FROM posts WHERE id = ?` + row := r.db.QueryRow(query, id) + + post := &models.Post{} + var tagsString sql.NullString + var createdAtStr, updatedAtStr string + err := row.Scan( + &post.ID, + &post.Title, + &post.Content, + &createdAtStr, + &post.Category, + &tagsString, + &updatedAtStr, + ) + if err != nil { + if err == sql.ErrNoRows { + log.Printf("Post with ID %d not found", id) + return nil, nil + } + log.Printf("Error when receiving a post by ID: %v", err) + return nil, err + } + + post.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtStr) + if err != nil { + log.Printf("Error during conversion created_at: %v", err) + return nil, err + } + + post.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtStr) + if err != nil { + log.Printf("Error during conversion updated_at: %v", err) + return nil, err + } + + if tagsString.Valid { + post.Tags = strings.Split(tagsString.String, ",") + } + + return post, nil +} + +// GetAll +func (r *PostRepository) GetAll() ([]models.Post, error) { + query := `SELECT id, title, content, created_at, category, tags, updated_at FROM posts` + rows, err := r.db.Query(query) + if err != nil { + log.Printf("Error when receiving all posts: %v", err) + return nil, err + } + defer rows.Close() + + var posts []models.Post + for rows.Next() { + post := models.Post{} + var tagsString sql.NullString + var createdAtStr, updatedAtStr string + err := rows.Scan( + &post.ID, + &post.Title, + &post.Content, + &createdAtStr, + &post.Category, + &tagsString, + &updatedAtStr, + ) + if err != nil { + log.Printf("Error when scanning a line of a post: %v", err) + return nil, err + } + + post.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtStr) + if err != nil { + log.Printf("Error during conversion created_at: %v", err) + return nil, err + } + + post.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtStr) + if err != nil { + log.Printf("Error during conversion updated_at: %v", err) + return nil, err + } + + if tagsString.Valid { + post.Tags = strings.Split(tagsString.String, ",") + } + + posts = append(posts, post) + } + + if err := rows.Err(); err != nil { + log.Printf("Error when processing strings: %v", err) + return nil, err + } + + return posts, nil +} + +// Update +func (r *PostRepository) Update(post *models.Post) error { + tagsString := strings.Join(post.Tags, ",") + query := `UPDATE posts SET title = ?, content = ?, category = ?, tags = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?` + _, err := r.db.Exec(query, post.Title, post.Content, post.Category, tagsString, post.ID) + if err != nil { + log.Printf("Error when updating a post with an ID %d: %v", post.ID, err) + } + return err +} + +// Delete +func (r *PostRepository) Delete(id int) error { + query := `DELETE FROM posts WHERE id = ?` + _, err := r.db.Exec(query, id) + if err != nil { + log.Printf("Error when deleting a post with an ID %d: %v", id, err) + } + return err +} diff --git a/exercise7/blogging-platform/internal/service/post_service.go b/exercise7/blogging-platform/internal/service/post_service.go new file mode 100644 index 00000000..9b2704a5 --- /dev/null +++ b/exercise7/blogging-platform/internal/service/post_service.go @@ -0,0 +1,66 @@ +package service + +import ( + "errors" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/repository" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/models" +) + +type PostService struct { + repo *repository.PostRepository +} + +func NewPostService(repo *repository.PostRepository) *PostService { + return &PostService{repo: repo} +} + +// CreatePost — проверка данных и создание поста +func (s *PostService) CreatePost(post *models.Post) error { + if post.Title == "" || post.Content == "" || post.Category == "" { + return errors.New("all fields (title, content, category) are required") + } + return s.repo.Create(post) +} + +// GetAllPosts — получение всех постов +func (s *PostService) GetAllPosts() ([]models.Post, error) { + return s.repo.GetAll() +} + +// GetPostByID — получение поста по ID +func (s *PostService) GetPostByID(id int64) (*models.Post, error) { + post, err := s.repo.GetPostByID(int(id)) + if err != nil { + return nil, errors.New("the post was not found") + } + return post, nil +} + +// UpdatePost — обновление поста по ID +func (s *PostService) UpdatePost(post *models.Post) error { + if post.Title == "" || post.Content == "" || post.Category == "" { + return errors.New("all fields (title, content, category) are required") + } + + existingPost, err := s.repo.GetPostByID(int(post.ID)) + if err != nil { + return errors.New("the post was not found") + } + + existingPost.Title = post.Title + existingPost.Content = post.Content + existingPost.Category = post.Category + existingPost.Tags = post.Tags + + return s.repo.Update(existingPost) +} + +// DeletePost — удаление поста по ID +func (s *PostService) DeletePost(id int64) error { + _, err := s.repo.GetPostByID(int(id)) + if err != nil { + return errors.New("the post was not found") + } + return s.repo.Delete(int(id)) +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go deleted file mode 100644 index 1ffa1477..00000000 --- a/exercise7/blogging-platform/main.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "context" - "log/slog" - "os" - "os/signal" - - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api" - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - - // db - _, err := db.New() - if err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "db", - "error", err, - ) - panic(err) - } - - // api - a := api.New() - if err := a.Start(ctx); err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "api", - "error", err, - ) - panic(err) - } - - go func() { - shutdown := make(chan os.Signal, 1) // Create channel to signify s signal being sent - signal.Notify(shutdown, os.Interrupt) // When an interrupt is sent, notify the channel - - sig := <-shutdown - slog.WarnContext(ctx, "signal received - shutting down...", "signal", sig) - - cancel() - }() -} diff --git a/exercise7/blogging-platform/models/post.go b/exercise7/blogging-platform/models/post.go new file mode 100644 index 00000000..34a59a1b --- /dev/null +++ b/exercise7/blogging-platform/models/post.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type Post struct { + ID int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Category string `json:"category"` + Tags []string `json:"tags"` +}