From 5dd2909b84dc5cbe3c6cbeab1504bbb03a86714d Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Tue, 30 Jul 2024 15:42:37 +0600 Subject: [PATCH 01/30] =?UTF-8?q?=D0=9C=D0=B0=D0=B4=D0=B8=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=91=D0=B8=D0=BC=D0=B0=D0=BA=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=20?= =?UTF-8?q?add=20solutions=20for=20problems=201-5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- exercise1/problem1/main.go | 4 +++- exercise1/problem2/main.go | 16 +++++++++++++++- exercise1/problem3/main.go | 8 +++++++- exercise1/problem4/main.go | 12 +++++++++++- exercise1/problem5/main.go | 6 +++++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..b5911936 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,5 @@ package main -func addUp() {} +func addUp(n int) int { + return n * (n + 1) / 2 +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..2dffb851 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,17 @@ package main -func binary() {} +import "strconv" + +func binary(decNumber int) string { + if decNumber == 0 { + return "0" + } + binaryNumber := "" + for decNumber > 0 { + binaryNumber = strconv.Itoa(decNumber%2) + binaryNumber + decNumber = decNumber / 2 + } + + return binaryNumber + +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..97254203 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,9 @@ package main -func numberSquares() {} +func numberSquares(number int) int { + squares := 0 + for i := 1; i <= number; i++ { + squares += i * i + } + return squares +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..2c1d92df 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,13 @@ package main -func detectWord() {} +import "unicode" + +func detectWord(s string) string { + word := "" + for _, char := range s { + if unicode.IsLower(char) { + word += string(char) + } + } + return word +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..79358694 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,7 @@ package main -func potatoes() {} +import "strings" + +func potatoes(s string) int { + return strings.Count(s, "potato") +} From 7106bc9466989047e776be824446a604321ccf3e Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Tue, 30 Jul 2024 15:57:20 +0500 Subject: [PATCH 02/30] =?UTF-8?q?=D0=9C=D0=B0=D0=B4=D0=B8=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=91=D0=B8=D0=BC=D0=B0=D0=BA=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=20?= =?UTF-8?q?add=20solution=20for=20problem=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- exercise1/problem6/main.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..9a139dd6 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,11 @@ package main -func emojify() {} +import "strings" + +func emojify(s string) string{ + s = strings.ReplaceAll(s, "smile", "🙂") + s = strings.ReplaceAll(s, "grin", "😀") + s = strings.ReplaceAll(s, "sad", "😥") + s = strings.ReplaceAll(s, "mad", "😠") + return s +} From a42d395dc2be4a85e587d0c3228464e7a860bdc8 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Tue, 30 Jul 2024 16:16:58 +0600 Subject: [PATCH 03/30] =?UTF-8?q?=D0=9C=D0=B0=D0=B4=D0=B8=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=91=D0=B8=D0=BC=D0=B0=D0=BA=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=20?= =?UTF-8?q?add=20solutions=20for=20problems=207-10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- exercise1/problem10/main.go | 17 ++++++++++++++++- exercise1/problem7/main.go | 12 +++++++++++- exercise1/problem8/main.go | 13 ++++++++++++- exercise1/problem9/main.go | 12 +++++++++--- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..793b28cc 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,18 @@ package main -func sum() {} +import "strconv" + +func sum(s1, s2 string) (string, error) { + n1, err := strconv.Atoi(s1) + if err != nil { + return "", err + } + + n2, err := strconv.Atoi(s2) + if err != nil { + return "", err + } + + n3 := n1 + n2 + return strconv.Itoa(n3), nil +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..9f65f851 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,13 @@ package main -func highestDigit() {} +func highestDigit(number int) int { + maxDigit := 0 + for number != 0 { + digit := number % 10 + if digit > maxDigit { + maxDigit = digit + } + number = number / 10 + } + return maxDigit +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..2c29c2c2 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,14 @@ package main -func countVowels() {} +func countVowels(word string) int { + vowels := []string{"a", "e", "i", "o", "u"} + count := 0 + for _, i := range word { + for _, j := range vowels { + if string(i) == j { + count++ + } + } + } + return count +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..0f0adcc0 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,13 @@ package main -func bitwiseAND() {} +func bitwiseAND(n1, n2 int) int { + return n1 & n2 +} -func bitwiseOR() {} +func bitwiseOR(n1, n2 int) int { + return n1 | n2 +} -func bitwiseXOR() {} +func bitwiseXOR(n1, n2 int) int { + return n1 ^ n2 +} From ba4a0a920fd41d7b16b69d12018c473d74bf95d6 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 10:38:30 +0600 Subject: [PATCH 04/30] Madina Bimakanova add solution for exercise2/problem1 --- exercise2/problem1/problem1.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..a4b74ec9 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,17 @@ package problem1 -func isChangeEnough() { +func isChangeEnough(change [4]int, amount float32) bool { + var ( + is_pay bool = false + summa int = 0 + nominals = [4]int{25, 10, 5, 1} + ) + for i, v := range change { + summa += nominals[i] * v + } + amount_int := int(amount * 100) + if summa >= amount_int { + is_pay = true + } + return is_pay } From 4541d7f6b00940f6e8dd523ce7101af107645ff1 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 11:07:49 +0600 Subject: [PATCH 05/30] Madina Bimakanova add solution for exercise2/problem2 --- exercise2/problem2/problem2.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..da2e02f9 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,18 @@ package problem2 -func capitalize() { +func capitalize(names []string) []string { + var capitalizeNames []string + for _, name := range names { + var capitalizeName string + for i, char := range name { + if i == 0 && (char >= 'a' && char <= 'z') { + char -= 'a' - 'A' + } else if i != 0 && (char >= 'A' && char <= 'Z') { + char += 'a' - 'A' + } + capitalizeName += string(char) + } + capitalizeNames = append(capitalizeNames, capitalizeName) + } + return capitalizeNames } From 11d06cb950676f43474160c9cb43de2d235cc72a Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 11:12:49 +0600 Subject: [PATCH 06/30] Madina Bimakanova add solution for exercise2/problem4 --- exercise2/problem4/problem4.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..33cbeaa2 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,9 @@ package problem4 -func mapping() { +func mapping(masLetters []string) map[string]string { + mapLetters := make(map[string]string) + for _, v := range masLetters { + mapLetters[v] = string(v[0] - 32) + } + return mapLetters } From 06c8f21dbd04d43acfe20d4ebb5c1a72e7672f13 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 12:05:08 +0600 Subject: [PATCH 07/30] Madina Bimakanova add solution for exercise2/problem5 --- exercise2/problem5/problem5.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..86b77636 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,24 @@ package problem5 -func products() { +func products(pricesProducts map[string]int, minPrice int) []string { + var result []string + for i, v := range pricesProducts { + if v > minPrice { + result = append(result, i) + } + } + sortProducts(result, pricesProducts) + return result +} +func sortProducts(products []string, prices map[string]int) []string { + n := len(products) + for i := 0; i < n; i++ { + for j := 0; j < n-i-1; j++ { + if prices[products[j]] < prices[products[j+1]] || + (prices[products[j]] == prices[products[j+1]] && products[j] > products[j+1]) { + products[j], products[j+1] = products[j+1], products[j] + } + } + } + return products } From 126aceb298f858ec739b744ac04c4b609598293c Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 12:23:21 +0600 Subject: [PATCH 08/30] Madina Bimakanova add solution for exercise2/problem3 --- exercise2/problem3/problem3.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/exercise2/problem3/problem3.go b/exercise2/problem3/problem3.go index f183fafb..bd7b2acb 100644 --- a/exercise2/problem3/problem3.go +++ b/exercise2/problem3/problem3.go @@ -9,5 +9,21 @@ const ( lr dir = "lr" ) -func diagonalize() { +func diagonalize(number int, diagonal dir) [][]int { + matrix := make([][]int, number) + for i := 0; i < number; i++ { + matrix[i] = make([]int, number) + for j := 0; j < number; j++ { + if diagonal == ul { + matrix[i][j] = i + j + } else if diagonal == ur { + matrix[i][j] = number + i - j - 1 + } else if diagonal == ll { + matrix[i][j] = number - i + j - 1 + } else { + matrix[i][j] = 2*number - i - j - 2 + } + } + } + return matrix } From 08b6a6706e38d5937311894612a77a8f5502ddb5 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 12:29:29 +0600 Subject: [PATCH 09/30] Madina Bimakanova add solution for exercise2/problem7 --- exercise2/problem7/problem7.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..79f51068 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,5 @@ package problem7 -func swap() { +func swap(a, b *int) { + *a, *b = *b, *a } From 291220b112d1bd05bd2bb69b098363fe80289fab Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 12:37:25 +0600 Subject: [PATCH 10/30] Madina Bimakanova add solution for exercise2/problem8 --- exercise2/problem8/problem8.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/exercise2/problem8/problem8.go b/exercise2/problem8/problem8.go index 9389d3b0..bb214f21 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -1,16 +1,15 @@ package problem8 func simplify(list []string) map[string]int { - var indMap map[string]int + indMap := make(map[string]int) - indMap = make(map[string]int) - load(&indMap, &list) + load(indMap, list) return indMap } -func load(m *map[string]int, students *[]string) { - for i, name := range *students { - (*m)[name] = i +func load(m map[string]int, students []string) { + for i, name := range students { + m[name] = i } } From e1711a9b57de5b38aca29b11902afc03da4a3e98 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 12:46:51 +0600 Subject: [PATCH 11/30] Madina Bimakanova add solution for exercise2/problem9 --- exercise2/problem9/problem9.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..1c2f07ae 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,10 @@ package problem9 -func factory() {} +func factory(number int) func(...int) []int { + return func(mas ...int) []int { + for i := 0; i < len(mas); i++ { + mas[i] = mas[i] * number + } + return mas + } +} From 2900ae5952fa443e59c786befa71f8de696062f0 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 12:54:14 +0600 Subject: [PATCH 12/30] Madina Bimakanova add solution for exercise2/problem10 --- exercise2/problem10/problem10.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..4d7857f7 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,12 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(string) func(int)) { + brands := make(map[string]int) + makeBrand := func(brand string) func(int) { + brands[brand] = 0 + return func(count int) { + brands[brand] += count + } + } + return brands, makeBrand +} From 7514e5bb378d5af035f9880d302a8a9897b4516f Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 13:06:30 +0600 Subject: [PATCH 13/30] Madina Bimakanova add solution for exercise2/problem6 --- exercise2/problem6/problem6.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..b0cec5b6 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,14 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(slc1 []int, slc2 []int, number int) bool { + var is_sum bool + for _, v1 := range slc1 { + for _, v2 := range slc2 { + if (v1 + v2) == number { + is_sum = true + break + } + } + } + return is_sum } From 899c151ed6e744ce7bf2d2e66dded91982d7b12c Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 13:18:15 +0600 Subject: [PATCH 14/30] Madina Bimakanova add solution for exercise2/problem11 --- exercise2/problem11/problem11.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..995433fe 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,13 @@ package problem11 -func removeDups() {} +func removeDups[T comparable](slc []T) []T { + new_slc := make([]T, 0, len(slc)) + proverka_slc := make(map[any]struct{}) + for _, v := range slc { + if _, ok := proverka_slc[v]; !ok { + proverka_slc[v] = struct{}{} + new_slc = append(new_slc, v) + } + } + return new_slc +} From b99d2d6c70c3fc81893c5cf0fc708dbad7162637 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Wed, 25 Sep 2024 14:15:50 +0600 Subject: [PATCH 15/30] Madina Bimakanova add solution for exercise2/problem12 --- exercise2/problem12/problem12.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..eadf6fa0 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,33 @@ package problem11 -func keysAndValues() {} +import "fmt" + +func keysAndValues[K comparable, V any](map_keysAndValues map[K]V) ([]K, []V) { + mas_keys := make([]K, 0, len(map_keysAndValues)) + mas_values := make([]V, 0, len(map_keysAndValues)) + + for k, v := range map_keysAndValues { + mas_keys = append(mas_keys, k) + mas_values = append(mas_values, v) + } + + sort_keys(mas_keys) + + sortedValues := make([]V, len(mas_keys)) + for i, k := range mas_keys { + sortedValues[i] = map_keysAndValues[k] + } + + return mas_keys, sortedValues +} + +func sort_keys[K comparable](keys []K) { + n := len(keys) + for i := 0; i < n; i++ { + for j := 0; j < n-i-1; j++ { + if fmt.Sprintf("%v", keys[j]) > fmt.Sprintf("%v", keys[j+1]) { + keys[j], keys[j+1] = keys[j+1], keys[j] + } + } + } +} From 4bd7da48d0f51bf1c8b8f90e790347f1bdf8df09 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Fri, 4 Oct 2024 15:14:22 +0600 Subject: [PATCH 16/30] Madina Bimakanova add solution for exercise 3 problems 1-3 --- exercise3/problem1/problem1.go | 36 ++++++++++- exercise3/problem2/problem2.go | 34 +++++++++- exercise3/problem3/problem3.go | 115 ++++++++++++++++++++++++++++++++- 3 files changed, 182 insertions(+), 3 deletions(-) diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..04098942 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,37 @@ package problem1 -type Queue struct{} +import ( + "errors" +) + +type Queue struct { + elements []any +} + +func (q *Queue) Enqueue(value any) { + q.elements = append(q.elements, value) +} + +func (q *Queue) Dequeue() (any, error) { + if q.IsEmpty() { + return nil, errors.New("queue is empty") + } + value := q.elements[0] + q.elements = q.elements[1:] + return value, nil +} + +func (q *Queue) Peek() (any, 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) +} + +func (q *Queue) IsEmpty() bool { + return len(q.elements) == 0 +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..0723ccc3 100644 --- a/exercise3/problem2/problem2.go +++ b/exercise3/problem2/problem2.go @@ -1,3 +1,35 @@ package problem2 -type Stack struct{} +import "errors" + +type Stack struct { + elements []any +} + +func (s *Stack) Push(el any) { + s.elements = append(s.elements, el) +} + +func (s *Stack) Pop() (any, error) { + if s.IsEmpty() { + return nil, errors.New("stack is empty") + } + elements := s.elements[len(s.elements)-1] + s.elements = s.elements[:len(s.elements)-1] + return elements, nil +} + +func (s *Stack) Peek() (any, error) { + if s.IsEmpty() { + return nil, errors.New("stack is empty") + } + return s.elements[len(s.elements)-1], nil +} + +func (s *Stack) Size() int { + return len(s.elements) +} + +func (s *Stack) IsEmpty() bool { + return len(s.elements) == 0 +} diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..3e1b9e9f 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,116 @@ package problem3 -type Set struct{} +type Set struct { + elements []any +} + +func (s *Set) Has(el any) bool { + var isHasElement bool + for _, i := range s.elements { + if i == el { + isHasElement = true + } + } + return isHasElement +} + +func (s *Set) Add(el any) { + if !s.Has(el) { + s.elements = append(s.elements, el) + } +} + +func (s *Set) Remove(el any) { + var isHas bool + var num int + for i, v := range s.elements { + if v == el { + num = i + isHas = true + break + } + } + if isHas { + s.elements = append(s.elements[:num], s.elements[num+1:]...) + } +} + +func (s *Set) Size() int { + return len(s.elements) +} + +func (s *Set) IsEmpty() bool { + return len(s.elements) == 0 +} + +func (s *Set) List() []any { + return s.elements +} + +func (s *Set) Copy() *Set { + newSet := &Set{ + elements: make([]any, len(s.elements)), + } + copy(newSet.elements, s.elements) + return newSet +} + +func NewSet() *Set { + return &Set{ + elements: []any{}, + } +} + +func (s1 *Set) Difference(s2 *Set) *Set { + newSet := NewSet() + for _, el := range s1.elements { + if !s2.Has(el) { + newSet.Add(el) + } + } + return newSet +} + +func (s1 *Set) IsSubset(s2 *Set) bool { + for _, el := range s1.elements { + if !s2.Has(el) { + return false + } + } + return true +} + +func Union(s ...*Set) *Set { + newSet := NewSet() + for _, i := range s { + for _, j := range i.elements { + if !newSet.Has(j) { + newSet.Add(j) + } + } + } + return newSet +} + +func Intersect(sets ...*Set) *Set { + if len(sets) == 0 { + return NewSet() + } + newSet := NewSet() + for _, el := range sets[0].List() { + hasSet := NewSet() + hasSet.Add(el) + isHas := true + for _, set := range sets[1:] { + if hasSet.Difference(set).Size() != 0 { + isHas = false + break + } + } + if isHas { + newSet.Add(el) + } + } + + return newSet +} From 480ea03314da7b8d67c0ebd559b2404853a5d823 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sat, 5 Oct 2024 10:48:31 +0600 Subject: [PATCH 17/30] Madina Bimakanova add solutions for exercise 3 problems 4-7 --- exercise3/problem4/problem4.go | 126 ++++++++++++++++++++++++++++++++- exercise3/problem5/problem5.go | 16 ++++- exercise3/problem6/problem6.go | 31 +++++++- exercise3/problem7/problem7.go | 73 +++++++++++++++++++ 4 files changed, 241 insertions(+), 5 deletions(-) diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..0aa2420a 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,127 @@ package problem4 -type LinkedList struct{} +import ( + "errors" +) + +type Element[T comparable] struct { + value T + next *Element[T] +} + +type LinkedList[T comparable] struct { + head *Element[T] + size int +} + +func (l *LinkedList[T]) Add(el *Element[T]) { + if l.head == nil { + l.head = el + } else { + current := l.head + for current.next != nil { + current = current.next + } + current.next = el + } + l.size++ +} + +func (l *LinkedList[T]) Insert(el *Element[T], position int) error { + + if position < 0 || position > l.size { + return errors.New("position is out of range") + } + + if position == 0 { + el.next = l.head + l.head = el + l.size++ + return nil + } + + if position == l.size { + l.Add(el) + return nil + } + + current := l.head + count := 0 + for current != nil { + if count == position-1 { + el.next = current.next + current.next = el + l.size++ + return nil + } + current = current.next + count++ + } + return errors.New("element not found") +} + +func (l *LinkedList[T]) Delete(el *Element[T]) error { + if l.head == nil { + return errors.New("list is empty") + } + + if l.head.value == el.value { + l.head = l.head.next + l.size-- + return nil + } + + current := l.head + var prev *Element[T] + + for current != nil { + if current.value == el.value { + prev.next = current.next + l.size-- + return nil + } + prev = current + current = current.next + } + + return errors.New("element not found") +} + +func (l *LinkedList[T]) Find(el any) (*Element[T], error) { + + if l.head == nil { + return nil, errors.New("empty") + } + + current := l.head + + for current != nil { + + if current.value == el { + + return current, nil + } + + current = current.next + } + + return nil, errors.New("element not found") +} + +func (l *LinkedList[T]) List() []T { + list := make([]T, 0, l.size) + current := l.head + for current != nil { + list = append(list, current.value) + current = current.next + } + return list +} + +func (l *LinkedList[T]) Size() int { + return l.size +} + +func (l *LinkedList[T]) IsEmpty() bool { + return l.head == nil +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..953efab2 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,17 @@ package problem5 -type Person struct{} +type Person struct { + name string + age int +} + +func (person1 *Person) compareAge(person2 *Person) string { + var answer string + answer = person2.name + " is the same age as me." + if person1.age < person2.age { + answer = person2.name + " is older than me." + } else if person1.age > person2.age { + answer = person2.name + " is younger than me." + } + return answer +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..a0cd3d29 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,32 @@ package problem6 -type Animal struct{} +// Legged interface defines a method to get the number of legs +type Legs interface { + sumLegs() int +} -type Insect struct{} +type Animal struct { + name string + legsNum int +} -func sumOfAllLegsNum() {} +func (a *Animal) sumLegs() int { + return a.legsNum +} + +type Insect struct { + name string + legsNum int +} + +func (i *Insect) sumLegs() int { + return i.legsNum +} + +func sumOfAllLegsNum(l ...Legs) int { + sum := 0 + for _, i := range l { + sum += i.sumLegs() + } + return sum +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..93273dc3 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,83 @@ package problem7 +type Account interface { + nameAccount() string +} + +type WithdrawInterface interface { + Account + Withdraw(amount int) bool +} + +type SendPackageInterface interface { + Account + SendPackage(name string) string +} + type BankAccount struct { + name string + balance int +} + +func (b *BankAccount) nameAccount() string { + return b.name +} + +func (b *BankAccount) Withdraw(money int) bool { + if b.balance >= money { + b.balance -= money + return true + } + return false } type FedexAccount struct { + name string + packages []string +} + +func (f *FedexAccount) nameAccount() string { + return f.name +} + +func (f *FedexAccount) SendPackage(name string) string { + message := f.name + " send package to " + name + f.packages = append(f.packages, message) + return message } type KazPostAccount struct { + name string + balance int + packages []string +} + +func (k *KazPostAccount) nameAccount() string { + return k.name +} + +func (k *KazPostAccount) Withdraw(money int) bool { + if k.balance >= money { + k.balance -= money + return true + } + return false +} + +func (k *KazPostAccount) SendPackage(name string) string { + message := k.name + " send package to " + name + k.packages = append(k.packages, message) + return message +} + +func withdrawMoney(money int, accounts ...WithdrawInterface) { + for _, account := range accounts { + account.Withdraw(money) + } +} + +func sendPackagesTo(name string, accounts ...SendPackageInterface) { + for _, account := range accounts { + account.SendPackage(name) + } } From 0d1cea557f31ab6a55362024c09cb010dff638a3 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sun, 20 Oct 2024 14:24:52 +0600 Subject: [PATCH 18/30] Add pkg --- exercise4/bot/pkg/httputils/request/body.go | 76 +++++++++++++++++++ exercise4/bot/pkg/httputils/response/body.go | 33 ++++++++ .../bot/pkg/httputils/statusError/main.go | 18 +++++ exercise4/bot/pkg/logger/logger.go | 20 +++++ 4 files changed, 147 insertions(+) create mode 100644 exercise4/bot/pkg/httputils/request/body.go create mode 100644 exercise4/bot/pkg/httputils/response/body.go create mode 100644 exercise4/bot/pkg/httputils/statusError/main.go create mode 100644 exercise4/bot/pkg/logger/logger.go diff --git a/exercise4/bot/pkg/httputils/request/body.go b/exercise4/bot/pkg/httputils/request/body.go new file mode 100644 index 00000000..23af73f3 --- /dev/null +++ b/exercise4/bot/pkg/httputils/request/body.go @@ -0,0 +1,76 @@ +package request + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/pkg/httputils/statusError" +) + +func JSON(w http.ResponseWriter, r *http.Request, dst interface{}) error { + ct := r.Header.Get("Content-Type") + if ct != "" { + mediaType := strings.ToLower(strings.TrimSpace(strings.Split(ct, ";")[0])) + if mediaType != "application/json" { + msg := "Content-Type header is not application/json" + return statusError.New(http.StatusUnsupportedMediaType, msg) + } + } + + r.Body = http.MaxBytesReader(w, r.Body, 1048576) + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(&dst) + if err != nil { + var syntaxError *json.SyntaxError + var unmarshalTypeError *json.UnmarshalTypeError + + switch { + case errors.As(err, &syntaxError): + msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset) + return statusError.New(http.StatusBadRequest, msg) + + case errors.Is(err, io.ErrUnexpectedEOF): + msg := "Request body contains badly-formed JSON" + return statusError.New(http.StatusBadRequest, msg) + + case errors.As(err, &unmarshalTypeError): + msg := fmt.Sprintf( + "Request body contains an invalid value for the %q field (at position %d)", + unmarshalTypeError.Field, + unmarshalTypeError.Offset, + ) + return statusError.New(http.StatusBadRequest, msg) + + case strings.HasPrefix(err.Error(), "json: unknown field "): + fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") + msg := fmt.Sprintf("Request body contains unknown field %s", fieldName) + return statusError.New(http.StatusBadRequest, msg) + + case errors.Is(err, io.EOF): + msg := "Request body must not be empty" + return statusError.New(http.StatusBadRequest, msg) + + case err.Error() == "http: request body too large": + msg := "Request body must not be larger than 1MB" + return statusError.New(http.StatusRequestEntityTooLarge, msg) + + default: + return err + } + } + + err = dec.Decode(&struct{}{}) + if !errors.Is(err, io.EOF) { + msg := "Request body must only contain a single JSON object" + return statusError.New(http.StatusBadRequest, msg) + } + + return nil +} diff --git a/exercise4/bot/pkg/httputils/response/body.go b/exercise4/bot/pkg/httputils/response/body.go new file mode 100644 index 00000000..e1fd78a8 --- /dev/null +++ b/exercise4/bot/pkg/httputils/response/body.go @@ -0,0 +1,33 @@ +package response + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type DataResponse struct { + Data interface{} `json:"data"` +} + +func JSON(w http.ResponseWriter, status int, data interface{}) error { + if data == nil { + w.WriteHeader(http.StatusNoContent) + return nil + } + + js, err := json.Marshal(data) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return fmt.Errorf("JSON marshal error: %w", err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if _, err := w.Write(js); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return fmt.Errorf("writer error: %w", err) + } + + return nil +} diff --git a/exercise4/bot/pkg/httputils/statusError/main.go b/exercise4/bot/pkg/httputils/statusError/main.go new file mode 100644 index 00000000..6cf4e1b6 --- /dev/null +++ b/exercise4/bot/pkg/httputils/statusError/main.go @@ -0,0 +1,18 @@ +package statusError + +type StatusError struct { + status int + msg string +} + +func New(status int, msg string) error { + return &StatusError{status, msg} +} + +func (st *StatusError) Error() string { + return st.msg +} + +func (st *StatusError) Status() int { + return st.status +} diff --git a/exercise4/bot/pkg/logger/logger.go b/exercise4/bot/pkg/logger/logger.go new file mode 100644 index 00000000..d4631c3a --- /dev/null +++ b/exercise4/bot/pkg/logger/logger.go @@ -0,0 +1,20 @@ +package logger + +import ( + "log/slog" + "os" +) + +func New(isJson bool) *slog.Logger { + if isJson { + return slog.New(slog.NewJSONHandler(os.Stdout, nil)) + } + + return slog.New( + slog.NewTextHandler( + os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }, + ), + ) +} From 580e48d4c43c05b86510b90f00b83b99be7d18af Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sun, 20 Oct 2024 14:28:14 +0600 Subject: [PATCH 19/30] Add player --- exercise4/bot/tictacToe/player.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 exercise4/bot/tictacToe/player.go diff --git a/exercise4/bot/tictacToe/player.go b/exercise4/bot/tictacToe/player.go new file mode 100644 index 00000000..d3d07d40 --- /dev/null +++ b/exercise4/bot/tictacToe/player.go @@ -0,0 +1,23 @@ +package ticTacToe + +type Token string + +const ( + TokenEmpty Token = " " + TokenX Token = "x" + TokenO Token = "o" +) + +type Player struct { + Name string `json:"name"` + URL string `json:"url"` + Token Token `json:"token"` +} + +func New(name string, url string) *Player { + return &Player{ + Name: name, + URL: url, + Token: TokenEmpty, + } +} From 8892d27887f2c337e5e8c8bc72822a534e049238 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sun, 20 Oct 2024 14:30:06 +0600 Subject: [PATCH 20/30] Add handler --- exercise4/bot/handler/main.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 exercise4/bot/handler/main.go diff --git a/exercise4/bot/handler/main.go b/exercise4/bot/handler/main.go new file mode 100644 index 00000000..c01dbc93 --- /dev/null +++ b/exercise4/bot/handler/main.go @@ -0,0 +1,16 @@ +package handler + +import ( + "github.com/talgat-ruby/exercises-go/exercise4/bot/ticTacToe" +) + +type Handler struct { + Player *ticTacToe.Player +} + +func New(name, url string) *Handler { + player := ticTacToe.New(name, url) + return &Handler{ + Player: player, + } +} From e65cb351ce462b2fb6fb0b2345be530a29fc95b3 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sun, 20 Oct 2024 14:33:07 +0600 Subject: [PATCH 21/30] Add server --- exercise4/bot/server/server.go | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 exercise4/bot/server/server.go diff --git a/exercise4/bot/server/server.go b/exercise4/bot/server/server.go new file mode 100644 index 00000000..0eaacb13 --- /dev/null +++ b/exercise4/bot/server/server.go @@ -0,0 +1,72 @@ +package server + +import ( + "errors" + "fmt" + "log" + "net" + "net/http" + "os" + "sync" + "time" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/handler" +) + +type readyListener struct { + net.Listener + ready chan struct{} + once sync.Once +} + +func (l *readyListener) Accept() (net.Conn, error) { + l.once.Do(func() { close(l.ready) }) + return l.Listener.Accept() +} + +// Start the server and handle requests using http.NewServeMux() +func StartServer() (<-chan struct{}, *http.Server) { + ready := make(chan struct{}) + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + if port == "4444" { + log.Fatalf("Fatal error: Port %s is reserved for the game.", port) + } + + listener, err := net.Listen("tcp", fmt.Sprintf(":%s", os.Getenv("PORT"))) + if err != nil { + panic(err) + } + + list := &readyListener{Listener: listener, ready: ready} + srv := &http.Server{ + IdleTimeout: 2 * time.Minute, + Handler: New(), + } + + go func() { + err := srv.Serve(list) + if !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + }() + + return ready, srv +} + +func New() *http.ServeMux { + name := os.Getenv("NAME") + url := fmt.Sprintf("http://localhost:%s", os.Getenv("PORT")) + + han := handler.New(name, url) + mux := http.NewServeMux() + + mux.Handle("GET /ping", http.HandlerFunc(han.Ping)) + mux.Handle("POST /move", http.HandlerFunc(han.Move)) + + return mux +} From f162366af1f1ba3242e37bb1a1eb48cef213b3ad Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sun, 20 Oct 2024 14:37:54 +0600 Subject: [PATCH 22/30] Modified main.go and add joingame --- exercise4/bot/main.go | 30 +++++++++++++-- exercise4/bot/server.go | 45 ---------------------- exercise4/bot/tictacToe/joingame.go | 58 +++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 49 deletions(-) delete mode 100644 exercise4/bot/server.go create mode 100644 exercise4/bot/tictacToe/joingame.go diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..471328e1 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -2,20 +2,42 @@ package main import ( "context" + "fmt" + "log" "os" "os/signal" "syscall" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/server" + "github.com/talgat-ruby/exercises-go/exercise4/bot/ticTacToe" ) func main() { - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - ready := startServer() + ready, srv := server.StartServer() <-ready - // TODO after server start + name := os.Getenv("NAME") + if name == "" { + name = "Player" + } + + port := os.Getenv("PORT") + url := fmt.Sprintf("http://localhost:%s", port) + player := ticTacToe.New(name, url) + + if err := player.JoinGame(ctx); err != nil { + log.Fatalf("Failed to join the game: %v", err) + } stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - <-stop // Wait for SIGINT or SIGTERM + <-stop + + log.Println("Shutting down...") + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server shutdown failed: %v", err) + } } diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go deleted file mode 100644 index e6760ec5..00000000 --- a/exercise4/bot/server.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net" - "net/http" - "os" - "sync" - "time" -) - -type readyListener struct { - net.Listener - ready chan struct{} - once sync.Once -} - -func (l *readyListener) Accept() (net.Conn, error) { - l.once.Do(func() { close(l.ready) }) - return l.Listener.Accept() -} - -func startServer() <-chan struct{} { - ready := make(chan struct{}) - - listener, err := net.Listen("tcp", fmt.Sprintf(":%s", os.Getenv("PORT"))) - if err != nil { - panic(err) - } - - list := &readyListener{Listener: listener, ready: ready} - srv := &http.Server{ - IdleTimeout: 2 * time.Minute, - } - - go func() { - err := srv.Serve(list) - if !errors.Is(err, http.ErrServerClosed) { - panic(err) - } - }() - - return ready -} diff --git a/exercise4/bot/tictacToe/joingame.go b/exercise4/bot/tictacToe/joingame.go new file mode 100644 index 00000000..a6182ad6 --- /dev/null +++ b/exercise4/bot/tictacToe/joingame.go @@ -0,0 +1,58 @@ +package ticTacToe + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" +) + +type RequestJoin struct { + Name string `json:"name"` + URL string `json:"url"` +} + +func (p *Player) JoinGame(ctx context.Context) error { + + reqBody := RequestJoin{Name: p.Name, URL: p.URL} + + reqBodyBytes, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("failed to marshal join request: %w", err) + } + + client := &http.Client{} + + judgePort := os.Getenv("JUDGE") + if judgePort == "" { + return fmt.Errorf("JUDGE environment variable is not set") + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + fmt.Sprintf("http://localhost:%s/join", judgePort), + bytes.NewBuffer(reqBodyBytes)) + + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to join the game: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + log.Println("Successfully joined the game") + return nil +} From c37b11a7fca0638b27ba2b32371f0eb19fc52134 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sun, 20 Oct 2024 14:42:16 +0600 Subject: [PATCH 23/30] Add ping and move --- exercise4/bot/handler/move.go | 36 +++++++++++++++++++++++ exercise4/bot/handler/ping.go | 16 +++++++++++ exercise4/bot/tictacToe/game.go | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 exercise4/bot/handler/move.go create mode 100644 exercise4/bot/handler/ping.go create mode 100644 exercise4/bot/tictacToe/game.go diff --git a/exercise4/bot/handler/move.go b/exercise4/bot/handler/move.go new file mode 100644 index 00000000..a8f0fa36 --- /dev/null +++ b/exercise4/bot/handler/move.go @@ -0,0 +1,36 @@ +package handler + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise4/bot/ticTacToe" +) + +type RequestMove struct { + Board *ticTacToe.Board `json:"board"` + Token ticTacToe.Token `json:"token"` +} + +type ResponseMove struct { + Index int `json:"index"` +} + +func (h *Handler) Move(w http.ResponseWriter, r *http.Request) { + var req RequestMove + if err := request.JSON(w, r, &req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + position := ticTacToe.CalculateBestMove(req.Board, req.Token) + resp := ResponseMove{Index: position} + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(resp); err != nil { + http.Error(w, "failed to respond to move", http.StatusInternalServerError) + log.Printf("Failed to encode response: %v", err) + } +} diff --git a/exercise4/bot/handler/ping.go b/exercise4/bot/handler/ping.go new file mode 100644 index 00000000..a32da2ba --- /dev/null +++ b/exercise4/bot/handler/ping.go @@ -0,0 +1,16 @@ +package handler + +import ( + "log" + "net/http" +) + +func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) { + + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("pong")) + + if err != nil { + log.Printf("failed to write response: %v", err) + } +} \ No newline at end of file diff --git a/exercise4/bot/tictacToe/game.go b/exercise4/bot/tictacToe/game.go new file mode 100644 index 00000000..284c5180 --- /dev/null +++ b/exercise4/bot/tictacToe/game.go @@ -0,0 +1,51 @@ +package ticTacToe + +type State string + +const ( + StatePending State = "PENDING" + StateRunning State = "RUNNING" + StateFinished State = "FINISHED" +) + +type Game struct { + State State `json:"state"` + Players []*Player `json:"players"` + Matches []*Match `json:"matches"` +} + +type Match struct { + Players [2]*Player `json:"players"` + Rounds []*Round `json:"rounds"` +} + +type Round struct { + Players [2]*Player `json:"players"` + Board *Board `json:"board"` + Moves []*Move `json:"moves"` + Winner *Player `json:"winner"` +} + +type Move struct { + Player *Player `json:"players"` + Board *Board `json:"board"` +} + +const ( + Cols = 3 + Rows = 3 +) + +type Board [Cols * Rows]Token + +const ( + MinScore = -1_000_000 + MaxScore = 1_000_000 + WinScore = 10 + LoseScore = -10 +) + +// Вычисляет наилучший ход для текущего игрока +func CalculateBestMove(board *Board, player Token) int { + +} From 2afa7b747de4cbd777dc8675a1a43dd496f24591 Mon Sep 17 00:00:00 2001 From: akabdylk Date: Sun, 20 Oct 2024 14:45:27 +0600 Subject: [PATCH 24/30] Add logic for move --- exercise4/bot/tictacToe/game.go | 112 ++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/exercise4/bot/tictacToe/game.go b/exercise4/bot/tictacToe/game.go index 284c5180..322a8fd5 100644 --- a/exercise4/bot/tictacToe/game.go +++ b/exercise4/bot/tictacToe/game.go @@ -47,5 +47,117 @@ const ( // Вычисляет наилучший ход для текущего игрока func CalculateBestMove(board *Board, player Token) int { + bestScore := MinScore + bestMove := -1 + for i := 0; i < len(*board); i++ { + if (*board)[i] == TokenEmpty { + (*board)[i] = player + score := minimax(board, 0, false, player, MinScore, MaxScore) + (*board)[i] = TokenEmpty + + if score > bestScore { + bestScore = score + bestMove = i + } + } + } + return bestMove +} + +// Алгоритм Minimax с альфа-бета отсечением +func minimax(board *Board, depth int, isMaximizing bool, player Token, alpha, beta int) int { + winner := checkWinner(board) + if winner == player { + return WinScore - depth + } else if winner != TokenEmpty { + return LoseScore + depth + } else if isBoardFull(board) { + return 0 + } + + opponent := switchPlayer(player) + + if isMaximizing { + maxEval := MinScore + for i := 0; i < len(*board); i++ { + if (*board)[i] == TokenEmpty { + (*board)[i] = player + eval := minimax(board, depth+1, false, player, alpha, beta) + (*board)[i] = TokenEmpty + maxEval = max(maxEval, eval) + alpha = max(alpha, eval) + if beta <= alpha { + break + } + } + } + return maxEval + } else { + minEval := MaxScore + for i := 0; i < len(*board); i++ { + if (*board)[i] == TokenEmpty { + (*board)[i] = opponent + eval := minimax(board, depth+1, true, player, alpha, beta) + (*board)[i] = TokenEmpty + minEval = min(minEval, eval) + beta = min(beta, eval) + if beta <= alpha { + break + } + } + } + return minEval + } +} + +// Проверка победителя на доске +func checkWinner(board *Board) Token { + winningCombinations := [8][3]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 _, combo := range winningCombinations { + if (*board)[combo[0]] != TokenEmpty && + (*board)[combo[0]] == (*board)[combo[1]] && + (*board)[combo[1]] == (*board)[combo[2]] { + return (*board)[combo[0]] + } + } + return TokenEmpty +} + +// Проверка на заполненность доски +func isBoardFull(board *Board) bool { + for _, cell := range *board { + if cell == TokenEmpty { + return false + } + } + return true +} + +// Переключение текущего игрока +func switchPlayer(player Token) Token { + if player == TokenX { + return TokenO + } + return TokenX +} + +// Вспомогательные функции для нахождения максимума и минимума +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b } From 09d5d695a0a7264ad75b4f81e204acb48f9ab3d0 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sun, 20 Oct 2024 16:41:29 +0500 Subject: [PATCH 25/30] Add same change --- exercise4/bot/{tictacToe => ticTacToe}/game.go | 0 exercise4/bot/{tictacToe => ticTacToe}/joingame.go | 0 exercise4/bot/{tictacToe => ticTacToe}/player.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename exercise4/bot/{tictacToe => ticTacToe}/game.go (100%) rename exercise4/bot/{tictacToe => ticTacToe}/joingame.go (100%) rename exercise4/bot/{tictacToe => ticTacToe}/player.go (100%) diff --git a/exercise4/bot/tictacToe/game.go b/exercise4/bot/ticTacToe/game.go similarity index 100% rename from exercise4/bot/tictacToe/game.go rename to exercise4/bot/ticTacToe/game.go diff --git a/exercise4/bot/tictacToe/joingame.go b/exercise4/bot/ticTacToe/joingame.go similarity index 100% rename from exercise4/bot/tictacToe/joingame.go rename to exercise4/bot/ticTacToe/joingame.go diff --git a/exercise4/bot/tictacToe/player.go b/exercise4/bot/ticTacToe/player.go similarity index 100% rename from exercise4/bot/tictacToe/player.go rename to exercise4/bot/ticTacToe/player.go From 437d76f086c2c3d1e594b763629098dc97d78d68 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Mon, 21 Oct 2024 16:42:25 +0500 Subject: [PATCH 26/30] Change port for judge in joingame.go --- exercise4/bot/ticTacToe/joingame.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise4/bot/ticTacToe/joingame.go b/exercise4/bot/ticTacToe/joingame.go index a6182ad6..9e5bc8dd 100644 --- a/exercise4/bot/ticTacToe/joingame.go +++ b/exercise4/bot/ticTacToe/joingame.go @@ -28,7 +28,7 @@ func (p *Player) JoinGame(ctx context.Context) error { judgePort := os.Getenv("JUDGE") if judgePort == "" { - return fmt.Errorf("JUDGE environment variable is not set") + judgePort = "4444" } req, err := http.NewRequestWithContext( From 4f7efb7d12afd2f203b6b21a30ad02a10f5906cc Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Sat, 2 Nov 2024 11:30:31 +0600 Subject: [PATCH 27/30] Madina Bimakanova add all solutions for exercise 5 --- exercise5/problem1/problem1.go | 7 ++++++- exercise5/problem2/problem2.go | 32 ++++++++++++++++++++++++++++++++ exercise5/problem3/problem3.go | 8 +++----- exercise5/problem4/problem4.go | 1 + exercise5/problem5/problem5.go | 19 +++++++++++++++++-- exercise5/problem6/problem6.go | 31 ++++++++++++++++++++++++++++--- exercise5/problem7/problem7.go | 34 +++++++++++++++++++++++++++++++++- exercise5/problem8/problem8.go | 12 +++++++++++- 8 files changed, 131 insertions(+), 13 deletions(-) diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..5ceb3f21 100644 --- a/exercise5/problem1/problem1.go +++ b/exercise5/problem1/problem1.go @@ -1,9 +1,14 @@ 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..604564b6 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 @@ -10,7 +15,34 @@ func add(numbers []int) int64 { } func addConcurrently(numbers []int) int64 { + numCPU := runtime.NumCPU() + countCPU := len(numbers) / numCPU + var wg sync.WaitGroup + var mu sync.Mutex var sum int64 + for i := 0; i < numCPU; i++ { + start := i * countCPU + end := start + countCPU + if i == numCPU-1 { + end = len(numbers) + } + + wg.Add(1) + go addSum(numbers[start:end], &wg, &mu, &sum) + } + + wg.Wait() return sum } + +func addSum(nums []int, wg *sync.WaitGroup, mu *sync.Mutex, sum *int64) { + defer wg.Done() + var s int64 + for _, n := range nums { + s += int64(n) + } + mu.Lock() + *sum += s + mu.Unlock() +} diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..71b0e280 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -1,11 +1,9 @@ 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..958b793e 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 } + defer close(ch) } func sum(nums []int) int { diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..a619efad 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,8 +1,23 @@ package problem5 -func producer() {} +import "strings" -func consumer() {} +func producer(str []string, ch chan<- string) { + for _, s := range str { + ch <- s + } + defer close(ch) +} + +func consumer(ch <-chan string) string { + var str []string + + for s := range ch { + str = append(str, s) + } + + return strings.Join(str, " ") +} func send( words []string, diff --git a/exercise5/problem6/problem6.go b/exercise5/problem6/problem6.go index e1beea87..23f07501 100644 --- a/exercise5/problem6/problem6.go +++ b/exercise5/problem6/problem6.go @@ -2,8 +2,33 @@ package problem6 type pipe func(in <-chan int) <-chan int -var multiplyBy2 pipe = func() {} +var multiplyBy2 pipe = func(in <-chan int) <-chan int { + ot := make(chan int) + go func() { + for i := range in { + ot <- i * 2 + } + defer close(ot) + }() + return ot +} -var add5 pipe = func() {} +var add5 pipe = func(in <-chan int) <-chan int { + ot := make(chan int) + go func() { + for i := range in { + ot <- i + 5 + } + defer close(ot) + }() + return ot +} -func piper(in <-chan int, pipes []pipe) <-chan int {} +func piper(in <-chan int, pipes []pipe) <-chan int { + var ot <-chan int + for _, pipe := range pipes { + ot = pipe(in) + in = ot + } + return ot +} diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..65958711 100644 --- a/exercise5/problem7/problem7.go +++ b/exercise5/problem7/problem7.go @@ -1,3 +1,35 @@ package problem7 -func multiplex(ch1 <-chan string, ch2 <-chan string) []string {} +func multiplex(ch1 <-chan string, ch2 <-chan string) []string { + ot := []string{} + str := make(chan string) + go func() { + for { + select { + case val, ok := <-ch1: + if ok { + str <- val + } else { + ch1 = nil + } + case val, ok := <-ch2: + if ok { + str <- val + } else { + ch2 = nil + } + } + + if ch1 == nil && ch2 == nil { + break + } + } + close(str) + }() + + for val := range str { + ot = append(ot, val) + } + + return ot +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..2341095d 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -4,4 +4,14 @@ import ( "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + select { + case msg, ok := <-ch: + if !ok { + return "fail" + } + return msg + case <-time.After(ttl): + return "timeout" + } +} From e83d5579b36b36ce5830f3e026105546cefdeb49 Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Mon, 18 Nov 2024 10:04:59 +0600 Subject: [PATCH 28/30] Madina Bimakanova add all solutions for exercise 6 --- exercise6/problem1/problem1.go | 21 ++++++++++++++++++++- exercise6/problem2/problem2.go | 28 +++++++++++++++++++++++++--- exercise6/problem3/problem3.go | 14 ++++++++++++++ exercise6/problem4/problem4.go | 27 ++++++++++++++++++--------- exercise6/problem5/problem5.go | 20 ++++++++++++++++---- exercise6/problem6/problem6.go | 10 ++++++++-- exercise6/problem7/problem7.go | 19 ++++++++++--------- exercise6/problem8/problem8.go | 26 +++++++++++++++++++++++++- 8 files changed, 136 insertions(+), 29 deletions(-) diff --git a/exercise6/problem1/problem1.go b/exercise6/problem1/problem1.go index ee453b24..8c2f57f6 100644 --- a/exercise6/problem1/problem1.go +++ b/exercise6/problem1/problem1.go @@ -1,9 +1,28 @@ 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 (acc *bankAccount) withdraw(money int) bool { + acc.mu.Lock() + defer acc.mu.Unlock() + if acc.blnc < money { + return false + } + acc.blnc -= money + return true +} + +func (acc *bankAccount) deposit(money int) { + acc.mu.Lock() + defer acc.mu.Unlock() + acc.blnc += money } diff --git a/exercise6/problem2/problem2.go b/exercise6/problem2/problem2.go index 97e02368..a753f325 100644 --- a/exercise6/problem2/problem2.go +++ b/exercise6/problem2/problem2.go @@ -1,6 +1,7 @@ package problem2 import ( + "sync" "time" ) @@ -8,13 +9,34 @@ 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 { - time.Sleep(readDelay) - return 0 + if readDelay > 0 { + time.Sleep(readDelay) + } + b.mu.Lock() + defer b.mu.Unlock() + return b.blnc +} + +func (acc *bankAccount) withdraw(money int) bool { + acc.mu.Lock() + defer acc.mu.Unlock() + if acc.blnc < money { + return false + } + acc.blnc -= money + return true +} + +func (acc *bankAccount) deposit(money int) { + acc.mu.Lock() + defer acc.mu.Unlock() + acc.blnc += money } diff --git a/exercise6/problem3/problem3.go b/exercise6/problem3/problem3.go index b34b90bb..acb946e2 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) inc() { + atomic.AddInt64(&c.val, 1) +} + +func (c *counter) dec() { + 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..3b033700 100644 --- a/exercise6/problem4/problem4.go +++ b/exercise6/problem4/problem4.go @@ -1,31 +1,40 @@ package problem4 import ( + "sync" "time" ) -func worker(id int, _ *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +func worker(id int, shoppingList *[]string, c *sync.Cond, ch chan<- int) { + c.L.Lock() + defer c.L.Unlock() + for len(*shoppingList) == 0 { + c.Wait() + } + ch <- id } - -func updateShopList(shoppingList *[]string) { +func updateShopList(shoppingList *[]string, c *sync.Cond) { time.Sleep(10 * time.Millisecond) - + c.L.Lock() + defer c.L.Unlock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + + c.Signal() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - + m := sync.Mutex{} + cond := sync.NewCond(&m) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) - time.Sleep(time.Millisecond) // order matters + go worker(i+1, shoppingList, cond, notifier) + time.Sleep(time.Millisecond) } - go updateShopList(shoppingList) + go updateShopList(shoppingList, cond) return notifier } diff --git a/exercise6/problem5/problem5.go b/exercise6/problem5/problem5.go index 8e4a1703..7082f9f6 100644 --- a/exercise6/problem5/problem5.go +++ b/exercise6/problem5/problem5.go @@ -1,28 +1,40 @@ package problem5 import ( + "sync" "time" ) -func worker(id int, shoppingList *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +var cond = sync.NewCond(&sync.Mutex{}) +var isFilled bool + +func worker(id int, _ *[]string, ch chan<- int) { + cond.L.Lock() + for !isFilled { + cond.Wait() + } ch <- id + cond.L.Unlock() } func updateShopList(shoppingList *[]string) { time.Sleep(10 * time.Millisecond) + cond.L.Lock() + defer cond.L.Unlock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + isFilled = true + cond.Broadcast() } 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/problem6/problem6.go b/exercise6/problem6/problem6.go index 0c1122b9..1e29e6d0 100644 --- a/exercise6/problem6/problem6.go +++ b/exercise6/problem6/problem6.go @@ -6,14 +6,20 @@ import ( func runTasks(init func()) { var wg sync.WaitGroup + var mu sync.Mutex + var isInit bool for range 10 { wg.Add(1) go func() { defer wg.Done() - //TODO: modify so that load function gets called only once. - init() + mu.Lock() + if !isInit { + init() + isInit = true + } + mu.Unlock() }() } wg.Wait() diff --git a/exercise6/problem7/problem7.go b/exercise6/problem7/problem7.go index ef49497b..881d86ef 100644 --- a/exercise6/problem7/problem7.go +++ b/exercise6/problem7/problem7.go @@ -3,20 +3,21 @@ package problem7 import ( "fmt" "math/rand" + "sync" "time" ) +var once sync.Once + func task() { start := time.Now() - var t *time.Timer - t = time.AfterFunc( - randomDuration(), - func() { - fmt.Println(time.Now().Sub(start)) - t.Reset(randomDuration()) - }, - ) - time.Sleep(5 * time.Second) + + once.Do(func() { + t := time.AfterFunc(randomDuration(), func() { + fmt.Println("Duration:", time.Now().Sub(start)) + }) + t.Reset(randomDuration()) + }) } func randomDuration() time.Duration { diff --git a/exercise6/problem8/problem8.go b/exercise6/problem8/problem8.go index 949eb2d2..49e54e01 100644 --- a/exercise6/problem8/problem8.go +++ b/exercise6/problem8/problem8.go @@ -1,3 +1,27 @@ package problem8 -func multiplex(chs []<-chan string) []string {} +func multiplex(chs []<-chan string) []string { + var slc []string + ch1 := make(chan string) + ch2 := make(chan struct{}, len(chs)) + for _, ch := range chs { + go func(ch <-chan string) { + for val := range ch { + ch1 <- val + } + ch2 <- struct{}{} + }(ch) + } + + go func() { + for i := 0; i < len(chs); i++ { + <-ch2 + } + close(ch1) + }() + for val := range ch1 { + slc = append(slc, val) + } + + return slc +} From 8405b1ba2d03e4b67a19ef545bb92857b8544bfd Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Thu, 5 Dec 2024 10:42:46 +0600 Subject: [PATCH 29/30] Madina Bimakanova add solution for exercise 7 --- exercise7/blogging-platform/.env | 8 + exercise7/blogging-platform/Dockerfile | 19 ++ .../blogging-platform/docker-compose.yml | 32 +++ exercise7/blogging-platform/go.mod | 5 +- exercise7/blogging-platform/go.sum | 2 + .../api/handler/categories/all_categories.go | 29 +++ .../categories/all_posts_of_category.go | 46 +++++ .../api/handler/categories/delete_category.go | 35 ++++ .../api/handler/categories/info_category.go | 39 ++++ .../api/handler/categories/insert_category.go | 44 ++++ .../internal/api/handler/categories/main.go | 19 ++ .../api/handler/categories/update_category.go | 52 +++++ .../internal/api/handler/main.go | 24 +++ .../internal/api/handler/ping.go | 16 ++ .../internal/api/handler/posts/all_posts.go | 55 +++++ .../internal/api/handler/posts/delete_post.go | 35 ++++ .../internal/api/handler/posts/info_post.go | 39 ++++ .../internal/api/handler/posts/insert_post.go | 41 ++++ .../internal/api/handler/posts/main.go | 19 ++ .../internal/api/handler/posts/update_post.go | 56 ++++++ .../api/handler/tags/all_posts of tags.go | 46 +++++ .../internal/api/handler/tags/all_tags.go | 35 ++++ .../internal/api/handler/tags/main.go | 19 ++ .../blogging-platform/internal/api/main.go | 73 +++++++ .../internal/api/router/categories.go | 15 ++ .../internal/api/router/posts.go | 13 ++ .../internal/api/router/router.go | 30 +++ .../internal/api/router/server.go | 9 + .../internal/api/router/tags.go | 10 + .../internal/db/blog/main.go | 103 ++++++++++ .../internal/db/category/all_categories.go | 38 ++++ .../db/category/all_posts_of_category.go | 108 ++++++++++ .../internal/db/category/delete_category.go | 32 +++ .../internal/db/category/info_category.go | 27 +++ .../internal/db/category/insert_category.go | 22 ++ .../internal/db/category/main.go | 18 ++ .../internal/db/category/update_category.go | 32 +++ .../blogging-platform/internal/db/init.go | 108 ++++++++++ .../blogging-platform/internal/db/main.go | 73 +++++++ .../internal/db/post/all_posts.go | 107 ++++++++++ .../internal/db/post/delete_post.go | 32 +++ .../internal/db/post/info_post.go | 69 +++++++ .../internal/db/post/insert_post.go | 157 +++++++++++++++ .../internal/db/post/main.go | 18 ++ .../internal/db/post/search_posts.go | 119 +++++++++++ .../internal/db/post/update_post.go | 188 ++++++++++++++++++ .../internal/db/tag/all_posts_of_tag.go | 113 +++++++++++ .../internal/db/tag/all_tags.go | 38 ++++ .../blogging-platform/internal/db/tag/main.go | 18 ++ exercise7/blogging-platform/main.go | 24 ++- .../pkg/httputils/request/body.go | 4 +- 51 files changed, 2306 insertions(+), 7 deletions(-) create mode 100644 exercise7/blogging-platform/.env create mode 100644 exercise7/blogging-platform/Dockerfile create mode 100644 exercise7/blogging-platform/docker-compose.yml create mode 100644 exercise7/blogging-platform/internal/api/handler/categories/all_categories.go create mode 100644 exercise7/blogging-platform/internal/api/handler/categories/all_posts_of_category.go create mode 100644 exercise7/blogging-platform/internal/api/handler/categories/delete_category.go create mode 100644 exercise7/blogging-platform/internal/api/handler/categories/info_category.go create mode 100644 exercise7/blogging-platform/internal/api/handler/categories/insert_category.go create mode 100644 exercise7/blogging-platform/internal/api/handler/categories/main.go create mode 100644 exercise7/blogging-platform/internal/api/handler/categories/update_category.go create mode 100644 exercise7/blogging-platform/internal/api/handler/main.go create mode 100644 exercise7/blogging-platform/internal/api/handler/ping.go create mode 100644 exercise7/blogging-platform/internal/api/handler/posts/all_posts.go create mode 100644 exercise7/blogging-platform/internal/api/handler/posts/delete_post.go create mode 100644 exercise7/blogging-platform/internal/api/handler/posts/info_post.go create mode 100644 exercise7/blogging-platform/internal/api/handler/posts/insert_post.go create mode 100644 exercise7/blogging-platform/internal/api/handler/posts/main.go create mode 100644 exercise7/blogging-platform/internal/api/handler/posts/update_post.go create mode 100644 exercise7/blogging-platform/internal/api/handler/tags/all_posts of tags.go create mode 100644 exercise7/blogging-platform/internal/api/handler/tags/all_tags.go create mode 100644 exercise7/blogging-platform/internal/api/handler/tags/main.go create mode 100644 exercise7/blogging-platform/internal/api/main.go create mode 100644 exercise7/blogging-platform/internal/api/router/categories.go create mode 100644 exercise7/blogging-platform/internal/api/router/posts.go create mode 100644 exercise7/blogging-platform/internal/api/router/router.go create mode 100644 exercise7/blogging-platform/internal/api/router/server.go create mode 100644 exercise7/blogging-platform/internal/api/router/tags.go create mode 100644 exercise7/blogging-platform/internal/db/blog/main.go create mode 100644 exercise7/blogging-platform/internal/db/category/all_categories.go create mode 100644 exercise7/blogging-platform/internal/db/category/all_posts_of_category.go create mode 100644 exercise7/blogging-platform/internal/db/category/delete_category.go create mode 100644 exercise7/blogging-platform/internal/db/category/info_category.go create mode 100644 exercise7/blogging-platform/internal/db/category/insert_category.go create mode 100644 exercise7/blogging-platform/internal/db/category/main.go create mode 100644 exercise7/blogging-platform/internal/db/category/update_category.go create mode 100644 exercise7/blogging-platform/internal/db/init.go create mode 100644 exercise7/blogging-platform/internal/db/main.go create mode 100644 exercise7/blogging-platform/internal/db/post/all_posts.go create mode 100644 exercise7/blogging-platform/internal/db/post/delete_post.go create mode 100644 exercise7/blogging-platform/internal/db/post/info_post.go create mode 100644 exercise7/blogging-platform/internal/db/post/insert_post.go create mode 100644 exercise7/blogging-platform/internal/db/post/main.go create mode 100644 exercise7/blogging-platform/internal/db/post/search_posts.go create mode 100644 exercise7/blogging-platform/internal/db/post/update_post.go create mode 100644 exercise7/blogging-platform/internal/db/tag/all_posts_of_tag.go create mode 100644 exercise7/blogging-platform/internal/db/tag/all_tags.go create mode 100644 exercise7/blogging-platform/internal/db/tag/main.go diff --git a/exercise7/blogging-platform/.env b/exercise7/blogging-platform/.env new file mode 100644 index 00000000..f89c0b3d --- /dev/null +++ b/exercise7/blogging-platform/.env @@ -0,0 +1,8 @@ +API_HOST=localhost +API_PORT=4010 + +DB_NAME=blog +DB_USER=postgres +DB_PASSWORD=postgres +DB_PORT=5432 +DB_HOST=192.168.99.100 \ No newline at end of file diff --git a/exercise7/blogging-platform/Dockerfile b/exercise7/blogging-platform/Dockerfile new file mode 100644 index 00000000..54b85cf9 --- /dev/null +++ b/exercise7/blogging-platform/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.23-alpine as builder + +WORKDIR /app + +COPY . . + +RUN go mod tidy + +RUN go build -o myapp . + +FROM alpine:latest + +RUN apk add --no-cache libpq + +COPY --from=builder /app/myapp /app/myapp + +ENTRYPOINT ["/app/myapp"] + +EXPOSE 8080 \ No newline at end of file diff --git a/exercise7/blogging-platform/docker-compose.yml b/exercise7/blogging-platform/docker-compose.yml new file mode 100644 index 00000000..82bc18c4 --- /dev/null +++ b/exercise7/blogging-platform/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3' + +services: + app: + build: . + ports: + - ${API_PORT}:8080 + environment: + - DB_HOST=db + - DB_PORT=${DB_PORT} + - DB_USER=${DB_USER} + - DB_PASSWORD=${DB_PASSWORD} + - DB_NAME=${DB_NAME} + - API_PORT=8080 + depends_on: + - db + + db: + image: postgres:17-alpine + environment: + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME} + ports: + - ${DB_PORT}:5432 + volumes: + - pg_data:/var/lib/postgresql/data + +volumes: + pg_data: + driver: local + diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..7cd5a99e 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -2,4 +2,7 @@ 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/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 +) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..ecb9035f 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,2 +1,4 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/exercise7/blogging-platform/internal/api/handler/categories/all_categories.go b/exercise7/blogging-platform/internal/api/handler/categories/all_categories.go new file mode 100644 index 00000000..0360f297 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/categories/all_categories.go @@ -0,0 +1,29 @@ +package categories + +import ( + "fmt" + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (c *Categories) GetAllCategories(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := c.logger.With("method", "GetAllCategories") + + categories, err := c.db.GetAllCategories(ctx) + if err != nil { + log.ErrorContext(ctx, "failed to get categories", "error", err) + http.Error(w, fmt.Sprintf("Failed to get categories: %s", err.Error()), http.StatusInternalServerError) + return + } + + if err := response.JSON(w, http.StatusOK, categories); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext(ctx, "success get all categories", "number of categories", len(categories)) + +} diff --git a/exercise7/blogging-platform/internal/api/handler/categories/all_posts_of_category.go b/exercise7/blogging-platform/internal/api/handler/categories/all_posts_of_category.go new file mode 100644 index 00000000..d84088ff --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/categories/all_posts_of_category.go @@ -0,0 +1,46 @@ +package categories + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (c *Categories) GetAllPostsOfCategory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := c.logger.With("method", "GetAllPostsOfCategory") + + idString := r.URL.Path[len("/categories/"):] + idString = idString[:len(idString)-6] + id, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid category ID", "error", err) + http.Error(w, "Invalid category ID", http.StatusBadRequest) + return + } + posts, err := c.db.GetAllPostsOfCategory(ctx, id) + if err != nil { + log.ErrorContext(ctx, "failed to get posts by category", "error", err) + http.Error(w, fmt.Sprintf("Failed to get posts: %s", err.Error()), http.StatusInternalServerError) + return + } + + if len(posts) == 0 { + log.InfoContext(ctx, "no posts found for category", "category_id", id) + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + + if err := response.JSON(w, http.StatusOK, posts); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, "success get all posts of category", + "category_id", id, + "number of posts", len(posts)) +} diff --git a/exercise7/blogging-platform/internal/api/handler/categories/delete_category.go b/exercise7/blogging-platform/internal/api/handler/categories/delete_category.go new file mode 100644 index 00000000..0d49517d --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/categories/delete_category.go @@ -0,0 +1,35 @@ +package categories + +import ( + "fmt" + "net/http" + "strconv" +) + +func (c *Categories) DeleteCategory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := c.logger.With("method", "DeleteCategory") + + idString := r.URL.Path[len("/categories/"):] + id, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid category ID", "error", err) + http.Error(w, "Invalid category ID", http.StatusBadRequest) + return + } + + err = c.db.DeleteCategory(ctx, id) + if err != nil { + if err.Error() == fmt.Sprintf("category with id %d not found", id) { + log.ErrorContext(ctx, "category not found", "category_id", id) + http.Error(w, "404 Not Found", http.StatusNotFound) + } else { + log.ErrorContext(ctx, "failed to delete category", "error", err) + http.Error(w, fmt.Sprintf("Failed to delete category: %s", err.Error()), http.StatusInternalServerError) + } + return + } + + log.InfoContext(ctx, "success delete category", "category_id", id) + w.WriteHeader(http.StatusNoContent) +} diff --git a/exercise7/blogging-platform/internal/api/handler/categories/info_category.go b/exercise7/blogging-platform/internal/api/handler/categories/info_category.go new file mode 100644 index 00000000..834f339b --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/categories/info_category.go @@ -0,0 +1,39 @@ +package categories + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (c *Categories) GetInformationOfCategory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := c.logger.With("method", "GetInformationOfCategory") + + idString := r.URL.Path[len("/categories/"):] + id, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid category ID", "error", err) + http.Error(w, "Invalid category ID", http.StatusBadRequest) + return + } + + category, err := c.db.GetInformationOfCategory(ctx, id) + if err != nil || category == nil { + log.ErrorContext(ctx, "category not found", "category_id", id) + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + + if err := response.JSON(w, http.StatusOK, category); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, "success get information of category", + "category_id", id) +} diff --git a/exercise7/blogging-platform/internal/api/handler/categories/insert_category.go b/exercise7/blogging-platform/internal/api/handler/categories/insert_category.go new file mode 100644 index 00000000..2c85fbce --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/categories/insert_category.go @@ -0,0 +1,44 @@ +package categories + +import ( + "fmt" + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (c *Categories) InsertCategory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := c.logger.With("method", "InsertCategory") + + //w.Header().Set("Content-Type", "application/json") + + var cat blog.CategoryRequest + if err := request.JSON(w, r, &cat); err != nil { + log.ErrorContext(ctx, "invalid request body", "error", err) + http.Error(w, fmt.Sprintf("Invalid request body: %v", err), http.StatusBadRequest) + return + } + + category, err := c.db.InsertCategory(ctx, cat) + if err != nil { + log.ErrorContext(ctx, "failed to create category", "error", err) + http.Error(w, fmt.Sprintf("Failed to create category: %v", err), http.StatusInternalServerError) + return + } + + if err := response.JSON(w, http.StatusCreated, category); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, + "success create category", + "category", category.String(), + ) + +} diff --git a/exercise7/blogging-platform/internal/api/handler/categories/main.go b/exercise7/blogging-platform/internal/api/handler/categories/main.go new file mode 100644 index 00000000..64e039c4 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/categories/main.go @@ -0,0 +1,19 @@ +package categories + +import ( + "log/slog" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" +) + +type Categories struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Categories { + return &Categories{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/categories/update_category.go b/exercise7/blogging-platform/internal/api/handler/categories/update_category.go new file mode 100644 index 00000000..1bfcff88 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/categories/update_category.go @@ -0,0 +1,52 @@ +package categories + +import ( + "database/sql" + "fmt" + "net/http" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (c *Categories) UpdateCategory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := c.logger.With("method", "UpdateCategory") + + idString := r.URL.Path[len("/categories/"):] + id, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid category ID", "error", err) + http.Error(w, "Invalid category ID", http.StatusBadRequest) + return + } + + var cat blog.CategoryRequest + if err := request.JSON(w, r, &cat); err != nil { + log.ErrorContext(ctx, "invalid request body", "error", err) + http.Error(w, fmt.Sprintf("Invalid request body: %v", err), http.StatusBadRequest) + return + } + + category, err := c.db.UpdateCategory(ctx, id, cat) + if err != nil { + if err == sql.ErrNoRows { + log.ErrorContext(ctx, "category not found", "category_id", id) + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + log.ErrorContext(ctx, "failed to update category", "error", err) + http.Error(w, fmt.Sprintf("Failed to update category: %s", err.Error()), http.StatusInternalServerError) + return + } + + if err := response.JSON(w, http.StatusCreated, category); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext(ctx, "success update category", "category", category.String()) +} diff --git a/exercise7/blogging-platform/internal/api/handler/main.go b/exercise7/blogging-platform/internal/api/handler/main.go new file mode 100644 index 00000000..04312ea5 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/main.go @@ -0,0 +1,24 @@ +package handler + +import ( + "log/slog" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler/categories" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler/posts" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler/tags" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" +) + +type Handler struct { + *categories.Categories + *posts.Posts + *tags.Tags +} + +func New(logger *slog.Logger, db *db.DB) *Handler { + return &Handler{ + Categories: categories.New(logger, db), + Posts: posts.New(logger, db), + Tags: tags.New(logger, db), + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/ping.go b/exercise7/blogging-platform/internal/api/handler/ping.go new file mode 100644 index 00000000..9c7b3efd --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/ping.go @@ -0,0 +1,16 @@ +package handler + +import ( + "log/slog" + "net/http" +) + +func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) { + slog.Info("Received Ping request") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("pong")) + + if err != nil { + slog.Error("failed to write response: %v", err) + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/all_posts.go b/exercise7/blogging-platform/internal/api/handler/posts/all_posts.go new file mode 100644 index 00000000..c7b1b24c --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/all_posts.go @@ -0,0 +1,55 @@ +package posts + +import ( + "fmt" + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (p *Posts) GetAllPosts(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := p.logger.With("method", "GetAllPosts") + perm := r.URL.Query().Get("term") + var posts []blog.Post + var err error + + if perm != "" { + + log := p.logger.With("method", "SearchPosts") + + posts, err = p.db.SearchPosts(ctx, perm) + if err != nil { + log.ErrorContext(ctx, "failed to search posts", "error", err) + http.Error(w, fmt.Sprintf("Failed to search posts: %s", err.Error()), http.StatusInternalServerError) + return + } + + } else { + + posts, err = p.db.GetAllPosts(ctx) + if err != nil { + log.ErrorContext(ctx, "failed to get posts", "error", err) + http.Error(w, fmt.Sprintf("Failed to get posts: %s", err.Error()), http.StatusInternalServerError) + return + } + + } + + if len(posts) == 0 { + log.InfoContext(ctx, "no posts found") + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + + if err := response.JSON(w, http.StatusOK, posts); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, "success get all posts", + "number of posts", len(posts)) +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/delete_post.go b/exercise7/blogging-platform/internal/api/handler/posts/delete_post.go new file mode 100644 index 00000000..89dc08bf --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/delete_post.go @@ -0,0 +1,35 @@ +package posts + +import ( + "fmt" + "net/http" + "strconv" +) + +func (p *Posts) DeletePost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := p.logger.With("method", "DeletePost") + + idString := r.URL.Path[len("/posts/"):] + id, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid post ID", "error", err) + http.Error(w, "Invalid post ID", http.StatusBadRequest) + return + } + + err = p.db.DeletePost(ctx, id) + if err != nil { + if err.Error() == fmt.Sprintf("post with id %d not found", id) { + log.ErrorContext(ctx, "post not found", "category_id", id) + http.Error(w, "404 Not Found", http.StatusNotFound) + } else { + log.ErrorContext(ctx, "failed to delete post", "error", err) + http.Error(w, fmt.Sprintf("Failed to delete post: %s", err.Error()), http.StatusInternalServerError) + } + return + } + + log.InfoContext(ctx, "success delete post", "post_id", id) + w.WriteHeader(http.StatusNoContent) +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/info_post.go b/exercise7/blogging-platform/internal/api/handler/posts/info_post.go new file mode 100644 index 00000000..d65ef985 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/info_post.go @@ -0,0 +1,39 @@ +package posts + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (p *Posts) GetInformationOfPost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := p.logger.With("method", "GetInformationOfPost") + + idString := r.URL.Path[len("/posts/"):] + id, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid post ID", "error", err) + http.Error(w, "Invalid post ID", http.StatusBadRequest) + return + } + + post, err := p.db.GetInformationOfPost(ctx, id) + if err != nil { + log.ErrorContext(ctx, "post not found", "post_id", id) + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + + if err := response.JSON(w, http.StatusOK, post); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, "success get information of post", + "post_id", id) +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/insert_post.go b/exercise7/blogging-platform/internal/api/handler/posts/insert_post.go new file mode 100644 index 00000000..c9037f40 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/insert_post.go @@ -0,0 +1,41 @@ +package posts + +import ( + "fmt" + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (p *Posts) InsertPost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := p.logger.With("method", "InsertPost") + var req blog.PostRequest + + if err := request.JSON(w, r, &req); err != nil { + log.ErrorContext(ctx, "invalid request body", "error", err) + http.Error(w, fmt.Sprintf("Invalid request body: %v", err), http.StatusBadRequest) + return + } + + post, err := p.db.InsertPost(ctx, req) + if err != nil { + log.ErrorContext(ctx, "failed to create post", "error", err) + http.Error(w, fmt.Sprintf("Failed to create post: %v", err), http.StatusInternalServerError) + return + } + + if err := response.JSON(w, http.StatusCreated, post); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, + "success create post", + "post", post.String(), + ) +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/main.go b/exercise7/blogging-platform/internal/api/handler/posts/main.go new file mode 100644 index 00000000..aa27223f --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/main.go @@ -0,0 +1,19 @@ +package posts + +import ( + "log/slog" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" +) + +type Posts struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Posts { + return &Posts{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/update_post.go b/exercise7/blogging-platform/internal/api/handler/posts/update_post.go new file mode 100644 index 00000000..dae945ab --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/update_post.go @@ -0,0 +1,56 @@ +package posts + +import ( + "database/sql" + "fmt" + "net/http" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (p *Posts) UpdatePost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := p.logger.With("method", "UpdatePost") + + idString := r.URL.Path[len("/posts/"):] + id_post, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid post ID", "error", err) + http.Error(w, "Invalid post ID", http.StatusBadRequest) + return + } + + var req blog.PostRequest + if err := request.JSON(w, r, &req); err != nil { + log.ErrorContext(ctx, "invalid request body", "error", err) + http.Error(w, fmt.Sprintf("Invalid request body: %v", err), http.StatusBadRequest) + return + } + + post, err := p.db.UpdatePost(ctx, id_post, req) + if err != nil { + if err == sql.ErrNoRows { + log.ErrorContext(ctx, "post not found", "post_id", id_post) + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + log.ErrorContext(ctx, "failed to update post", "error", err) + http.Error(w, fmt.Sprintf("Failed to update post: %v", err), http.StatusInternalServerError) + return + } + + if err := response.JSON(w, http.StatusCreated, post); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, + "success update post", + "post", post.String(), + ) +} diff --git a/exercise7/blogging-platform/internal/api/handler/tags/all_posts of tags.go b/exercise7/blogging-platform/internal/api/handler/tags/all_posts of tags.go new file mode 100644 index 00000000..7e851e7c --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/tags/all_posts of tags.go @@ -0,0 +1,46 @@ +package tags + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (t *Tags) GetAllPostsOfTag(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := t.logger.With("method", "GetAllPostsOfTag") + + idString := r.URL.Path[len("/tags/"):] + idString = idString[:len(idString)-6] + id, err := strconv.Atoi(idString) + if err != nil { + log.ErrorContext(ctx, "invalid tag ID", "error", err) + http.Error(w, "Invalid tag ID", http.StatusBadRequest) + return + } + posts, err := t.db.GetAllPostsOfTag(ctx, id) + if err != nil { + log.ErrorContext(ctx, "failed to get posts by tag", "error", err) + http.Error(w, fmt.Sprintf("Failed to get posts: %s", err.Error()), http.StatusInternalServerError) + return + } + + if len(posts) == 0 { + log.InfoContext(ctx, "no posts found for tag", "tag_id", id) + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + + if err := response.JSON(w, http.StatusOK, posts); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext( + ctx, "success get all posts of tag", + "tag_id", id, + "number of posts", len(posts)) +} diff --git a/exercise7/blogging-platform/internal/api/handler/tags/all_tags.go b/exercise7/blogging-platform/internal/api/handler/tags/all_tags.go new file mode 100644 index 00000000..471d30a6 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/tags/all_tags.go @@ -0,0 +1,35 @@ +package tags + +import ( + "fmt" + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +func (t *Tags) GetAllTags(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := t.logger.With("method", "GetAllTags") + + tags, err := t.db.GetAllTags(ctx) + if err != nil { + log.ErrorContext(ctx, "failed to get tags", "error", err) + http.Error(w, fmt.Sprintf("Failed to get tags: %s", err.Error()), http.StatusInternalServerError) + return + } + + if len(tags) == 0 { + log.InfoContext(ctx, "no tags found") + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + + if err := response.JSON(w, http.StatusOK, tags); err != nil { + log.ErrorContext(ctx, "failed to encode response", "error", err) + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + log.InfoContext(ctx, "success get all tags", "number of tags", len(tags)) + +} diff --git a/exercise7/blogging-platform/internal/api/handler/tags/main.go b/exercise7/blogging-platform/internal/api/handler/tags/main.go new file mode 100644 index 00000000..6f218cb8 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/tags/main.go @@ -0,0 +1,19 @@ +package tags + +import ( + "log/slog" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" +) + +type Tags struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Tags { + return &Tags{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/api/main.go b/exercise7/blogging-platform/internal/api/main.go new file mode 100644 index 00000000..e06ac3c1 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/main.go @@ -0,0 +1,73 @@ +package api + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/router" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" +) + +type Api struct { + logger *slog.Logger + router *router.Router + server *http.Server +} + +func New(logger *slog.Logger, db *db.DB) *Api { + h := handler.New(logger, db) + r := router.New(h) + + return &Api{ + logger: logger, + router: r, + } +} + +func (api *Api) Start(ctx context.Context) error { + mux := api.router.Start(ctx) + + port, err := strconv.Atoi(os.Getenv("API_PORT")) + if err != nil { + return err + } + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + BaseContext: func(_ net.Listener) context.Context { + return ctx + }, + } + + api.server = srv + + slog.InfoContext( + ctx, + "starting service", + "port", port, + ) + + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.ErrorContext(ctx, "service error", "error", err) + return err + } + + return nil +} + +func (api *Api) Stop(ctx context.Context) error { + if err := api.server.Shutdown(ctx); err != nil { + slog.ErrorContext(ctx, "server shutdown error", "error", err) + return err + } + + return nil +} diff --git a/exercise7/blogging-platform/internal/api/router/categories.go b/exercise7/blogging-platform/internal/api/router/categories.go new file mode 100644 index 00000000..865d0849 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/categories.go @@ -0,0 +1,15 @@ +package router + +import ( + "context" +) + +func (r *Router) categories(ctx context.Context) { + r.router.HandleFunc("POST /categories", r.handler.InsertCategory) + r.router.HandleFunc("DELETE /categories/{id}", r.handler.DeleteCategory) + r.router.HandleFunc("PUT /categories/{id}", r.handler.UpdateCategory) + + r.router.HandleFunc("GET /categories", r.handler.GetAllCategories) + r.router.HandleFunc("GET /categories/{id}", r.handler.GetInformationOfCategory) + r.router.HandleFunc("GET /categories/{id}/posts", r.handler.GetAllPostsOfCategory) +} diff --git a/exercise7/blogging-platform/internal/api/router/posts.go b/exercise7/blogging-platform/internal/api/router/posts.go new file mode 100644 index 00000000..c1954955 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/posts.go @@ -0,0 +1,13 @@ +package router + +import ( + "context" +) + +func (r *Router) posts(ctx context.Context) { + r.router.HandleFunc("POST /posts", r.handler.InsertPost) + r.router.HandleFunc("DELETE /posts/{id}", r.handler.DeletePost) + r.router.HandleFunc("PUT /posts/{id}", r.handler.UpdatePost) + r.router.HandleFunc("GET /posts/{id}", r.handler.GetInformationOfPost) + r.router.HandleFunc("GET /posts", r.handler.GetAllPosts) +} diff --git a/exercise7/blogging-platform/internal/api/router/router.go b/exercise7/blogging-platform/internal/api/router/router.go new file mode 100644 index 00000000..6886a78c --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/router.go @@ -0,0 +1,30 @@ +package router + +import ( + "context" + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler" +) + +type Router struct { + router *http.ServeMux + handler *handler.Handler +} + +func New(handler *handler.Handler) *Router { + mux := http.NewServeMux() + + return &Router{ + router: mux, + handler: handler, + } +} + +func (r *Router) Start(ctx context.Context) *http.ServeMux { + r.server(ctx) + r.categories(ctx) + r.posts(ctx) + r.tags(ctx) + return r.router +} diff --git a/exercise7/blogging-platform/internal/api/router/server.go b/exercise7/blogging-platform/internal/api/router/server.go new file mode 100644 index 00000000..b4fa6747 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/server.go @@ -0,0 +1,9 @@ +package router + +import ( + "context" +) + +func (r *Router) server(ctx context.Context) { + r.router.HandleFunc("GET /ping", r.handler.Ping) +} diff --git a/exercise7/blogging-platform/internal/api/router/tags.go b/exercise7/blogging-platform/internal/api/router/tags.go new file mode 100644 index 00000000..7112d5f1 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/tags.go @@ -0,0 +1,10 @@ +package router + +import ( + "context" +) + +func (r *Router) tags(ctx context.Context) { + r.router.HandleFunc("GET /tags", r.handler.GetAllTags) + r.router.HandleFunc("GET /tags/{id}/posts", r.handler.GetAllPostsOfTag) +} diff --git a/exercise7/blogging-platform/internal/db/blog/main.go b/exercise7/blogging-platform/internal/db/blog/main.go new file mode 100644 index 00000000..1959bfbc --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/main.go @@ -0,0 +1,103 @@ +package blog + +import ( + "encoding/json" + "fmt" + "time" +) + +type Category struct { + ID int `json:"id"` + Name string `json:"name"` + Posts []*Post `json:"posts,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +type CategoryRequest struct { + Name string `json:"name"` +} + +func (c *Category) MarshalJSON() ([]byte, error) { + type Alias Category + return json.Marshal(&struct { + *Alias + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + }{ + Alias: (*Alias)(c), + CreatedAt: filterNullTime(c.CreatedAt), + UpdatedAt: filterNullTime(c.UpdatedAt), + }) +} + +func filterNullTime(t *time.Time) *time.Time { + if t == nil || t.IsZero() { + return nil + } + return t +} + +type Tag struct { + ID int `json:"id"` + Name string `json:"name"` + Posts []*Post `json:"posts,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +func (t *Tag) MarshalJSON() ([]byte, error) { + type Alias Tag + return json.Marshal(&struct { + *Alias + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + }{ + Alias: (*Alias)(t), + CreatedAt: filterNullTime(t.CreatedAt), + UpdatedAt: filterNullTime(t.UpdatedAt), + }) +} + +type Post struct { + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Category *Category `json:"category"` + Tags []*Tag `json:"tags,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +func (p *Post) MarshalJSON() ([]byte, error) { + type Alias Post + return json.Marshal(&struct { + *Alias + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + }{ + Alias: (*Alias)(p), + CreatedAt: filterNullTime(p.CreatedAt), + UpdatedAt: filterNullTime(p.UpdatedAt), + }) +} + +type PostRequest struct { + Title string `json:"title"` + Content string `json:"content"` + Category string `json:"category"` + Tags []string `json:"tags"` +} + +// для правильного вывода логов +func (c *Category) String() string { + return fmt.Sprintf("Category: {ID: %d, Name: %s }", c.ID, c.Name) +} + +func (t *Tag) String() string { + return fmt.Sprintf("Tag: {ID: %d, Name: %s}", t.ID, t.Name) +} + +func (p *Post) String() string { + return fmt.Sprintf("Post:{ID: %d, Title: %s, Content: %s, %s, Tags: %v}", p.ID, p.Title, p.Content, p.Category.String(), p.Tags) +} diff --git a/exercise7/blogging-platform/internal/db/category/all_categories.go b/exercise7/blogging-platform/internal/db/category/all_categories.go new file mode 100644 index 00000000..ff8427c7 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/category/all_categories.go @@ -0,0 +1,38 @@ +package category + +import ( + "context" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (c *Category) GetAllCategories(ctx context.Context) ([]blog.Category, error) { + log := c.logger.With("method", "GetAllCategories") + + query := `SELECT id, name, created_at, updated_at FROM category` + rows, err := c.db.QueryContext(ctx, query) + if err != nil { + log.ErrorContext(ctx, "fail to query categories", "error", err) + return nil, err + } + + defer rows.Close() + + var categories []blog.Category + for rows.Next() { + var cat blog.Category + if err := rows.Scan(&cat.ID, &cat.Name, &cat.CreatedAt, &cat.UpdatedAt); err != nil { + log.ErrorContext(ctx, "fail to scan category", "error", err) + return nil, err + } + categories = append(categories, cat) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success get all categories") + return categories, nil +} diff --git a/exercise7/blogging-platform/internal/db/category/all_posts_of_category.go b/exercise7/blogging-platform/internal/db/category/all_posts_of_category.go new file mode 100644 index 00000000..ae970bd0 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/category/all_posts_of_category.go @@ -0,0 +1,108 @@ +package category + +import ( + "context" + "database/sql" + "sort" + "strconv" + "strings" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (c *Category) GetAllPostsOfCategory(ctx context.Context, id int) ([]blog.Post, error) { + log := c.logger.With("method", "GetAllPostsOfCategory") + + query := ` + SELECT + p.id, + p.title, + p.content, + p.created_at, + p.updated_at, + c.id AS category_id, + c.name AS category_name, + STRING_AGG(t.id::TEXT, ',') AS tag_ids, + STRING_AGG(t.name, ',') AS tag_names + FROM + post p + JOIN category c ON p.id_category = c.id + LEFT JOIN post_tags pt ON p.id = pt.id_post + LEFT JOIN tag t ON pt.id_tag = t.id + WHERE c.id = $1 + GROUP BY p.id, c.id + ORDER BY p.id; + ` + + rows, err := c.db.QueryContext(ctx, query, id) + if err != nil { + log.ErrorContext(ctx, "fail to query posts", "error", err) + return nil, err + } + defer rows.Close() + + var posts []blog.Post + + for rows.Next() { + var post blog.Post + var category blog.Category + var tagIDs, tagNames sql.NullString + + err := rows.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.CreatedAt, + &post.UpdatedAt, + &category.ID, + &category.Name, + &tagIDs, + &tagNames, + ) + if err != nil { + log.ErrorContext(ctx, "fail to scan posts", "error", err) + return nil, err + } + + post.Category = &category + + if tagIDs.Valid && tagIDs.String != "" { + tagIDStrings := strings.Split(tagIDs.String, ",") + for _, idStr := range tagIDStrings { + tagID, err := strconv.Atoi(idStr) + if err == nil { + post.Tags = append(post.Tags, &blog.Tag{ + ID: tagID, + }) + } + } + } + + if tagNames.Valid && tagNames.String != "" { + tagNameStrings := strings.Split(tagNames.String, ",") + + for i, name := range tagNameStrings { + if i < len(post.Tags) { + post.Tags[i].Name = name + } else { + post.Tags = append(post.Tags, &blog.Tag{ + Name: name, + }) + } + } + } + sort.Slice(post.Tags, func(i, j int) bool { + return post.Tags[i].ID < post.Tags[j].ID + }) + + posts = append(posts, post) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success get all posts of category") + return posts, nil +} diff --git a/exercise7/blogging-platform/internal/db/category/delete_category.go b/exercise7/blogging-platform/internal/db/category/delete_category.go new file mode 100644 index 00000000..c46e98dd --- /dev/null +++ b/exercise7/blogging-platform/internal/db/category/delete_category.go @@ -0,0 +1,32 @@ +package category + +import ( + "context" + "fmt" +) + +func (c *Category) DeleteCategory(ctx context.Context, id int) error { + log := c.logger.With("method", "DeleteCategory") + + queryDeleteCategory := `DELETE FROM category WHERE id = $1` + deleteCategory, err := c.db.ExecContext(ctx, queryDeleteCategory, id) + if err != nil { + log.ErrorContext(ctx, "failed to delete category", "error", err) + return err + } + + countCategory, err := deleteCategory.RowsAffected() + if err != nil { + log.ErrorContext(ctx, "failed to get rows affected", "error", err) + return err + } + + if countCategory == 0 { + log.ErrorContext(ctx, "category not found", "category_id", id) + return fmt.Errorf("category with id %d not found", id) + } + + log.InfoContext(ctx, "success delete category") + + return nil +} diff --git a/exercise7/blogging-platform/internal/db/category/info_category.go b/exercise7/blogging-platform/internal/db/category/info_category.go new file mode 100644 index 00000000..3f3251fa --- /dev/null +++ b/exercise7/blogging-platform/internal/db/category/info_category.go @@ -0,0 +1,27 @@ +package category + +import ( + "context" + "database/sql" + "fmt" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (c *Category) GetInformationOfCategory(ctx context.Context, id int) (*blog.Category, error) { + log := c.logger.With("method", "GetInformationOfCategory") + + var category blog.Category + query := `SELECT id, name, created_at, updated_at FROM category WHERE id = $1` + row := c.db.QueryRowContext(ctx, query, id) + err := row.Scan(&category.ID, &category.Name, &category.CreatedAt, &category.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("category with id %d not found", id) + } + return nil, err + } + + log.InfoContext(ctx, "success get information of category") + return &category, nil +} diff --git a/exercise7/blogging-platform/internal/db/category/insert_category.go b/exercise7/blogging-platform/internal/db/category/insert_category.go new file mode 100644 index 00000000..2416b8bf --- /dev/null +++ b/exercise7/blogging-platform/internal/db/category/insert_category.go @@ -0,0 +1,22 @@ +package category + +import ( + "context" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (c *Category) InsertCategory(ctx context.Context, cat blog.CategoryRequest) (*blog.Category, error) { + log := c.logger.With("method", "InsertCategory") + + var category blog.Category + query := `INSERT INTO category (name) VALUES ($1) RETURNING id, name, created_at, updated_at` + err := c.db.QueryRowContext(ctx, query, cat.Name).Scan(&category.ID, &category.Name, &category.CreatedAt, &category.UpdatedAt) + if err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success insert new category") + return &category, nil +} diff --git a/exercise7/blogging-platform/internal/db/category/main.go b/exercise7/blogging-platform/internal/db/category/main.go new file mode 100644 index 00000000..440c191a --- /dev/null +++ b/exercise7/blogging-platform/internal/db/category/main.go @@ -0,0 +1,18 @@ +package category + +import ( + "database/sql" + "log/slog" +) + +type Category struct { + logger *slog.Logger + db *sql.DB +} + +func New(db *sql.DB, logger *slog.Logger) *Category { + return &Category{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/db/category/update_category.go b/exercise7/blogging-platform/internal/db/category/update_category.go new file mode 100644 index 00000000..57aa59c5 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/category/update_category.go @@ -0,0 +1,32 @@ +package category + +import ( + "context" + "database/sql" + "errors" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (c *Category) UpdateCategory(ctx context.Context, id int, cat blog.CategoryRequest) (*blog.Category, error) { + log := c.logger.With("method", "UpdateCategory") + + if cat.Name == "" { + log.ErrorContext(ctx, "category name cannot be empty") + return nil, errors.New("category name cannot be empty") + } + + var category blog.Category + query := `UPDATE category SET name = $1, updated_at = NOW() WHERE id = $2 RETURNING id, name, created_at, updated_at` + err := c.db.QueryRowContext(ctx, query, cat.Name, id).Scan(&category.ID, &category.Name, &category.CreatedAt, &category.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + log.ErrorContext(ctx, "failed to update category", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success update category") + return &category, nil +} diff --git a/exercise7/blogging-platform/internal/db/init.go b/exercise7/blogging-platform/internal/db/init.go new file mode 100644 index 00000000..905caad7 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/init.go @@ -0,0 +1,108 @@ +package db + +import ( + "context" +) + +func (db *DB) Init(ctx context.Context) error { + log := db.logger.With("method", "Init") + + if err := db.InitCategory(ctx); err != nil { + return err + } + + if err := db.InitTag(ctx); err != nil { + return err + } + + if err := db.InitPost(ctx); err != nil { + return err + } + + if err := db.InitPostTags(ctx); err != nil { + return err + } + + log.InfoContext(ctx, "success create tables for blogging system") + return nil +} + +func (db *DB) InitCategory(ctx context.Context) error { + log := db.logger.With("table", "category") + stmt := ` + CREATE TABLE IF NOT EXISTS category + ( + id serial PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + );` + + if _, err := db.pg.Exec(stmt); err != nil { + log.ErrorContext(ctx, "failed to create category table", "error", err) + return err + } + + return nil +} + +func (db *DB) InitTag(ctx context.Context) error { + log := db.logger.With("table", "tag") + stmt := ` + CREATE TABLE IF NOT EXISTS tag + ( + id serial PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + );` + + if _, err := db.pg.Exec(stmt); err != nil { + log.ErrorContext(ctx, "failed to create tag table", "error", err) + return err + } + + return nil +} + +func (db *DB) InitPost(ctx context.Context) error { + log := db.logger.With("table", "post") + stmt := ` + CREATE TABLE IF NOT EXISTS post + ( + id serial PRIMARY KEY, + id_category int references category (id) on delete cascade not null, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + );` + + if _, err := db.pg.Exec(stmt); err != nil { + log.ErrorContext(ctx, "failed to create post table", "error", err) + return err + } + + return nil +} + +func (db *DB) InitPostTags(ctx context.Context) error { + log := db.logger.With("table", "post_tags") + + stmt := ` + CREATE TABLE IF NOT EXISTS post_tags + ( + id serial PRIMARY KEY, + id_post int references post (id) on delete cascade not null, + id_tag int references tag (id) on delete cascade not null, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + );` + + if _, err := db.pg.Exec(stmt); err != nil { + log.ErrorContext(ctx, "failed to create post_tags table", "error", err) + return err + } + + return nil +} diff --git a/exercise7/blogging-platform/internal/db/main.go b/exercise7/blogging-platform/internal/db/main.go new file mode 100644 index 00000000..d50e3518 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/main.go @@ -0,0 +1,73 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "log/slog" + "os" + "strconv" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/category" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/post" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/tag" + + _ "github.com/lib/pq" +) + +type DB struct { + logger *slog.Logger + pg *sql.DB + *category.Category + *post.Post + *tag.Tag +} + +func New(logger *slog.Logger) (*DB, error) { + pgsql, err := newPgSQL() + if err != nil { + return nil, err + } + + return &DB{ + logger: logger, + pg: pgsql, + Category: category.New(pgsql, logger), + Post: post.New(pgsql, logger), + Tag: tag.New(pgsql, logger), + }, nil +} + +func newPgSQL() (*sql.DB, error) { + host := os.Getenv("DB_HOST") + port, err := strconv.Atoi(os.Getenv("DB_PORT")) + if err != nil { + return nil, err + } + user := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + dbname := os.Getenv("DB_NAME") + + psqlInfo := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname, + ) + + db, err := sql.Open("postgres", psqlInfo) + if err != nil { + return nil, err + } + + return db, nil +} + +func (db *DB) Ping(ctx context.Context) error { + err := db.pg.PingContext(ctx) + if err != nil { + db.logger.ErrorContext(ctx, "failed to connect to database", "error", err) + return err + } + + db.logger.InfoContext(ctx, "success connected to database") + return nil +} diff --git a/exercise7/blogging-platform/internal/db/post/all_posts.go b/exercise7/blogging-platform/internal/db/post/all_posts.go new file mode 100644 index 00000000..ef7fe80d --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/all_posts.go @@ -0,0 +1,107 @@ +package post + +import ( + "context" + "database/sql" + "sort" + "strconv" + "strings" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (p *Post) GetAllPosts(ctx context.Context) ([]blog.Post, error) { + log := p.logger.With("method", "GetAllPosts") + + query := ` + SELECT + p.id, + p.title, + p.content, + p.created_at, + p.updated_at, + c.id AS category_id, + c.name AS category_name, + STRING_AGG(t.id::TEXT, ',') AS tag_ids, + STRING_AGG(t.name, ',') AS tag_names + FROM + post p + JOIN category c ON p.id_category = c.id + LEFT JOIN post_tags pt ON p.id = pt.id_post + LEFT JOIN tag t ON pt.id_tag = t.id + GROUP BY p.id, c.id + ORDER BY p.id; + ` + + rows, err := p.db.QueryContext(ctx, query) + if err != nil { + log.ErrorContext(ctx, "fail to query posts", "error", err) + return nil, err + } + defer rows.Close() + + var posts []blog.Post + + for rows.Next() { + var post blog.Post + var category blog.Category + var tagIDs, tagNames sql.NullString + + err := rows.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.CreatedAt, + &post.UpdatedAt, + &category.ID, + &category.Name, + &tagIDs, + &tagNames, + ) + if err != nil { + log.ErrorContext(ctx, "fail to scan posts", "error", err) + return nil, err + } + + post.Category = &category + + if tagIDs.Valid && tagIDs.String != "" { + tagIDStrings := strings.Split(tagIDs.String, ",") + for _, idStr := range tagIDStrings { + tagID, err := strconv.Atoi(idStr) + if err == nil { + post.Tags = append(post.Tags, &blog.Tag{ + ID: tagID, + }) + } + } + } + + if tagNames.Valid && tagNames.String != "" { + tagNameStrings := strings.Split(tagNames.String, ",") + + for i, name := range tagNameStrings { + if i < len(post.Tags) { + post.Tags[i].Name = name + } else { + post.Tags = append(post.Tags, &blog.Tag{ + Name: name, + }) + } + } + } + sort.Slice(post.Tags, func(i, j int) bool { + return post.Tags[i].ID < post.Tags[j].ID + }) + + posts = append(posts, post) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success get all posts") + return posts, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/delete_post.go b/exercise7/blogging-platform/internal/db/post/delete_post.go new file mode 100644 index 00000000..9f273a95 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/delete_post.go @@ -0,0 +1,32 @@ +package post + +import ( + "context" + "fmt" +) + +func (p *Post) DeletePost(ctx context.Context, id int) error { + log := p.logger.With("method", "DeletePost") + + queryDeletePost := `DELETE FROM post WHERE id = $1` + deletePost, err := p.db.ExecContext(ctx, queryDeletePost, id) + if err != nil { + log.ErrorContext(ctx, "failed to delete post", "error", err) + return err + } + + countPost, err := deletePost.RowsAffected() + if err != nil { + log.ErrorContext(ctx, "failed to get rows affected", "error", err) + return err + } + + if countPost == 0 { + log.ErrorContext(ctx, "post not found", "post_id", id) + return fmt.Errorf("post with id %d not found", id) + } + + log.InfoContext(ctx, "success delete post") + + return nil +} diff --git a/exercise7/blogging-platform/internal/db/post/info_post.go b/exercise7/blogging-platform/internal/db/post/info_post.go new file mode 100644 index 00000000..892fcdfc --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/info_post.go @@ -0,0 +1,69 @@ +package post + +import ( + "context" + "database/sql" + "fmt" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (p *Post) GetInformationOfPost(ctx context.Context, id int) (blog.Post, error) { + log := p.logger.With("method", "GetInformationOfPost") + + query := ` + SELECT p.id, p.title, p.content, p.created_at, p.updated_at, + c.id, c.name, + t.id, t.name + FROM post p + JOIN category c ON p.id_category = c.id + LEFT JOIN post_tags pt ON pt.id_post = p.id + LEFT JOIN tag t ON pt.id_tag = t.id + WHERE p.id = $1 + ` + + rows, err := p.db.QueryContext(ctx, query, id) + if err != nil { + log.ErrorContext(ctx, "fail to get post information", "error", err) + return blog.Post{}, err + } + defer rows.Close() + + var post blog.Post + var category blog.Category + var tags []*blog.Tag + postFound := false + for rows.Next() { + var tagID sql.NullInt64 + var tagName sql.NullString + + err := rows.Scan(&post.ID, &post.Title, &post.Content, &post.CreatedAt, &post.UpdatedAt, + &category.ID, &category.Name, &tagID, &tagName) + if err != nil { + log.ErrorContext(ctx, "failed to scan row", "error", err) + return blog.Post{}, err + } + + post.Category = &category + + if tagID.Valid && tagName.Valid { + tags = append(tags, &blog.Tag{ID: int(tagID.Int64), Name: tagName.String}) + } + + postFound = true + } + + if !postFound { + log.ErrorContext(ctx, "post not found", "post_id", id) + return blog.Post{}, fmt.Errorf("post with id %d not found", id) + } + + post.Tags = tags + + if len(tags) == 0 { + log.InfoContext(ctx, "no tags found for the post", "post_id", id) + } + + log.InfoContext(ctx, "success get information of post") + return post, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/insert_post.go b/exercise7/blogging-platform/internal/db/post/insert_post.go new file mode 100644 index 00000000..a09fcedc --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/insert_post.go @@ -0,0 +1,157 @@ +package post + +import ( + "context" + "database/sql" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (p *Post) InsertPost(ctx context.Context, req blog.PostRequest) (*blog.Post, error) { + log := p.logger.With("method", "InsertPost") + + tx, err := p.db.BeginTx(ctx, nil) + if err != nil { + log.ErrorContext(ctx, "fail to begin transaction", "error", err) + return nil, err + } + defer tx.Rollback() + + id_category, err := p.GetCategoryIDByName(ctx, tx, req.Category) + if err != nil { + log.ErrorContext(ctx, "failed to get category by name", "error", err) + return nil, err + } + + category := &blog.Category{ + ID: id_category, + Name: req.Category, + } + + if id_category == 0 { + category, err = p.InsertCat(ctx, tx, *category) + if err != nil { + log.ErrorContext(ctx, "failed to create category", "error", err) + return nil, err + } + } + + id_category = category.ID + post, err := p.InsertInfoPost(ctx, tx, id_category, req) + if err != nil { + log.ErrorContext(ctx, "failed to create post", "error", err) + return nil, err + } + post.Category = category + + tags, err := p.InsertTags(ctx, tx, post.ID, req.Tags) + if err != nil { + log.ErrorContext(ctx, "failed to insert tags", "error", err) + return nil, err + } + post.Tags = tags + + if err := tx.Commit(); err != nil { + log.ErrorContext(ctx, "fail to commit transaction", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success insert new post") + return post, nil +} + +func (p *Post) InsertInfoPost(ctx context.Context, tx *sql.Tx, id_category int, req blog.PostRequest) (*blog.Post, error) { + log := p.logger.With("method", "InsertPost") + var post blog.Post + query := `INSERT INTO post (id_category,title,"content") VALUES ($1, $2, $3) RETURNING id, title, content, created_at, updated_at` + err := tx.QueryRowContext(ctx, query, id_category, req.Title, req.Content).Scan(&post.ID, &post.Title, &post.Content, &post.CreatedAt, &post.UpdatedAt) + if err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + return &post, nil +} + +func (p *Post) GetCategoryIDByName(ctx context.Context, tx *sql.Tx, name string) (int, error) { + log := p.logger.With("method", "InsertPost") + query := `SELECT category.id FROM category WHERE name= $1` + var id int + err := tx.QueryRowContext(ctx, query, name).Scan(&id) + if err != nil { + if err == sql.ErrNoRows { + return 0, nil + } + log.ErrorContext(ctx, "failed to get category by name", "error", err) + return 0, err + } + + return id, nil +} + +func (p *Post) InsertCat(ctx context.Context, tx *sql.Tx, cat blog.Category) (*blog.Category, error) { + log := p.logger.With("method", "InsertPost") + var category blog.Category + query := `INSERT INTO category (name) VALUES ($1) RETURNING id, name, created_at, updated_at` + err := tx.QueryRowContext(ctx, query, cat.Name).Scan(&category.ID, &category.Name, &category.CreatedAt, &category.UpdatedAt) + if err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success insert new category") + return &category, nil +} + +func (p *Post) InsertTags(ctx context.Context, tx *sql.Tx, id_post int, tags []string) ([]*blog.Tag, error) { + log := p.logger.With("method", "InsertPost") + var insertedTags []*blog.Tag + + for _, tagName := range tags { + var tag blog.Tag + + err := tx.QueryRowContext(ctx, "SELECT id, name, created_at, updated_at FROM tag WHERE name = $1", tagName).Scan(&tag.ID, &tag.Name, &tag.CreatedAt, &tag.UpdatedAt) + + if err == sql.ErrNoRows { + + err := tx.QueryRowContext(ctx, "INSERT INTO tag(name) VALUES($1) RETURNING id, name, created_at, updated_at", tagName).Scan(&tag.ID, &tag.Name, &tag.CreatedAt, &tag.UpdatedAt) + if err != nil { + log.ErrorContext(ctx, "fail to insert tag", "error", err) + return nil, err + } + } else if err != nil { + log.ErrorContext(ctx, "fail to check if tag exists", "error", err) + return nil, err + } + + insertedTags = append(insertedTags, &tag) + + err = p.InsertPostTags(ctx, tx, tag.ID, id_post) + if err != nil { + log.ErrorContext(ctx, "fail to insert tag-post relation", "error", err) + return nil, err + } + } + + return insertedTags, nil +} + +func (p *Post) InsertPostTags(ctx context.Context, tx *sql.Tx, tagID, postID int) error { + log := p.logger.With("method", "InsertPost") + var count int + err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM post_tags WHERE id_post = $1 AND id_tag = $2", postID, tagID).Scan(&count) + if err != nil { + log.ErrorContext(ctx, "fail to check if post-tag relation exists", "error", err) + return err + } + + if count == 0 { + _, err := tx.ExecContext(ctx, "INSERT INTO post_tags(id_post, id_tag) VALUES($1, $2)", postID, tagID) + if err != nil { + log.ErrorContext(ctx, "fail to insert post-tag relation", "error", err) + return err + } + } + + return nil +} diff --git a/exercise7/blogging-platform/internal/db/post/main.go b/exercise7/blogging-platform/internal/db/post/main.go new file mode 100644 index 00000000..5995966b --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/main.go @@ -0,0 +1,18 @@ +package post + +import ( + "database/sql" + "log/slog" +) + +type Post struct { + logger *slog.Logger + db *sql.DB +} + +func New(db *sql.DB, logger *slog.Logger) *Post { + return &Post{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/db/post/search_posts.go b/exercise7/blogging-platform/internal/db/post/search_posts.go new file mode 100644 index 00000000..cb14493b --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/search_posts.go @@ -0,0 +1,119 @@ +package post + +import ( + "context" + "database/sql" + "sort" + "strconv" + "strings" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (p *Post) SearchPosts(ctx context.Context, searchQuery string) ([]blog.Post, error) { + log := p.logger.With("method", "SearchPosts") + + query := ` + SELECT + p.id, + p.title, + p.content, + p.created_at, + p.updated_at, + c.id AS category_id, + c.name AS category_name, + STRING_AGG(t.id::TEXT, ',') AS tag_ids, + STRING_AGG(t.name, ',') AS tag_names + FROM + post p + JOIN category c ON p.id_category = c.id + LEFT JOIN post_tags pt ON p.id = pt.id_post + LEFT JOIN tag t ON pt.id_tag = t.id + WHERE + p.title ILIKE '%' || $1 || '%' OR + p.content ILIKE '%' || $2 || '%' OR + c.name ILIKE '%' || $3 || '%' OR + t.name ILIKE '%' || $4 || '%' + GROUP BY p.id, c.id + ORDER BY p.id; + ` + + args := []interface{}{ + "%" + searchQuery + "%", + "%" + searchQuery + "%", + "%" + searchQuery + "%", + "%" + searchQuery + "%", + } + + rows, err := p.db.QueryContext(ctx, query, args...) + if err != nil { + log.ErrorContext(ctx, "failed to search posts", "error", err) + return nil, err + } + defer rows.Close() + + var posts []blog.Post + + for rows.Next() { + var post blog.Post + var category blog.Category + var tagIDs, tagNames sql.NullString + + err := rows.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.CreatedAt, + &post.UpdatedAt, + &category.ID, + &category.Name, + &tagIDs, + &tagNames, + ) + if err != nil { + log.ErrorContext(ctx, "fail to scan posts", "error", err) + return nil, err + } + + post.Category = &category + + if tagIDs.Valid && tagIDs.String != "" { + tagIDStrings := strings.Split(tagIDs.String, ",") + for _, idStr := range tagIDStrings { + tagID, err := strconv.Atoi(idStr) + if err == nil { + post.Tags = append(post.Tags, &blog.Tag{ + ID: tagID, + }) + } + } + } + + if tagNames.Valid && tagNames.String != "" { + tagNameStrings := strings.Split(tagNames.String, ",") + + for i, name := range tagNameStrings { + if i < len(post.Tags) { + post.Tags[i].Name = name + } else { + post.Tags = append(post.Tags, &blog.Tag{ + Name: name, + }) + } + } + } + sort.Slice(post.Tags, func(i, j int) bool { + return post.Tags[i].ID < post.Tags[j].ID + }) + + posts = append(posts, post) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success search posts") + return posts, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/update_post.go b/exercise7/blogging-platform/internal/db/post/update_post.go new file mode 100644 index 00000000..f523530a --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/update_post.go @@ -0,0 +1,188 @@ +package post + +import ( + "context" + "database/sql" + "fmt" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (p *Post) UpdatePost(ctx context.Context, id_post int, req blog.PostRequest) (*blog.Post, error) { + log := p.logger.With("method", "UpdatePost") + + tx, err := p.db.BeginTx(ctx, nil) + if err != nil { + log.ErrorContext(ctx, "fail to begin transaction", "error", err) + return nil, err + } + defer tx.Rollback() + + id_category, err := p.GetCategoryIDByName(ctx, tx, req.Category) + if err != nil { + log.ErrorContext(ctx, "failed to get category by name", "error", err) + return nil, err + } + category := &blog.Category{ + ID: id_category, + Name: req.Category, + } + if id_category == 0 { + category, err = p.InsertCat(ctx, tx, *category) + if err != nil { + log.ErrorContext(ctx, "failed to create category", "error", err) + return nil, err + } + } + + id_category = category.ID + post, err := p.UpdateInfoPost(ctx, tx, id_post, id_category, req) + if err != nil { + log.ErrorContext(ctx, "failed to update post", "error", err) + return nil, err + } + post.Category = category + + tags, err := p.UpdateTags(ctx, tx, post.ID, req.Tags) + if err != nil { + log.ErrorContext(ctx, "failed to update tags", "error", err) + return nil, err + } + post.Tags = tags + + if err := tx.Commit(); err != nil { + log.ErrorContext(ctx, "fail to commit transaction", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success update post") + + return post, nil +} + +func (p *Post) UpdateInfoPost(ctx context.Context, tx *sql.Tx, id_post int, id_category int, req blog.PostRequest) (*blog.Post, error) { + log := p.logger.With("method", "UpdatePost") + + var post blog.Post + query := `UPDATE post SET id_category = $1 , title = $2, content = $3, updated_at = NOW() WHERE id = $4 RETURNING id, title, content, created_at, updated_at` + err := tx.QueryRowContext(ctx, query, id_category, req.Title, req.Content, id_post).Scan(&post.ID, &post.Title, &post.Content, &post.CreatedAt, &post.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + log.ErrorContext(ctx, "failed to update post", "error", err) + return nil, err + } + + return &post, nil +} + +func (p *Post) UpdateTags(ctx context.Context, tx *sql.Tx, id_post int, tags []string) ([]*blog.Tag, error) { + log := p.logger.With("method", "UpdatePost") + + query := ` + SELECT t.id, t.name + FROM tag t + JOIN post_tags pt ON pt.id_tag = t.id + WHERE pt.id_post = $1 + ` + rows, err := tx.QueryContext(ctx, query, id_post) + if err != nil { + log.ErrorContext(ctx, "fail getting current tags for post", "error", err) + return nil, err + } + defer rows.Close() + + currentTags := make(map[int]*blog.Tag) + for rows.Next() { + var tag blog.Tag + if err := rows.Scan(&tag.ID, &tag.Name); err != nil { + log.ErrorContext(ctx, "fail to scanning tag", "error", err) + return nil, err + } + currentTags[tag.ID] = &tag + } + + var newTagIDs []int + for _, tagName := range tags { + var tag blog.Tag + + err := tx.QueryRowContext(ctx, "SELECT id, name FROM tag WHERE name = $1", tagName).Scan(&tag.ID, &tag.Name) + if err != nil && err != sql.ErrNoRows { + log.ErrorContext(ctx, "fail to query tag", "error", err) + return nil, err + } + + if err == sql.ErrNoRows { + + err := tx.QueryRowContext(ctx, "INSERT INTO tag(name) VALUES($1) RETURNING id", tagName).Scan(&tag.ID) + if err != nil { + log.ErrorContext(ctx, "fail to insert new tag", "error", err) + return nil, err + } + } + + newTagIDs = append(newTagIDs, tag.ID) + } + + for tagID, _ := range currentTags { + if !contains(newTagIDs, tagID) { + _, err := tx.ExecContext(ctx, "DELETE FROM post_tags WHERE id_post = $1 AND id_tag = $2", id_post, tagID) + if err != nil { + log.ErrorContext(ctx, fmt.Sprintf("fail to delete tag %d from post %d", tagID, id_post), "error", err) + return nil, err + } + } + } + + for _, tagID := range newTagIDs { + + var count int + err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM post_tags WHERE id_post = $1 AND id_tag = $2", id_post, tagID).Scan(&count) + if err != nil { + log.ErrorContext(ctx, fmt.Sprintf("fail to checking if post is already linked with tag: %d", tagID), "error", err) + return nil, err + } + if count == 0 { + _, err := tx.ExecContext(ctx, "INSERT INTO post_tags(id_post, id_tag) VALUES($1, $2)", id_post, tagID) + if err != nil { + log.ErrorContext(ctx, fmt.Sprintf("fail to insert post_tag for post %d and tag %d", id_post, tagID), "error", err) + return nil, err + } + } + } + + query = ` + SELECT t.id, t.name + FROM tag t + JOIN post_tags pt ON pt.id_tag = t.id + WHERE pt.id_post = $1 + ` + rows, err = tx.QueryContext(ctx, query, id_post) + if err != nil { + log.ErrorContext(ctx, fmt.Sprintf("fail getting updated tags for post %d", id_post), "error", err) + return nil, err + } + defer rows.Close() + + var updatedTags []*blog.Tag + for rows.Next() { + var tag blog.Tag + if err := rows.Scan(&tag.ID, &tag.Name); err != nil { + log.ErrorContext(ctx, "fail scanning updated tag", "error", err) + return nil, err + } + updatedTags = append(updatedTags, &tag) + } + + return updatedTags, nil +} + +func contains(slice []int, val int) bool { + for _, v := range slice { + if v == val { + return true + } + } + return false +} diff --git a/exercise7/blogging-platform/internal/db/tag/all_posts_of_tag.go b/exercise7/blogging-platform/internal/db/tag/all_posts_of_tag.go new file mode 100644 index 00000000..d06be8a5 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/tag/all_posts_of_tag.go @@ -0,0 +1,113 @@ +package tag + +import ( + "context" + "database/sql" + "sort" + "strconv" + "strings" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (t *Tag) GetAllPostsOfTag(ctx context.Context, id int) ([]blog.Post, error) { + log := t.logger.With("method", "GetAllPostsOfTag") + + query := ` + SELECT + p.id, + p.title, + p.content, + p.created_at, + p.updated_at, + c.id AS category_id, + c.name AS category_name, + STRING_AGG(t.id::TEXT, ',') AS tag_ids, + STRING_AGG(t.name, ',') AS tag_names + FROM + post p + JOIN category c ON p.id_category = c.id + LEFT JOIN post_tags pt ON p.id = pt.id_post + LEFT JOIN tag t ON pt.id_tag = t.id + WHERE + p.id IN ( + SELECT DISTINCT pt.id_post + FROM post_tags pt + WHERE pt.id_tag = $1 + ) + GROUP BY p.id, c.id + ORDER BY p.id; + ` + + rows, err := t.db.QueryContext(ctx, query, id) + if err != nil { + log.ErrorContext(ctx, "fail to query posts", "error", err) + return nil, err + } + defer rows.Close() + + var posts []blog.Post + + for rows.Next() { + var post blog.Post + var category blog.Category + var tagIDs, tagNames sql.NullString + + err := rows.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.CreatedAt, + &post.UpdatedAt, + &category.ID, + &category.Name, + &tagIDs, + &tagNames, + ) + if err != nil { + log.ErrorContext(ctx, "fail to scan posts", "error", err) + return nil, err + } + + post.Category = &category + + if tagIDs.Valid && tagIDs.String != "" { + tagIDStrings := strings.Split(tagIDs.String, ",") + for _, idStr := range tagIDStrings { + tagID, err := strconv.Atoi(idStr) + if err == nil { + post.Tags = append(post.Tags, &blog.Tag{ + ID: tagID, + }) + } + } + } + + if tagNames.Valid && tagNames.String != "" { + tagNameStrings := strings.Split(tagNames.String, ",") + + for i, name := range tagNameStrings { + if i < len(post.Tags) { + post.Tags[i].Name = name + } else { + post.Tags = append(post.Tags, &blog.Tag{ + Name: name, + }) + } + } + } + sort.Slice(post.Tags, func(i, j int) bool { + return post.Tags[i].ID < post.Tags[j].ID + }) + + posts = append(posts, post) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success get all posts of tag") + return posts, nil +} diff --git a/exercise7/blogging-platform/internal/db/tag/all_tags.go b/exercise7/blogging-platform/internal/db/tag/all_tags.go new file mode 100644 index 00000000..1bdc2175 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/tag/all_tags.go @@ -0,0 +1,38 @@ +package tag + +import ( + "context" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" +) + +func (t *Tag) GetAllTags(ctx context.Context) ([]blog.Tag, error) { + log := t.logger.With("method", "GetAllTags") + + query := `SELECT id, name, created_at, updated_at FROM tag` + rows, err := t.db.QueryContext(ctx, query) + if err != nil { + log.ErrorContext(ctx, "fail to query tags", "error", err) + return nil, err + } + + defer rows.Close() + + var tags []blog.Tag + for rows.Next() { + var tag_one blog.Tag + if err := rows.Scan(&tag_one.ID, &tag_one.Name, &tag_one.CreatedAt, &tag_one.UpdatedAt); err != nil { + log.ErrorContext(ctx, "fail to scan tags", "error", err) + return nil, err + } + tags = append(tags, tag_one) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success get all tags") + return tags, nil +} diff --git a/exercise7/blogging-platform/internal/db/tag/main.go b/exercise7/blogging-platform/internal/db/tag/main.go new file mode 100644 index 00000000..d72bc8bd --- /dev/null +++ b/exercise7/blogging-platform/internal/db/tag/main.go @@ -0,0 +1,18 @@ +package tag + +import ( + "database/sql" + "log/slog" +) + +type Tag struct { + logger *slog.Logger + db *sql.DB +} + +func New(db *sql.DB, logger *slog.Logger) *Tag { + return &Tag{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 1ffa1477..bfc01c1f 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -8,13 +8,16 @@ import ( "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api" "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" + + "github.com/joho/godotenv" ) func main() { ctx, cancel := context.WithCancel(context.Background()) - // db - _, err := db.New() + _ = godotenv.Load() + + d, err := db.New(slog.With("service", "db")) if err != nil { slog.ErrorContext( ctx, @@ -25,8 +28,15 @@ func main() { panic(err) } - // api - a := api.New() + if err := d.Ping(ctx); err != nil { + panic(err) + } + + if err := d.Init(ctx); err != nil { + panic(err) + } + + a := api.New(slog.With("service", "api"), d) if err := a.Start(ctx); err != nil { slog.ErrorContext( ctx, @@ -46,4 +56,10 @@ func main() { cancel() }() + + if err := a.Stop(ctx); err != nil { + slog.ErrorContext(ctx, "service stop error", "error", err) + } + + slog.InfoContext(ctx, "server was successfully shutdown.") } diff --git a/exercise7/blogging-platform/pkg/httputils/request/body.go b/exercise7/blogging-platform/pkg/httputils/request/body.go index 92d639f4..62b9baf9 100644 --- a/exercise7/blogging-platform/pkg/httputils/request/body.go +++ b/exercise7/blogging-platform/pkg/httputils/request/body.go @@ -1,14 +1,14 @@ package request import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/statusError" + "encoding/json" "errors" "fmt" "io" "net/http" "strings" - - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/statusError" ) func JSON(w http.ResponseWriter, r *http.Request, dst interface{}) error { From bd6c3f9f5ec1887991f8c8bf198be19cd3a630ac Mon Sep 17 00:00:00 2001 From: Madinab99999 Date: Thu, 5 Dec 2024 21:32:23 +0600 Subject: [PATCH 30/30] Update function UpdateTags() in exercise 7 --- .../internal/db/post/update_post.go | 98 ++++++++++++------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/exercise7/blogging-platform/internal/db/post/update_post.go b/exercise7/blogging-platform/internal/db/post/update_post.go index f523530a..be869fd4 100644 --- a/exercise7/blogging-platform/internal/db/post/update_post.go +++ b/exercise7/blogging-platform/internal/db/post/update_post.go @@ -78,8 +78,47 @@ func (p *Post) UpdateInfoPost(ctx context.Context, tx *sql.Tx, id_post int, id_c } func (p *Post) UpdateTags(ctx context.Context, tx *sql.Tx, id_post int, tags []string) ([]*blog.Tag, error) { - log := p.logger.With("method", "UpdatePost") + // Шаг 1: Получение текущих тегов для поста + currentTags, err := p.getCurrentTags(ctx, tx, id_post) + if err != nil { + return nil, err + } + + // Шаг 2: Обработка новых тегов + newTagIDs, err := p.processNewTags(ctx, tx, tags) + if err != nil { + return nil, err + } + + // Шаг 3: Удаление старых тегов, которые больше не привязаны к посту + err = p.deleteOldTags(ctx, tx, id_post, currentTags, newTagIDs) + if err != nil { + return nil, err + } + + // Шаг 4: Добавление новых связей тегов с постом + err = p.addNewTags(ctx, tx, id_post, newTagIDs) + if err != nil { + return nil, err + } + + // Шаг 5: Получение обновленных теги для поста + updatedTags, err := p.getCurrentTags(ctx, tx, id_post) + if err != nil { + return nil, err + } + + var tagsSlice []*blog.Tag + for _, tag := range updatedTags { + tagsSlice = append(tagsSlice, tag) + } + + return tagsSlice, nil +} + +func (p *Post) getCurrentTags(ctx context.Context, tx *sql.Tx, id_post int) (map[int]*blog.Tag, error) { + log := p.logger.With("method", "UpdatePost") query := ` SELECT t.id, t.name FROM tag t @@ -97,16 +136,20 @@ func (p *Post) UpdateTags(ctx context.Context, tx *sql.Tx, id_post int, tags []s for rows.Next() { var tag blog.Tag if err := rows.Scan(&tag.ID, &tag.Name); err != nil { - log.ErrorContext(ctx, "fail to scanning tag", "error", err) + log.ErrorContext(ctx, "fail to scan tag", "error", err) return nil, err } currentTags[tag.ID] = &tag } + return currentTags, nil +} + +func (p *Post) processNewTags(ctx context.Context, tx *sql.Tx, tags []string) ([]int, error) { + log := p.logger.With("method", "UpdatePost") var newTagIDs []int for _, tagName := range tags { var tag blog.Tag - err := tx.QueryRowContext(ctx, "SELECT id, name FROM tag WHERE name = $1", tagName).Scan(&tag.ID, &tag.Name) if err != nil && err != sql.ErrNoRows { log.ErrorContext(ctx, "fail to query tag", "error", err) @@ -114,68 +157,49 @@ func (p *Post) UpdateTags(ctx context.Context, tx *sql.Tx, id_post int, tags []s } if err == sql.ErrNoRows { - err := tx.QueryRowContext(ctx, "INSERT INTO tag(name) VALUES($1) RETURNING id", tagName).Scan(&tag.ID) if err != nil { - log.ErrorContext(ctx, "fail to insert new tag", "error", err) + p.logger.ErrorContext(ctx, "fail to insert new tag", "error", err) return nil, err } } - newTagIDs = append(newTagIDs, tag.ID) } + return newTagIDs, nil +} - for tagID, _ := range currentTags { +func (p *Post) deleteOldTags(ctx context.Context, tx *sql.Tx, id_post int, currentTags map[int]*blog.Tag, newTagIDs []int) error { + log := p.logger.With("method", "UpdatePost") + for tagID := range currentTags { if !contains(newTagIDs, tagID) { _, err := tx.ExecContext(ctx, "DELETE FROM post_tags WHERE id_post = $1 AND id_tag = $2", id_post, tagID) if err != nil { log.ErrorContext(ctx, fmt.Sprintf("fail to delete tag %d from post %d", tagID, id_post), "error", err) - return nil, err + return err } } } + return nil +} +func (p *Post) addNewTags(ctx context.Context, tx *sql.Tx, id_post int, newTagIDs []int) error { + log := p.logger.With("method", "UpdatePost") for _, tagID := range newTagIDs { - var count int err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM post_tags WHERE id_post = $1 AND id_tag = $2", id_post, tagID).Scan(&count) if err != nil { - log.ErrorContext(ctx, fmt.Sprintf("fail to checking if post is already linked with tag: %d", tagID), "error", err) - return nil, err + log.ErrorContext(ctx, fmt.Sprintf("fail to check if post is already linked with tag: %d", tagID), "error", err) + return err } if count == 0 { _, err := tx.ExecContext(ctx, "INSERT INTO post_tags(id_post, id_tag) VALUES($1, $2)", id_post, tagID) if err != nil { log.ErrorContext(ctx, fmt.Sprintf("fail to insert post_tag for post %d and tag %d", id_post, tagID), "error", err) - return nil, err + return err } } } - - query = ` - SELECT t.id, t.name - FROM tag t - JOIN post_tags pt ON pt.id_tag = t.id - WHERE pt.id_post = $1 - ` - rows, err = tx.QueryContext(ctx, query, id_post) - if err != nil { - log.ErrorContext(ctx, fmt.Sprintf("fail getting updated tags for post %d", id_post), "error", err) - return nil, err - } - defer rows.Close() - - var updatedTags []*blog.Tag - for rows.Next() { - var tag blog.Tag - if err := rows.Scan(&tag.ID, &tag.Name); err != nil { - log.ErrorContext(ctx, "fail scanning updated tag", "error", err) - return nil, err - } - updatedTags = append(updatedTags, &tag) - } - - return updatedTags, nil + return nil } func contains(slice []int, val int) bool {