From c7641c0d5bc85d44c57690d046a2f85029dfbd37 Mon Sep 17 00:00:00 2001 From: galyym Date: Tue, 30 Jul 2024 00:05:22 +0500 Subject: [PATCH 01/23] update: homework exercise 1 --- exercise1/problem1/main.go | 8 +++++++- exercise1/problem10/main.go | 14 +++++++++++++- exercise1/problem2/main.go | 6 +++++- exercise1/problem3/main.go | 8 +++++++- exercise1/problem4/main.go | 14 +++++++++++++- exercise1/problem5/main.go | 11 ++++++++++- exercise1/problem6/main.go | 18 +++++++++++++++++- exercise1/problem7/main.go | 20 +++++++++++++++++++- exercise1/problem8/main.go | 11 ++++++++++- exercise1/problem9/main.go | 12 +++++++++--- 10 files changed, 110 insertions(+), 12 deletions(-) diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..09bc0dcd 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,9 @@ package main -func addUp() {} +func addUp(num int) int { + var result int + for i := 1; i <= num; i++ { + result = result + i + } + return result +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..6cdd428e 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,15 @@ package main -func sum() {} +import ( + "errors" + "strconv" +) + +func sum(a string, b string) (string, error) { + sA, errA := strconv.Atoi(a) + sB, errB := strconv.Atoi(b) + if errA != nil || errB != nil { + return "", errors.Join(errA, errB) + } + return strconv.Itoa(sA + sB), nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..98d166db 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,7 @@ package main -func binary() {} +import "fmt" + +func binary(num int) string { + return fmt.Sprintf("%b", num) +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..c959ef96 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,9 @@ package main -func numberSquares() {} +func numberSquares(num int) int { + var result int + for i := 1; i <= num; i++ { + result = result + (i * i) + } + return result +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..602dba07 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,15 @@ package main -func detectWord() {} +import ( + "unicode" +) + +func detectWord(crowd string) string { + var result string + for _, s := range crowd { + if !unicode.IsUpper(s) { + result = result + string(s) + } + } + return result +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..db2b8c81 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,12 @@ package main -func potatoes() {} +func potatoes(crowd string) int { + var result int + strL := len("potato") + for i := 0; i <= len(crowd)-strL; i++ { + if crowd[i:i+strL] == "potato" { + result++ + } + } + return result +} diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..da650cd2 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,19 @@ package main -func emojify() {} +import "strings" + +var emoji = map[string]string{ + "smile": "🙂", + "grin": "😀", + "sad": "😥", + "mad": "😠", +} + +func emojify(text string) string { + for i, v := range emoji { + if strings.Contains(text, i) { + text = strings.Replace(text, i, v, 1) + } + } + return text +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..02fc8aa9 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,21 @@ package main -func highestDigit() {} +import "slices" + +func highestDigit(num int) int { + if num != 0 { + numS := ItoSlice(num) + return slices.Max(numS) + } + return 0 +} + +func ItoSlice(n int) []int { + var numS []int + for n >= 1 { + i := n % 10 + numS = append(numS, i) + n = n / 10 + } + return numS +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..2d43c13a 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,12 @@ package main -func countVowels() {} +func countVowels(text string) int { + var count int + for _, v := range text { + switch v { + case 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U': + count++ + } + } + return count +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..d6870336 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,13 @@ package main -func bitwiseAND() {} +func bitwiseAND(a int, b int) int { + return a & b +} -func bitwiseOR() {} +func bitwiseOR(a int, b int) int { + return a | b +} -func bitwiseXOR() {} +func bitwiseXOR(a int, b int) int { + return a ^ b +} From b0848068ca4760557e58997ef52b131e237af1e4 Mon Sep 17 00:00:00 2001 From: galyym Date: Sun, 29 Sep 2024 00:50:53 +0500 Subject: [PATCH 02/23] Kemalatdin Galym --- exercise1/problem1/main.go | 6 ++--- exercise2/problem1/problem1.go | 9 ++++++- exercise2/problem10/problem10.go | 10 +++++++- exercise2/problem11/problem11.go | 12 ++++++++- exercise2/problem12/problem12.go | 31 ++++++++++++++++++++++- exercise2/problem2/problem2.go | 13 +++++++++- exercise2/problem3/problem3.go | 40 ++++++++++++++++++++++++++++- exercise2/problem4/problem4.go | 9 ++++++- exercise2/problem5/problem5.go | 43 +++++++++++++++++++++++++++++++- exercise2/problem6/problem6.go | 10 +++++++- exercise2/problem7/problem7.go | 3 ++- exercise2/problem8/problem8.go | 9 +++---- exercise2/problem9/problem9.go | 10 +++++++- 13 files changed, 186 insertions(+), 19 deletions(-) diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index 09bc0dcd..bb6e14e0 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,9 +1,9 @@ package main func addUp(num int) int { - var result int + sum := 0 for i := 1; i <= num; i++ { - result = result + i + sum = sum + i } - return result + return sum } diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..c3fd2632 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,11 @@ package problem1 -func isChangeEnough() { +var cents = []float32{0.25, 0.10, 0.05, 0.01} + +func isChangeEnough(changes [4]int, total float32) bool { + var sum float32 + for key, index := range changes { + sum += cents[key] * float32(index) + } + return sum >= total } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..3d42052b 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,11 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(brand string) func(int)) { + result := make(map[string]int) + return result, func(brand string) func(int) { + result[brand] = 0 + return func(i int) { + result[brand] = result[brand] + i + } + } +} diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..b9812a9a 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,13 @@ package problem11 -func removeDups() {} +import "slices" + +func removeDups[T comparable](nums []T) []T { + result := make([]T, 0) + for _, v := range nums { + if !slices.Contains(result, v) { + result = append(result, v) + } + } + return result +} diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..15a5e185 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,32 @@ package problem11 -func keysAndValues() {} +import ( + "fmt" + "sort" +) + +func keysAndValues[K comparable, V any](m map[K]V) ([]K, []V) { + if m == nil { + return []K{}, []V{} + } + + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + sort.Slice(keys, func(i, j int) bool { + return stringify(keys[i]) < stringify(keys[j]) + }) + + values := make([]V, len(keys)) + for i, k := range keys { + values[i] = m[k] + } + + return keys, values +} + +func stringify[T any](v T) string { + return fmt.Sprintf("%v", v) +} diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..2db8363f 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,15 @@ package problem2 -func capitalize() { +import "strings" + +func capitalize(names []string) []string { + for i, v := range names { + if v == "" { + continue + } + firstLetter := strings.ToUpper(string(v[0])) + lowerS := strings.ToLower(v[1:]) + names[i] = firstLetter + lowerS + } + return names } diff --git a/exercise2/problem3/problem3.go b/exercise2/problem3/problem3.go index f183fafb..871cd318 100644 --- a/exercise2/problem3/problem3.go +++ b/exercise2/problem3/problem3.go @@ -9,5 +9,43 @@ const ( lr dir = "lr" ) -func diagonalize() { +func diagonalize(n int, d dir) [][]int { + matrix := make([][]int, n) + switch d { + case ul: + count := 0 + getMatrix(count, matrix, n, true, true) + break + case ll: + count := n - 1 + getMatrix(count, matrix, n, true, false) + break + case ur: + count := n - 1 + getMatrix(count, matrix, n, false, true) + break + case lr: + count := (n - 1) * 2 + getMatrix(count, matrix, n, false, false) + break + } + return matrix +} + +func getMatrix(count int, matrix [][]int, n int, o bool, countChecker bool) { + for i := range n { + matrix[i] = make([]int, n) + for j := range n { + if o { + matrix[i][j] = count + j + } else { + matrix[i][j] = count - j + } + } + if countChecker { + count++ + } else { + count-- + } + } } diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..c31d499c 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,11 @@ package problem4 -func mapping() { +import "strings" + +func mapping(inp []string) map[string]string { + intM := make(map[string]string) + for _, v := range inp { + intM[v] = strings.ToUpper(v) + } + return intM } diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..c16c6ad6 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,45 @@ package problem5 -func products() { +import "sort" + +type Catalog struct { + Key string + Value int +} + +type CatalogList []Catalog + +func (c CatalogList) Len() int { + return len(c) +} + +func (c CatalogList) Less(i, j int) bool { + if c[i].Value == c[j].Value { + return c[i].Key < c[j].Key + } + return c[i].Value > c[j].Value +} + +func (c CatalogList) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +func products(catalog map[string]int, minPrice int) []string { + prices := make(CatalogList, len(catalog)) + i := 0 + + for k, v := range catalog { + if v > minPrice { + prices[i] = Catalog{k, v} + i++ + } + } + sort.Sort(prices) + result := make([]string, 0) + for _, v := range prices { + if v.Key != "" { + result = append(result, v.Key) + } + } + return result } diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..384774ae 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,12 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(first []int, second []int, sum int) bool { + for _, fv := range first { + for _, sv := range second { + if fv+sv == sum { + return true + } + } + } + return false } diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..b6cdf173 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,5 @@ package problem7 -func swap() { +func swap(a *int, b *int) { + *a, *b = *b, *a } diff --git a/exercise2/problem8/problem8.go b/exercise2/problem8/problem8.go index 9389d3b0..b4086870 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -4,13 +4,12 @@ func simplify(list []string) map[string]int { var indMap 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 } } diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..372cd80e 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,11 @@ package problem9 -func factory() {} +func factory(multiple int) func(nums ...int) []int { + return func(nums ...int) []int { + result := make([]int, 0) + for _, v := range nums { + result = append(result, v*multiple) + } + return result + } +} From ddf48daa7857d3c74cfe4364cfbde89f84d4cd62 Mon Sep 17 00:00:00 2001 From: galyym Date: Mon, 7 Oct 2024 03:04:19 +0500 Subject: [PATCH 03/23] Kemalatdin Galym exercise 3 --- exercise3/problem1/problem1.go | 32 ++++++++++++++- exercise3/problem5/problem5.go | 14 ++++++- exercise3/problem6/problem6.go | 30 ++++++++++++-- exercise3/problem7/problem7.go | 45 +++++++++++++++++++++ main.go | 71 ++++++++++++++++++++++++++++++++++ 5 files changed, 187 insertions(+), 5 deletions(-) diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..d4771545 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,33 @@ package problem1 -type Queue struct{} +import "slices" + +type Queue struct { + vals []any +} + +func (q *Queue) Enqueue(val any) { + q.vals = append(q.vals, val) +} + +func (q *Queue) Dequeue() ([]any, error) { + valLen := len(q.vals) + q.vals = slices.Delete(q.vals, valLen-1, valLen) + return q.vals, nil +} + +func (q *Queue) Peek() any { + valLen := len(q.vals) - 1 + return q.vals[valLen] +} + +func (q *Queue) Size() int { + return len(q.vals) +} + +func (q *Queue) IsEmpty() bool { + if len(q.vals) > 0 { + return false + } + return true +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..d03c0324 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,15 @@ package problem5 -type Person struct{} +type Person struct { + name string + age int +} + +func (m *Person) compareAge(person *Person) string { + if m.age < person.age { + return person.name + " is older than me." + } else if m.age > person.age { + return person.name + " is younger than me." + } + return person.name + " is the same age as me." +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..32fad2a3 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,31 @@ package problem6 -type Animal struct{} +type Len interface { + Lens() int +} -type Insect struct{} +type Animal struct { + name string + legsNum int +} -func sumOfAllLegsNum() {} +func (a *Animal) Lens() int { + return a.legsNum +} + +type Insect struct { + name string + legsNum int +} + +func (i *Insect) Lens() int { + return i.legsNum +} + +func sumOfAllLegsNum(lens ...Len) int { + sum := 0 + for _, v := range lens { + sum = sum + v.Lens() + } + return sum +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..d4ea836d 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,55 @@ package problem7 +import "fmt" + +type Drawable interface { + transaction(sum int) +} + +type Send interface { + sendPackage(name string) +} + type BankAccount struct { + name string + balance int +} + +func (b *BankAccount) transaction(sum int) { + b.balance = b.balance - sum } type FedexAccount struct { + name string + packages []string +} + +func (s *FedexAccount) sendPackage(name string) { + s.packages = append(s.packages, fmt.Sprintf("%s send package to %s", s.name, name)) } type KazPostAccount struct { + name string + balance int + packages []string +} + +func (b *KazPostAccount) transaction(sum int) { + b.balance = b.balance - sum +} + +func (s *KazPostAccount) sendPackage(name string) { + s.packages = append(s.packages, fmt.Sprintf("%s send package to %s", s.name, name)) +} + +func withdrawMoney(sum int, accounts ...Drawable) { + for _, v := range accounts { + v.transaction(sum) + } +} + +func sendPackagesTo(name string, accounts ...Send) { + for _, v := range accounts { + v.sendPackage(name) + } } diff --git a/main.go b/main.go index 06ab7d0f..76c1cef4 100644 --- a/main.go +++ b/main.go @@ -1 +1,72 @@ package main + +import ( + "fmt" + "slices" +) + +type Queue struct { + vals []any +} + +func (q *Queue) Enqueue(val any) { + q.vals = append(q.vals, val) +} + +func (q *Queue) Dequeue() (any, error) { + valLen := len(q.vals) + queueElement := q.vals[valLen-1] + q.vals = slices.Delete(q.vals, valLen-1, valLen) + return queueElement, nil +} + +func (q *Queue) Peek() any { + valLen := len(q.vals) - 1 + return q.vals[valLen] +} + +func (q *Queue) Size() int { + return len(q.vals) +} + +func (q *Queue) IsEmpty() bool { + if len(q.vals) > 0 { + return false + } + return true +} + +func main() { + queue := Queue{} + queue.Enqueue(1) + queue.Enqueue(5) + queue.Enqueue(9) + fmt.Println(queue) + queue.Dequeue() + fmt.Println(queue) + e := queue.Peek() + fmt.Println(e) + size := queue.Size() + fmt.Println(size) + fmt.Println(queue.IsEmpty()) + + table := []struct { + vals []any + }{ + {[]any{1, 2, 3}}, + {[]any{"1", "2", "3", "4"}}, + {[]any{true, false}}, + } + + for _, r := range table { + for _, v := range r.vals { + queue.Enqueue(v) + } + for i := range r.vals { + d, _ := queue.Dequeue() + if d != r.vals[i] { + fmt.Println("Error") + } + } + } +} From d859c19818fada6276308063312aa36a30d209aa Mon Sep 17 00:00:00 2001 From: tobirama Date: Mon, 7 Oct 2024 18:43:16 +0500 Subject: [PATCH 04/23] update: done problem 1 --- exercise3/problem1/problem1.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d4771545..2dc2fa74 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,6 +1,6 @@ package problem1 -import "slices" +import "errors" type Queue struct { vals []any @@ -10,15 +10,20 @@ func (q *Queue) Enqueue(val any) { q.vals = append(q.vals, val) } -func (q *Queue) Dequeue() ([]any, error) { - valLen := len(q.vals) - q.vals = slices.Delete(q.vals, valLen-1, valLen) - return q.vals, nil +func (q *Queue) Dequeue() (any, error) { + if len(q.vals) == 0 { + return nil, errors.New("queue is empty") + } + first := q.vals[0] + q.vals = append(q.vals[:0], q.vals[1:]...) + return first, nil } -func (q *Queue) Peek() any { - valLen := len(q.vals) - 1 - return q.vals[valLen] +func (q *Queue) Peek() (any, error) { + if len(q.vals) == 0 { + return nil, errors.New("queue empty") + } + return q.vals[0], nil } func (q *Queue) Size() int { From e1089f07f697fdd1b726842f54c49ba100639ede Mon Sep 17 00:00:00 2001 From: tobirama Date: Mon, 7 Oct 2024 18:55:08 +0500 Subject: [PATCH 05/23] update: done problem 2 --- exercise3/problem2/problem2.go | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..c4a88265 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 { + vals []any +} + +func (s *Stack) Push(val any) { + s.vals = append(s.vals, val) +} + +func (s *Stack) Pop() (any, error) { + if len(s.vals) == 0 { + return nil, errors.New("stack is empty") + } + topElement := s.vals[len(s.vals)-1] + s.vals = s.vals[:len(s.vals)-1] + return topElement, nil +} + +func (s *Stack) Peek() (any, error) { + if len(s.vals) == 0 { + return nil, errors.New("stack is empty") + } + return s.vals[len(s.vals)-1], nil +} + +func (s *Stack) Size() int { + return len(s.vals) +} + +func (s *Stack) IsEmpty() bool { + return len(s.vals) == 0 +} From d27f30c93887b9308532ef4298f5e451807424cf Mon Sep 17 00:00:00 2001 From: tobirama Date: Mon, 7 Oct 2024 21:04:52 +0500 Subject: [PATCH 06/23] update: problem 3 --- exercise3/problem3/problem3.go | 90 ++++++++++++++++++++++++++++++- main.go | 99 +++++++++++++++++++--------------- 2 files changed, 144 insertions(+), 45 deletions(-) diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..720fb47c 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,91 @@ package problem3 -type Set struct{} +import "slices" + +type Set struct { + vals []any + size int +} + +func NewSet() *Set { + return &Set{} +} + +func (s *Set) Add(val any) { + s.vals = append(s.vals, val) + s.size += 1 +} + +func (s *Set) Remove(val any) { + key := slices.Index(s.vals, val) + if key >= 0 { + s.vals = slices.Delete(s.vals, key, key+1) + s.size -= 1 + } +} + +func (s *Set) IsEmpty() bool { + return s.size == 0 +} + +func (s *Set) Size() int { + return s.size +} + +func (s *Set) List() []any { + return s.vals +} + +func (s *Set) Has(val any) bool { + if slices.Contains(s.vals, val) { + return true + } + return false +} + +func (s *Set) Copy() *Set { + copySlice := s + return copySlice +} + +func (s *Set) Difference(data *Set) Set { + for _, v := range s.vals { + if data.Has(v) { + s.Remove(v) + } + } + return *s +} + +func (s *Set) IsSubset(data *Set) bool { + for _, v := range data.vals { + if !s.Has(v) { + return false + } + } + return true +} + +func Union(data ...*Set) Set { + slice := NewSet() + for _, s := range data { + for _, v := range s.vals { + if !s.Has(v) { + slice.Add(v) + } + } + } + return *slice +} + +func Intersect(data ...*Set) Set { + slice := NewSet() + for _, s := range data { + for _, v := range s.vals { + if !s.Has(v) { + slice.Remove(v) + } + } + } + return *slice +} diff --git a/main.go b/main.go index 76c1cef4..74200f76 100644 --- a/main.go +++ b/main.go @@ -5,68 +5,79 @@ import ( "slices" ) -type Queue struct { +func main() { + set1 := Set{vals: []any{1, 2, 3, 4}, size: 4} + set2 := Set{vals: []any{1, 6, 7}, size: 2} + diff := set1.Union(set2) + fmt.Println(diff) +} + +type Set struct { vals []any + size int +} + +func (s *Set) Add(val any) { + s.vals = append(s.vals, val) + s.size += 1 } -func (q *Queue) Enqueue(val any) { - q.vals = append(q.vals, val) +func (s *Set) Remove(val any) { + key := slices.Index(s.vals, val) + if key >= 0 { + s.vals = slices.Delete(s.vals, key, key+1) + s.size -= 1 + } } -func (q *Queue) Dequeue() (any, error) { - valLen := len(q.vals) - queueElement := q.vals[valLen-1] - q.vals = slices.Delete(q.vals, valLen-1, valLen) - return queueElement, nil +func (s *Set) IsEmpty() bool { + return s.size == 0 } -func (q *Queue) Peek() any { - valLen := len(q.vals) - 1 - return q.vals[valLen] +func (s *Set) Size() int { + return s.size } -func (q *Queue) Size() int { - return len(q.vals) +func (s *Set) List() []any { + return s.vals } -func (q *Queue) IsEmpty() bool { - if len(q.vals) > 0 { - return false +func (s *Set) Has(val any) bool { + if slices.Contains(s.vals, val) { + return true } - return true + return false } -func main() { - queue := Queue{} - queue.Enqueue(1) - queue.Enqueue(5) - queue.Enqueue(9) - fmt.Println(queue) - queue.Dequeue() - fmt.Println(queue) - e := queue.Peek() - fmt.Println(e) - size := queue.Size() - fmt.Println(size) - fmt.Println(queue.IsEmpty()) +func (s *Set) Copy() []any { + copySlice := make([]any, 0) + copy(copySlice, s.vals) + return copySlice +} - table := []struct { - vals []any - }{ - {[]any{1, 2, 3}}, - {[]any{"1", "2", "3", "4"}}, - {[]any{true, false}}, +func (s *Set) Difference(data Set) Set { + for _, v := range s.vals { + if data.Has(v) { + s.Remove(v) + } } + return *s +} - for _, r := range table { - for _, v := range r.vals { - queue.Enqueue(v) +func (s *Set) IsSubset(data Set) bool { + for _, v := range data.vals { + if !s.Has(v) { + return false } - for i := range r.vals { - d, _ := queue.Dequeue() - if d != r.vals[i] { - fmt.Println("Error") - } + } + return true +} + +func (s *Set) Union(data Set) Set { + for _, v := range data.vals { + if !s.Has(v) { + s.Add(v) } } + return *s } From 6a6baa37dfe595310d2990a846ddb390f69f4a59 Mon Sep 17 00:00:00 2001 From: galyym Date: Tue, 8 Oct 2024 03:12:37 +0500 Subject: [PATCH 07/23] KemalatdinGalym-exercise3 --- exercise3/problem3/problem3.go | 45 ++++++++++------ exercise3/problem4/problem4.go | 95 +++++++++++++++++++++++++++++++++- main.go | 83 ++--------------------------- 3 files changed, 128 insertions(+), 95 deletions(-) diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index 720fb47c..7ebf4124 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,6 +1,8 @@ package problem3 -import "slices" +import ( + "slices" +) type Set struct { vals []any @@ -8,12 +10,14 @@ type Set struct { } func NewSet() *Set { - return &Set{} + return &Set{[]any{}, 0} } func (s *Set) Add(val any) { - s.vals = append(s.vals, val) - s.size += 1 + if !s.Has(val) { + s.vals = append(s.vals, val) + s.size += 1 + } } func (s *Set) Remove(val any) { @@ -43,23 +47,25 @@ func (s *Set) Has(val any) bool { return false } -func (s *Set) Copy() *Set { - copySlice := s - return copySlice +func (s Set) Copy() *Set { + newVals := make([]any, len(s.vals)) + copy(newVals, s.vals) + return &Set{vals: newVals, size: s.size} } func (s *Set) Difference(data *Set) Set { + result := s.Copy() for _, v := range s.vals { if data.Has(v) { - s.Remove(v) + result.Remove(v) } } - return *s + return *result } func (s *Set) IsSubset(data *Set) bool { - for _, v := range data.vals { - if !s.Has(v) { + for _, v := range s.vals { + if !data.Has(v) { return false } } @@ -70,7 +76,7 @@ func Union(data ...*Set) Set { slice := NewSet() for _, s := range data { for _, v := range s.vals { - if !s.Has(v) { + if !slice.Has(v) { slice.Add(v) } } @@ -79,13 +85,22 @@ func Union(data ...*Set) Set { } func Intersect(data ...*Set) Set { + if len(data) == 0 { + return Set{} + } + slice := NewSet() - for _, s := range data { - for _, v := range s.vals { + for _, v := range data[0].vals { + foundInAll := true + for _, s := range data[1:] { if !s.Has(v) { - slice.Remove(v) + foundInAll = false + break } } + if foundInAll { + slice.Add(v) + } } return *slice } diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..f8c9c113 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,96 @@ package problem4 -type LinkedList struct{} +import "fmt" + +type Element[T comparable] struct { + value T + next *Element[T] +} + +type LinkedList[T comparable] struct { + head *Element[T] + size int +} + +func (ll *LinkedList[T]) Add(element *Element[T]) { + if ll.head == nil { + ll.head = element + } else { + current := ll.head + for current.next != nil { + current = current.next + } + current.next = element + } + ll.size++ +} + +func (ll *LinkedList[T]) Insert(element *Element[T], position int) error { + if position < 0 || position > ll.size { + return fmt.Errorf("invalid position") + } + if position == 0 { + element.next = ll.head + ll.head = element + ll.size++ + return nil + } + current := ll.head + for i := 0; i < position-1; i++ { + current = current.next + } + element.next = current.next + current.next = element + ll.size++ + return nil +} + +func (ll *LinkedList[T]) Delete(element *Element[T]) error { + if ll.head == nil { + return fmt.Errorf("list is empty") + } + if ll.head.value == element.value { + ll.head = ll.head.next + ll.size-- + return nil + } + current := ll.head + for current.next != nil { + if current.next.value == element.value { + current.next = current.next.next + ll.size-- + return nil + } + current = current.next + } + return fmt.Errorf("element not found") +} + +func (ll *LinkedList[T]) Find(value T) (*Element[T], error) { + current := ll.head + for current != nil { + if current.value == value { + return current, nil + } + current = current.next + } + return nil, fmt.Errorf("element not found") +} + +func (ll *LinkedList[T]) List() []T { + result := make([]T, 0, ll.size) + current := ll.head + for current != nil { + result = append(result, current.value) + current = current.next + } + return result +} + +func (ll *LinkedList[T]) Size() int { + return ll.size +} + +func (ll *LinkedList[T]) IsEmpty() bool { + return ll.size == 0 +} diff --git a/main.go b/main.go index 74200f76..a0aa4fda 100644 --- a/main.go +++ b/main.go @@ -1,83 +1,8 @@ package main -import ( - "fmt" - "slices" -) - func main() { - set1 := Set{vals: []any{1, 2, 3, 4}, size: 4} - set2 := Set{vals: []any{1, 6, 7}, size: 2} - diff := set1.Union(set2) - fmt.Println(diff) -} - -type Set struct { - vals []any - size int -} - -func (s *Set) Add(val any) { - s.vals = append(s.vals, val) - s.size += 1 -} - -func (s *Set) Remove(val any) { - key := slices.Index(s.vals, val) - if key >= 0 { - s.vals = slices.Delete(s.vals, key, key+1) - s.size -= 1 - } -} - -func (s *Set) IsEmpty() bool { - return s.size == 0 -} - -func (s *Set) Size() int { - return s.size -} - -func (s *Set) List() []any { - return s.vals -} - -func (s *Set) Has(val any) bool { - if slices.Contains(s.vals, val) { - return true - } - return false -} - -func (s *Set) Copy() []any { - copySlice := make([]any, 0) - copy(copySlice, s.vals) - return copySlice -} - -func (s *Set) Difference(data Set) Set { - for _, v := range s.vals { - if data.Has(v) { - s.Remove(v) - } - } - return *s -} - -func (s *Set) IsSubset(data Set) bool { - for _, v := range data.vals { - if !s.Has(v) { - return false - } - } - return true -} - -func (s *Set) Union(data Set) Set { - for _, v := range data.vals { - if !s.Has(v) { - s.Add(v) - } - } - return *s + //set1 := Set{vals: []any{1, 2, 3, 1, 2}, size: 5} + //set2 := Set{vals: []any{1, 3}, size: 2} + //diff := set1.IsSubset(set2) + //fmt.Println(diff) } From 7ee31dcfc6ae24cd119dfc3505186a770156195e Mon Sep 17 00:00:00 2001 From: tobirama Date: Wed, 16 Oct 2024 12:59:42 +0500 Subject: [PATCH 08/23] feat: bot http ping route --- exercise4/bot/internal/api/handler/main.go | 25 +++++++++++++ exercise4/bot/internal/api/main.go | 41 ++++++++++++++++++++++ exercise4/bot/internal/api/router/main.go | 14 ++++++++ exercise4/bot/main.go | 10 +++++- 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 exercise4/bot/internal/api/handler/main.go create mode 100644 exercise4/bot/internal/api/main.go create mode 100644 exercise4/bot/internal/api/router/main.go diff --git a/exercise4/bot/internal/api/handler/main.go b/exercise4/bot/internal/api/handler/main.go new file mode 100644 index 00000000..bfbd012d --- /dev/null +++ b/exercise4/bot/internal/api/handler/main.go @@ -0,0 +1,25 @@ +package handler + +import ( + "fmt" + "log/slog" + "net/http" +) + +type Handler struct { + name string +} + +func New() *Handler { + return &Handler{} +} + +func (h *Handler) Status(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := slog.With( + "handler", "Status", + "path", r.URL.Path, + ) + fmt.Println(ctx, log) + w.WriteHeader(http.StatusOK) +} diff --git a/exercise4/bot/internal/api/main.go b/exercise4/bot/internal/api/main.go new file mode 100644 index 00000000..0e36532a --- /dev/null +++ b/exercise4/bot/internal/api/main.go @@ -0,0 +1,41 @@ +package api + +import ( + "context" + "errors" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api/router" + "log/slog" + "net" + "net/http" +) + +type Api struct { + srv *http.Server +} + +func New() *Api { + return &Api{} +} + +func (api *Api) Start(ctx context.Context, port string) error { + r := router.New() + + api.srv = &http.Server{ + Addr: ":" + port, + Handler: r, + BaseContext: func(net.Listener) context.Context { return ctx }, + } + + slog.InfoContext( + ctx, + "starting bot", + "port", port, + ) + + if err := api.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.ErrorContext(ctx, "service error", "error", err) + return err + } + + return nil +} diff --git a/exercise4/bot/internal/api/router/main.go b/exercise4/bot/internal/api/router/main.go new file mode 100644 index 00000000..04e151b5 --- /dev/null +++ b/exercise4/bot/internal/api/router/main.go @@ -0,0 +1,14 @@ +package router + +import ( + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api/handler" + "net/http" +) + +func New() *http.ServeMux { + han := handler.New() + mux := http.NewServeMux() + + mux.Handle("GET /player1/ping", http.HandlerFunc(han.Status)) + return mux +} diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..d2bda237 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api" + "log/slog" "os" "os/signal" "syscall" @@ -13,7 +15,13 @@ func main() { ready := startServer() <-ready - // TODO after server start + port := os.Getenv("PORT_API") + a := api.New() + + if err := a.Start(ctx, port); err != nil { + slog.ErrorContext(ctx, "bot start error", "error", err) + os.Exit(1) + } stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) From 192d4b6534291d543d09949d24dc8e959386ac9b Mon Sep 17 00:00:00 2001 From: tobirama Date: Fri, 18 Oct 2024 17:36:29 +0500 Subject: [PATCH 09/23] Revert "feat: bot http ping route" This reverts commit 7ee31dcfc6ae24cd119dfc3505186a770156195e. --- exercise4/bot/internal/api/handler/main.go | 25 ------------- exercise4/bot/internal/api/main.go | 41 ---------------------- exercise4/bot/internal/api/router/main.go | 14 -------- exercise4/bot/main.go | 10 +----- 4 files changed, 1 insertion(+), 89 deletions(-) delete mode 100644 exercise4/bot/internal/api/handler/main.go delete mode 100644 exercise4/bot/internal/api/main.go delete mode 100644 exercise4/bot/internal/api/router/main.go diff --git a/exercise4/bot/internal/api/handler/main.go b/exercise4/bot/internal/api/handler/main.go deleted file mode 100644 index bfbd012d..00000000 --- a/exercise4/bot/internal/api/handler/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package handler - -import ( - "fmt" - "log/slog" - "net/http" -) - -type Handler struct { - name string -} - -func New() *Handler { - return &Handler{} -} - -func (h *Handler) Status(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - log := slog.With( - "handler", "Status", - "path", r.URL.Path, - ) - fmt.Println(ctx, log) - w.WriteHeader(http.StatusOK) -} diff --git a/exercise4/bot/internal/api/main.go b/exercise4/bot/internal/api/main.go deleted file mode 100644 index 0e36532a..00000000 --- a/exercise4/bot/internal/api/main.go +++ /dev/null @@ -1,41 +0,0 @@ -package api - -import ( - "context" - "errors" - "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api/router" - "log/slog" - "net" - "net/http" -) - -type Api struct { - srv *http.Server -} - -func New() *Api { - return &Api{} -} - -func (api *Api) Start(ctx context.Context, port string) error { - r := router.New() - - api.srv = &http.Server{ - Addr: ":" + port, - Handler: r, - BaseContext: func(net.Listener) context.Context { return ctx }, - } - - slog.InfoContext( - ctx, - "starting bot", - "port", port, - ) - - if err := api.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - slog.ErrorContext(ctx, "service error", "error", err) - return err - } - - return nil -} diff --git a/exercise4/bot/internal/api/router/main.go b/exercise4/bot/internal/api/router/main.go deleted file mode 100644 index 04e151b5..00000000 --- a/exercise4/bot/internal/api/router/main.go +++ /dev/null @@ -1,14 +0,0 @@ -package router - -import ( - "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api/handler" - "net/http" -) - -func New() *http.ServeMux { - han := handler.New() - mux := http.NewServeMux() - - mux.Handle("GET /player1/ping", http.HandlerFunc(han.Status)) - return mux -} diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index d2bda237..64f9e0a3 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -2,8 +2,6 @@ package main import ( "context" - "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api" - "log/slog" "os" "os/signal" "syscall" @@ -15,13 +13,7 @@ func main() { ready := startServer() <-ready - port := os.Getenv("PORT_API") - a := api.New() - - if err := a.Start(ctx, port); err != nil { - slog.ErrorContext(ctx, "bot start error", "error", err) - os.Exit(1) - } + // TODO after server start stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) From 2c67699b30533ebe4d2a5101f3e0d1c8b0abe1ad Mon Sep 17 00:00:00 2001 From: tobirama Date: Fri, 18 Oct 2024 18:53:51 +0500 Subject: [PATCH 10/23] feat: send join request --- exercise4/bot/internal/client/main.go | 34 +++++++++++++++++++++++++++ exercise4/bot/main.go | 6 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 exercise4/bot/internal/client/main.go diff --git a/exercise4/bot/internal/client/main.go b/exercise4/bot/internal/client/main.go new file mode 100644 index 00000000..2dff2cae --- /dev/null +++ b/exercise4/bot/internal/client/main.go @@ -0,0 +1,34 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" +) + +type joinRequest struct { + name string `json:"name"` + url string `json:"url"` +} + +func SendJoinRequest(ctx context.Context, name string, url string) { + data := joinRequest{name, url} + + dataJson, err := json.Marshal(data) + if err != nil { + slog.Error("marshal error", data) + } + resp, err := http.NewRequest("POST", url, bytes.NewBuffer(dataJson)) + resp.Header.Set("Content-Type", "application/json") + if err != nil { + slog.Error("new request error", data) + } + defer resp.Body.Close() + + response, err := io.ReadAll(resp.Body) + fmt.Println(string(response)) +} diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..fe956900 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/client" "os" "os/signal" "syscall" @@ -13,7 +14,10 @@ func main() { ready := startServer() <-ready - // TODO after server start + name := os.Getenv("NAME") + port := os.Getenv("PORT") + url := "http://locolhost:" + port + client.SendJoinRequest(ctx, name, url) stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) From d7b47bf74d133b625a3fa07ed9a7fc27589406a4 Mon Sep 17 00:00:00 2001 From: galyym Date: Sun, 20 Oct 2024 16:30:27 +0500 Subject: [PATCH 11/23] feat: ping, move --- exercise4/bot/internal/client/main.go | 34 ++++++++++++++------ exercise4/bot/main.go | 5 +-- exercise4/bot/server.go | 45 ++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/exercise4/bot/internal/client/main.go b/exercise4/bot/internal/client/main.go index 2dff2cae..4501960c 100644 --- a/exercise4/bot/internal/client/main.go +++ b/exercise4/bot/internal/client/main.go @@ -8,25 +8,41 @@ import ( "io" "log/slog" "net/http" + "os" ) -type joinRequest struct { - name string `json:"name"` - url string `json:"url"` -} +var ( + lcl = "http://127.0.0.1" + port = os.Getenv("PORT") + url = fmt.Sprintf("%s:%s", lcl, port) + name = os.Getenv("NAME") +) -func SendJoinRequest(ctx context.Context, name string, url string) { - data := joinRequest{name, url} +func SendJoinRequest(ctx context.Context) { + data := map[string]string{ + "name": name, + "url": url, + } dataJson, err := json.Marshal(data) if err != nil { slog.Error("marshal error", data) } - resp, err := http.NewRequest("POST", url, bytes.NewBuffer(dataJson)) - resp.Header.Set("Content-Type", "application/json") + + serverUrl := fmt.Sprintf("%s:4444/join", lcl) + req, err := http.NewRequestWithContext(ctx, "POST", serverUrl, bytes.NewBuffer(dataJson)) + if err != nil { + slog.Error("join request error", "error", err) + return + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) if err != nil { - slog.Error("new request error", data) + slog.Error("join request error do", "error", err) + return } + fmt.Println("ok") defer resp.Body.Close() response, err := io.ReadAll(resp.Body) diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index fe956900..4cac747f 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -14,10 +14,7 @@ func main() { ready := startServer() <-ready - name := os.Getenv("NAME") - port := os.Getenv("PORT") - url := "http://locolhost:" + port - client.SendJoinRequest(ctx, name, url) + client.SendJoinRequest(ctx) stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go index e6760ec5..2a4cf37a 100644 --- a/exercise4/bot/server.go +++ b/exercise4/bot/server.go @@ -1,8 +1,11 @@ package main import ( + "encoding/json" "errors" "fmt" + "io" + "math/rand" "net" "net/http" "os" @@ -10,6 +13,11 @@ import ( "time" ) +type MoveRequest struct { + Board []string `json:"board"` + Token string `json:"token"` +} + type readyListener struct { net.Listener ready chan struct{} @@ -21,6 +29,39 @@ func (l *readyListener) Accept() (net.Conn, error) { return l.Listener.Accept() } +func pingHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("pong")) +} + +func moveHandler(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + + var moveReq MoveRequest + err = json.Unmarshal(bodyBytes, &moveReq) + if err != nil { + http.Error(w, "Invalid JSON format", http.StatusBadRequest) + return + } + + cell := rand.Intn(9) + moveReq.Board[cell] = moveReq.Token + responseData, err := json.Marshal(moveReq) + if err != nil { + http.Error(w, "Failed to marshal response", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(responseData) +} + func startServer() <-chan struct{} { ready := make(chan struct{}) @@ -34,12 +75,14 @@ func startServer() <-chan struct{} { IdleTimeout: 2 * time.Minute, } + http.HandleFunc("/ping", pingHandler) + http.HandleFunc("/move", moveHandler) + go func() { err := srv.Serve(list) if !errors.Is(err, http.ErrServerClosed) { panic(err) } }() - return ready } From 9b316e747a14b9dac9e0ab4c3dcb5845c359e4a1 Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Sun, 20 Oct 2024 20:42:26 +0500 Subject: [PATCH 12/23] exercise 4. move logic --- exercise4/bot/internal/game/tic_tac_toe.go | 99 ++++++++++++++++++++++ exercise4/bot/internal/types/types.go | 10 +++ exercise4/bot/server.go | 17 ++-- 3 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 exercise4/bot/internal/game/tic_tac_toe.go create mode 100644 exercise4/bot/internal/types/types.go diff --git a/exercise4/bot/internal/game/tic_tac_toe.go b/exercise4/bot/internal/game/tic_tac_toe.go new file mode 100644 index 00000000..28846285 --- /dev/null +++ b/exercise4/bot/internal/game/tic_tac_toe.go @@ -0,0 +1,99 @@ +package game + +import "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/types" + +const ( + PlayerX = "x" + PlayerO = "o" + Empty = " " +) + +func BestMove(request *types.MoveRequest) int { + + bestScore := -1000 + bestMove := -1 + + for i := 0; i < len(request.Board); i++ { + if request.Board[i] == Empty { + request.Board[i] = request.Token + score := minimax(request.Board, 0, false, request.Token) + request.Board[i] = Empty + + if score > bestScore { + bestScore = score + bestMove = i + } + } + } + + return bestMove +} + +func minimax(board []string, depth int, isMaximizing bool, token string) int { + opponent := PlayerO + if token == PlayerO { + opponent = PlayerX + } + + score := evaluate(board, token, opponent) + if score != 0 { + return score + } + if isBoardFull(board) { + return 0 + } + + if isMaximizing { + bestScore := -1000 + for i := 0; i < len(board); i++ { + if board[i] == Empty { + board[i] = token + score := minimax(board, depth+1, false, token) + board[i] = Empty + bestScore = max(score, bestScore) + } + } + return bestScore + } else { + bestScore := 1000 + for i := 0; i < len(board); i++ { + if board[i] == Empty { + board[i] = opponent + score := minimax(board, depth+1, true, token) + board[i] = Empty + bestScore = min(score, bestScore) + } + } + return bestScore + } +} + +// Оценка игрового поля +func evaluate(board []string, player string, opponent string) int { + winPatterns := [][]int{ + {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // вертикальные линии для победы + {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // горизонтальные линии для победы + {0, 4, 8}, {2, 4, 6}, // диагонали для победы + } + + for _, pattern := range winPatterns { + if board[pattern[0]] == player && board[pattern[1]] == player && board[pattern[2]] == player { + return 10 + } + if board[pattern[0]] == opponent && board[pattern[1]] == opponent && board[pattern[2]] == opponent { + return -10 + } + } + + return 0 +} + +// Проверка, заполнено ли игровое поле +func isBoardFull(board []string) bool { + for _, cell := range board { + if cell == Empty { + return false + } + } + return true +} diff --git a/exercise4/bot/internal/types/types.go b/exercise4/bot/internal/types/types.go new file mode 100644 index 00000000..d20d5978 --- /dev/null +++ b/exercise4/bot/internal/types/types.go @@ -0,0 +1,10 @@ +package types + +type MoveRequest struct { + Board []string `json:"board"` + Token string `json:"token"` +} + +type ResponseMove struct { + Index int `json:"index"` +} diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go index 2a4cf37a..fab24a3f 100644 --- a/exercise4/bot/server.go +++ b/exercise4/bot/server.go @@ -4,8 +4,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/game" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/types" "io" - "math/rand" "net" "net/http" "os" @@ -13,11 +14,6 @@ import ( "time" ) -type MoveRequest struct { - Board []string `json:"board"` - Token string `json:"token"` -} - type readyListener struct { net.Listener ready chan struct{} @@ -42,16 +38,17 @@ func moveHandler(w http.ResponseWriter, r *http.Request) { return } - var moveReq MoveRequest + var moveReq types.MoveRequest err = json.Unmarshal(bodyBytes, &moveReq) if err != nil { http.Error(w, "Invalid JSON format", http.StatusBadRequest) return } - cell := rand.Intn(9) - moveReq.Board[cell] = moveReq.Token - responseData, err := json.Marshal(moveReq) + cell := game.BestMove(&moveReq) + resp := types.ResponseMove{Index: cell} + + responseData, err := json.Marshal(resp) if err != nil { http.Error(w, "Failed to marshal response", http.StatusInternalServerError) return From 69c49317ae63b7a39ab5cb2170ee061454fd75e4 Mon Sep 17 00:00:00 2001 From: galyym Date: Thu, 7 Nov 2024 02:13:04 +0500 Subject: [PATCH 13/23] feat: exercise5 --- exercise4/bot/go.sum | 2 ++ exercise5/problem1/problem1.go | 5 ++++- exercise5/problem3/problem3.go | 5 +++-- exercise5/problem4/problem4.go | 13 +++++++++++-- 4 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 exercise4/bot/go.sum diff --git a/exercise4/bot/go.sum b/exercise4/bot/go.sum new file mode 100644 index 00000000..a105e617 --- /dev/null +++ b/exercise4/bot/go.sum @@ -0,0 +1,2 @@ +github.com/talgat-ruby/exercises-go/exercise4/judge v0.0.0-20241019003312-fda19e97708b h1:AAi4/EE39U7ZOe9wGHNxEX3s/0agWdnJvvko6FJHuSw= +github.com/talgat-ruby/exercises-go/exercise4/judge v0.0.0-20241019003312-fda19e97708b/go.mod h1:vVB0HyOSf4pmpiPGTaPuJTVmz/vz9aOPOcodFfRV56c= diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..0bba2e88 100644 --- a/exercise5/problem1/problem1.go +++ b/exercise5/problem1/problem1.go @@ -1,9 +1,12 @@ package problem1 func incrementConcurrently(num int) int { + ch := make(chan int) + defer close(ch) go func() { num++ + ch <- 1 }() - + <-ch return num } diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..6ff68d9a 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -2,10 +2,11 @@ package problem3 func sum(a, b int) int { var c int - + ch := make(chan int) go func(a, b int) { c = a + b + ch <- c }(a, b) - + c = <-ch return c } diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index b5899ddf..fe49a3ee 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -1,16 +1,25 @@ package problem4 -func iter(ch chan<- int, nums []int) { +import "sync" + +func iter(ch chan<- int, nums []int, wg *sync.WaitGroup) { for _, n := range nums { ch <- n } + wg.Done() } func sum(nums []int) int { ch := make(chan int) + var wg sync.WaitGroup - go iter(ch, nums) + wg.Add(1) + go iter(ch, nums, &wg) + go func() { + wg.Wait() + close(ch) + }() var sum int for n := range ch { sum += n From cf97485f0937f1b0b2942720a3a5b0fb9cdd0045 Mon Sep 17 00:00:00 2001 From: galyym Date: Sat, 9 Nov 2024 15:10:04 +0500 Subject: [PATCH 14/23] feat: exercise5 --- exercise5/problem4/problem4.go | 14 +++----------- exercise5/problem5/problem5.go | 18 ++++++++++++++++-- exercise5/problem7/problem7.go | 12 +++++++++++- exercise5/problem8/problem8.go | 11 ++++++++++- main.go | 5 +---- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index fe49a3ee..f8457faa 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -1,25 +1,17 @@ package problem4 -import "sync" - -func iter(ch chan<- int, nums []int, wg *sync.WaitGroup) { +func iter(ch chan<- int, nums []int) { + defer close(ch) for _, n := range nums { ch <- n } - wg.Done() } func sum(nums []int) int { ch := make(chan int) - var wg sync.WaitGroup - wg.Add(1) - go iter(ch, nums, &wg) + go iter(ch, nums) - go func() { - wg.Wait() - close(ch) - }() var sum int for n := range ch { sum += n diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..49190e86 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,8 +1,22 @@ package problem5 -func producer() {} +func producer(list []string, ch chan<- string) { + for _, v := range list { + ch <- v + } + close(ch) +} -func consumer() {} +func consumer(ch <-chan string) string { + var text string + for v := range ch { + if text != "" { + text += " " + } + text = text + v + } + return text +} func send( words []string, diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..45416cb3 100644 --- a/exercise5/problem7/problem7.go +++ b/exercise5/problem7/problem7.go @@ -1,3 +1,13 @@ package problem7 -func multiplex(ch1 <-chan string, ch2 <-chan string) []string {} +func multiplex(ch1 <-chan string, ch2 <-chan string) []string { + var result []string + for v := range ch1 { + result = append(result, v) + } + + for v := range ch2 { + result = append(result, v) + } + return result +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..5836c465 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -4,4 +4,13 @@ import ( "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + for { + select { + case v := <-ch: + return v + case <-time.After(ttl): + return "timeout" + } + } +} diff --git a/main.go b/main.go index a0aa4fda..4a9902ab 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,5 @@ package main func main() { - //set1 := Set{vals: []any{1, 2, 3, 1, 2}, size: 5} - //set2 := Set{vals: []any{1, 3}, size: 2} - //diff := set1.IsSubset(set2) - //fmt.Println(diff) + // } From 43345e19cc95e3526618ddfc1ded9addec8048c3 Mon Sep 17 00:00:00 2001 From: galyym Date: Mon, 2 Dec 2024 02:36:28 +0500 Subject: [PATCH 15/23] feat: exercise6 --- exercise6/problem1/problem1.go | 20 ++++++++++++- exercise6/problem2/problem2.go | 21 ++++++++++++-- exercise6/problem3/problem3.go | 14 +++++++++ exercise6/problem4/problem4.go | 25 +++++++++------- exercise6/problem5/problem5.go | 22 ++++++++++---- exercise6/problem7/problem7.go | 25 ++++++++++------ exercise6/problem8/problem8.go | 23 ++++++++++++++- main.go | 52 ++++++++++++++++++++++++++++++++++ 8 files changed, 174 insertions(+), 28 deletions(-) diff --git a/exercise6/problem1/problem1.go b/exercise6/problem1/problem1.go index ee453b24..27d18154 100644 --- a/exercise6/problem1/problem1.go +++ b/exercise6/problem1/problem1.go @@ -1,9 +1,27 @@ package problem1 +import "sync" + type bankAccount struct { blnc int + m *sync.Mutex } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + m := sync.Mutex{} + return &bankAccount{blnc, &m} +} + +func (b *bankAccount) withdraw(balance int) { + b.m.Lock() + if b.blnc > 10 { + b.blnc = b.blnc - balance + } + b.m.Unlock() +} + +func (b *bankAccount) deposit(balance int) { + b.m.Lock() + b.blnc = b.blnc + balance + b.m.Unlock() } diff --git a/exercise6/problem2/problem2.go b/exercise6/problem2/problem2.go index 97e02368..a7eda3d1 100644 --- a/exercise6/problem2/problem2.go +++ b/exercise6/problem2/problem2.go @@ -1,6 +1,7 @@ package problem2 import ( + "sync" "time" ) @@ -8,13 +9,29 @@ var readDelay = 10 * time.Millisecond type bankAccount struct { blnc int + m *sync.Mutex } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + m := sync.Mutex{} + return &bankAccount{blnc, &m} } func (b *bankAccount) balance() int { time.Sleep(readDelay) - return 0 + return b.blnc +} + +func (b *bankAccount) withdraw(balance int) { + b.m.Lock() + if b.blnc > 10 { + b.blnc = b.blnc - balance + } + b.m.Unlock() +} + +func (b *bankAccount) deposit(balance int) { + b.m.Lock() + b.blnc = b.blnc + balance + b.m.Unlock() } diff --git a/exercise6/problem3/problem3.go b/exercise6/problem3/problem3.go index b34b90bb..5c1b08f3 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 c.val +} diff --git a/exercise6/problem4/problem4.go b/exercise6/problem4/problem4.go index 793449c9..ed7e3b60 100644 --- a/exercise6/problem4/problem4.go +++ b/exercise6/problem4/problem4.go @@ -1,31 +1,36 @@ 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{} + newCond := sync.NewCond(&m) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, newCond, notifier) time.Sleep(time.Millisecond) // order matters } - - go updateShopList(shoppingList) - + go updateShopList(shoppingList, newCond) return notifier } diff --git a/exercise6/problem5/problem5.go b/exercise6/problem5/problem5.go index 8e4a1703..077d4ac0 100644 --- a/exercise6/problem5/problem5.go +++ b/exercise6/problem5/problem5.go @@ -1,31 +1,41 @@ package problem5 import ( + "sync" "time" ) -func worker(id int, shoppingList *[]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.Broadcast() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - + m := sync.Mutex{} + newCond := sync.NewCond(&m) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, newCond, notifier) time.Sleep(time.Millisecond) // order matters } - go updateShopList(shoppingList) + go updateShopList(shoppingList, newCond) return notifier } diff --git a/exercise6/problem7/problem7.go b/exercise6/problem7/problem7.go index fbab9284..0ea037b2 100644 --- a/exercise6/problem7/problem7.go +++ b/exercise6/problem7/problem7.go @@ -1,23 +1,32 @@ package problem7 import ( - "fmt" "math/rand" + "sync" "time" ) -//TODO: identify the data race -// fix the issue. - func task() { - start := time.Now() + var m sync.Mutex var t *time.Timer - t = time.AfterFunc( - randomDuration(), func() { - fmt.Println(time.Now().Sub(start)) + + resetTimer := func() { + m.Lock() + defer m.Unlock() + if t != nil { t.Reset(randomDuration()) + } + } + + m.Lock() + t = time.AfterFunc( + randomDuration(), + func() { + resetTimer() }, ) + m.Unlock() + time.Sleep(5 * time.Second) } diff --git a/exercise6/problem8/problem8.go b/exercise6/problem8/problem8.go index 949eb2d2..fea87ccc 100644 --- a/exercise6/problem8/problem8.go +++ b/exercise6/problem8/problem8.go @@ -1,3 +1,24 @@ package problem8 -func multiplex(chs []<-chan string) []string {} +import "sync" + +func multiplex(chs []<-chan string) []string { + var result []string + var wg sync.WaitGroup + var mux sync.Mutex + + for _, ch := range chs { + wg.Add(1) + go func(ch <-chan string) { + defer wg.Done() + for msg := range ch { + mux.Lock() + result = append(result, msg) + mux.Unlock() + } + }(ch) + } + + wg.Wait() + return result +} diff --git a/main.go b/main.go index 06ab7d0f..eb9ba429 100644 --- a/main.go +++ b/main.go @@ -1 +1,53 @@ package main + +import ( + "fmt" + "sync" +) + +type bankAccount struct { + blnc int + m *sync.Mutex +} + +func newAccount(blnc int) *bankAccount { + m := sync.Mutex{} + return &bankAccount{blnc, &m} +} + +func main() { + exp := 3 + var wg sync.WaitGroup + acc := newAccount(43) + + n := 100 + + wg.Add(n) + for i := range n { + go func(i int) { + defer wg.Done() + acc.withdraw(10) + }(i) + } + + wg.Wait() + + fmt.Println(acc.blnc) + if acc.blnc != exp { + fmt.Println("erroorrrrrr") + } else { + fmt.Println("vse ok") + } +} + +func (b *bankAccount) withdraw(balance int) { + b.m.Lock() + if b.blnc > 10 { + b.blnc = b.blnc - balance + } + b.m.Unlock() +} + +func main() { + // +} From cbdfcfe66d03e084c07dbab74b57b3b4cd16ccae Mon Sep 17 00:00:00 2001 From: galyym Date: Thu, 5 Dec 2024 02:06:41 +0500 Subject: [PATCH 16/23] feat: exercise7 --- .gitignore | 1 + exercise7/blogging-platform/.env | 0 exercise7/blogging-platform/go.mod | 2 ++ exercise7/blogging-platform/go.sum | 2 ++ exercise7/blogging-platform/internal/api/api.go | 14 ++++++++++++++ .../blogging-platform/internal/config/config.go | 14 ++++++++++++++ exercise7/blogging-platform/internal/db/db.go | 8 ++++++++ exercise7/blogging-platform/main.go | 12 ++++++++++++ 8 files changed, 53 insertions(+) create mode 100644 exercise7/blogging-platform/.env create mode 100644 exercise7/blogging-platform/internal/api/api.go create mode 100644 exercise7/blogging-platform/internal/config/config.go create mode 100644 exercise7/blogging-platform/internal/db/db.go diff --git a/.gitignore b/.gitignore index 09944b57..3ea2c01d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ go.work # Students students.txt +.env diff --git a/exercise7/blogging-platform/.env b/exercise7/blogging-platform/.env new file mode 100644 index 00000000..e69de29b diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..b1e38125 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -3,3 +3,5 @@ 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 // indirect 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/api.go b/exercise7/blogging-platform/internal/api/api.go new file mode 100644 index 00000000..2b6a4284 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/api.go @@ -0,0 +1,14 @@ +package api + +import "context" + +type Api struct { +} + +func New() *Api { + return &Api{} +} + +func (a *Api) Start(ctx context.Context) error { + return nil +} diff --git a/exercise7/blogging-platform/internal/config/config.go b/exercise7/blogging-platform/internal/config/config.go new file mode 100644 index 00000000..58880fc7 --- /dev/null +++ b/exercise7/blogging-platform/internal/config/config.go @@ -0,0 +1,14 @@ +package config + +import "github.com/joho/godotenv" + +type Config struct { +} + +func NewConfig() (*Config, error) { + config := &Config{} + if err := godotenv.Load(".env"); err != nil { + return nil, err + } + return config, nil +} diff --git a/exercise7/blogging-platform/internal/db/db.go b/exercise7/blogging-platform/internal/db/db.go new file mode 100644 index 00000000..b98d474a --- /dev/null +++ b/exercise7/blogging-platform/internal/db/db.go @@ -0,0 +1,8 @@ +package db + +type DB struct { +} + +func New() (*DB, error) { + return &DB{}, nil +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 1ffa1477..be051fc8 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -2,6 +2,7 @@ package main import ( "context" + config2 "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/config" "log/slog" "os" "os/signal" @@ -13,6 +14,17 @@ import ( func main() { ctx, cancel := context.WithCancel(context.Background()) + //config + _, err := config2.NewConfig() + if err != nil { + slog.ErrorContext( + ctx, + "error on load config", + "service", "config", + "error", err, + ) + } + // db _, err := db.New() if err != nil { From 5031efda3b4e4a629c7aaeaca110dd806b4e2bce Mon Sep 17 00:00:00 2001 From: tobirama Date: Thu, 5 Dec 2024 20:58:42 +0500 Subject: [PATCH 17/23] feat: api handler --- exercise4/bot/bot.zip | Bin 0 -> 4360 bytes exercise7/blogging-platform/.dockerignore | 32 +++++++ exercise7/blogging-platform/Dockerfile | 78 ++++++++++++++++++ exercise7/blogging-platform/README.Docker.md | 22 +++++ exercise7/blogging-platform/compose.yaml | 27 ++++++ .../blogging-platform/internal/api/api.go | 62 ++++++++++++-- .../internal/api/handler/blogs/get_blogs.go | 12 +++ .../internal/api/handler/blogs/main.go | 15 ++++ .../internal/api/handler/main.go | 20 +++++ .../internal/api/router/blogs.go | 9 ++ .../internal/api/router/main.go | 26 ++++++ exercise7/blogging-platform/main.go | 35 ++------ 12 files changed, 306 insertions(+), 32 deletions(-) create mode 100644 exercise4/bot/bot.zip create mode 100644 exercise7/blogging-platform/.dockerignore create mode 100644 exercise7/blogging-platform/Dockerfile create mode 100644 exercise7/blogging-platform/README.Docker.md create mode 100644 exercise7/blogging-platform/compose.yaml create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/main.go create mode 100644 exercise7/blogging-platform/internal/api/handler/main.go create mode 100644 exercise7/blogging-platform/internal/api/router/blogs.go create mode 100644 exercise7/blogging-platform/internal/api/router/main.go diff --git a/exercise4/bot/bot.zip b/exercise4/bot/bot.zip new file mode 100644 index 0000000000000000000000000000000000000000..de3f9227f6e3e3c1b84f2d8a2b94a210a461c138 GIT binary patch literal 4360 zcmbVP2{@E%8y-W9i6L8AQf`jy`4PWQErl^CX4{OP!xyrP4YvTef;+WsA!1P0KoSF`BNC?-7pSrE>7;=dqe{P zKtCi^N?z>X-=JMYxwuO>d;G5y9~v8BMx~gqM+Y_OBkvLJ4@n=QS?JVX5^XOtzcl85-KtJPKuOLov0Wd*`yuW1lx4*)!Q7+mz)sbr zc^s@~Jji5`7-SINILEo3tBg{sjGx@@I4`v9)_;piL1uz6}K1F>ryM^yFTNaOz?YgvyrtP-ilSlnJ z=mS#L?u%}+nWS!e6NvoScx|baF;rbozsu6q5gx=zpeWG6(?R`3sh}i-Wy4iu`yu?SY>~rd{p> zs?5qQhSF0=)JGYEWDc5}>Oy!^lQ{KksG32Qj=9g=IS+&yd){PywstjDwb})4Bctr< zhMB0u5nF|B!vpbh8Y2&rw$Qan9z(Vz0dwV?xEe?qY-~0x=0;%3olHB2Oitg3hA*RO zP>q`AjMfd9>vCcf);G~zv`GsZp~wsaE7n12Z8<)O7jV)(yK&3Rz2{e2nEVaVsZEmO zW^%<5cYEKe^8i)rR_h9<@+)vwUkjLTBkP*f`NgaFurpxaP&W@;8?7GwlZ1H8gnJ#Q z7DJlZBc!LT`6$#PMJktFtKQ{Vy+VPa&VOi zxK;ec0INr%0|!zsHE!@7)uR6v$s2-);1pHR-CvD1A%XF4j~+{S^31+IvPx8JAR<3n zrzoG(bi(nF)yGpBxZB}&Y^p3M#x6i?GPem}@Z+=F{=L2v_k5ZT_>58UEnc}c_U>>W zRg}~UFS7=~+wFL|&G38}RikW01H<@Hh&(=-iybQ?Cw-VGKY%V}?O6;6nQhlY(8YRH z$YOe^c`AUig8J%VqBmReD@(4_mOPqs%PlC&$1~_gSY%hke{_ZAM)YzOOvj~+$R2UB zVY&CH5dG8+F)d&`;&>Tn$Ds+A0uLp*v~*uMDt7zYfaJMEbkgRxCut0y$A|Ca`q?il z;!g!s9cVh*>M_vc2|Kp@fG4;*;laR(05QoWKM@c3c5_qLN z`wz}gGp#AYdtCIsVpgqql?IE>2A(*{JIp74DS|gwZYeRriLQdjKSVcwMJ@V?^MIj+ z;HwDJYccm{pYw!P!v|=BhWJZzt!&qrvV3HfP7!Zg3AgHFYc)GAb{DFSwf!=a zwbOHvfjvr7(_Drjz?#sOw;dXW0!H+q#=^zs? zwXB{DpEq6>Cn`HPDV{V=a1fu)Ha~7!R+JW)-_n`c78rDrf!D1$!{*Lmq-pUL0t-uc zjKs~28_nCJ0t9QU9C$Imr`5%!5^9Nn}4ea~4hrSon*6TLV zGc<+f!L-F^CV@uaT9Q*9JP(_l*CYgP64K0W69WHYlWIT;*AxNwJyVV8toFvF{Bm+L5NGV_J|qmvkZ2uaJECZV%5-g^m_Kf z+JHhPa#iR+IR^woyveCk(*LbWl(!mtoZ;^-K!f}bExAh09ugNl9KY-9hC9Sr%bLTP zGXKc}baWYtbm^FC_rThq`u)1yJ-*DeKU)UK|BO_NM@!vBdrAv$`lh>rI`$tB*eaU4i0Dz91(HSt3FlQIuhT2Rj%n(pivP<%-iCWbVu4l=Z5C!;`pqW#wYF@DGfF0 zgwIQO5Y`sEhff4*f?AC)jxiBus+HTgrF`NOs_I7@d@`8wE;vjbyM?x}j6Up{)i%C- zxtY4A>`O(!i|RC$_Rq#*G+S}BvlEK?Xs@bX{pmv_p&-g3Z?DjBZj;MNDcs+ebdeSS zpjbUGCr@7|&pq%p+f?6UREj}qv@S&UshR}KzC*s?SLy@BmWQYQT0<gp70+u&n=W-t19q_DoJsrA7d**e1dC|#$S(Dbq7NJwf~ z_kg?EKC2ngH>b|p4ZAYmOuN(T*X1pH?`}VPyY6`U=zL2GVK6NCI9y&}<(z~lDV-T* z+HokVbOqx`OdwnX$k4JV%(dXrqv$^I>JgMFv z>bPdrr|g&guVQV8hf0pYZ#Ityc2-u-ZQh*)KVWz_V_7RuG^e&r?IPj~#lIJ@E?y7N zJJ^=9F000Mcs2N)2=tEoW5t2w^5zTK=+A3Jw#a^(1dVmSEU5Jw{|h!m2TMrQ;vJQk zgCM0(1iLT+Bli%sII~F^!eA`+lC*4fn8o?8Ra-~j%N*H0n~^YKuP>BZcQAQgGw2Pe zR`B&ROHdJFUFtcB;lwA=19n_aN6LlcbMLN$7@^t~XBL)3S~K)fU!)w%`BIFPA;J8h zb|1fkM{gHxNE1FuD3z`&tSX?+g`>>jAKZ|Ql^cfGj|3QdP8X?q%IYZjr!*|DsnG=k1aT5`WN@BpTkma zSUa%=KeHQl&-bn0(06R@Zu3l0ww=HQ$Nvrb9$&`)R2hm7--#{w6mVB%_UOug+`w)J zK=I`}feV%*pVRME|Bn6dT>3xlg5oH4VhcV&X2b6*?bToWxQ*Qs{o^@^)BOc}uYO}^ z2RmxNTXiXFzZ1A%TC#uIoA&R Date: Mon, 9 Dec 2024 19:54:55 +0500 Subject: [PATCH 18/23] feat: db migrate --- exercise7/blogging-platform/.env | 4 ++++ exercise7/blogging-platform/.env.example | 4 ++++ exercise7/blogging-platform/.gitignore | 1 + exercise7/blogging-platform/Makefile | 10 ++++++++++ exercise7/blogging-platform/compose.yaml | 5 +++-- .../db/migrations/000001_create_blogs_table.down.sql | 1 + .../db/migrations/000001_create_blogs_table.up.sql | 7 +++++++ exercise7/blogging-platform/go.mod | 5 ++++- exercise7/blogging-platform/go.sum | 2 ++ 9 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 exercise7/blogging-platform/.env.example create mode 100644 exercise7/blogging-platform/.gitignore create mode 100644 exercise7/blogging-platform/Makefile create mode 100644 exercise7/blogging-platform/db/migrations/000001_create_blogs_table.down.sql create mode 100644 exercise7/blogging-platform/db/migrations/000001_create_blogs_table.up.sql diff --git a/exercise7/blogging-platform/.env b/exercise7/blogging-platform/.env index e69de29b..b227b471 100644 --- a/exercise7/blogging-platform/.env +++ b/exercise7/blogging-platform/.env @@ -0,0 +1,4 @@ +POSTGRES_DB=postgres +POSTGRES_USER=root +POSTGRES_PASSWORD=root +POSTGRES_URL='postgres://root:root@localhost:54322/postgres?sslmode=disable' \ No newline at end of file diff --git a/exercise7/blogging-platform/.env.example b/exercise7/blogging-platform/.env.example new file mode 100644 index 00000000..eb3c2296 --- /dev/null +++ b/exercise7/blogging-platform/.env.example @@ -0,0 +1,4 @@ +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_URL= \ No newline at end of file diff --git a/exercise7/blogging-platform/.gitignore b/exercise7/blogging-platform/.gitignore new file mode 100644 index 00000000..b9690d5f --- /dev/null +++ b/exercise7/blogging-platform/.gitignore @@ -0,0 +1 @@ +./.env \ No newline at end of file diff --git a/exercise7/blogging-platform/Makefile b/exercise7/blogging-platform/Makefile new file mode 100644 index 00000000..a4e7202d --- /dev/null +++ b/exercise7/blogging-platform/Makefile @@ -0,0 +1,10 @@ +ifneq (,$(wildcard .env)) + include .env + export +endif + +up: + migrate -database $(POSTGRES_URL) -path db/migrations/ up + +down: + migrate -database $(POSTGRES_URL) -path db/migrations/ down \ No newline at end of file diff --git a/exercise7/blogging-platform/compose.yaml b/exercise7/blogging-platform/compose.yaml index 9982f2d4..0b51c446 100644 --- a/exercise7/blogging-platform/compose.yaml +++ b/exercise7/blogging-platform/compose.yaml @@ -15,8 +15,9 @@ services: - POSTGRES_DB=postgres - POSTGRES_USER=root - POSTGRES_PASSWORD=root - expose: - - 5432 + - POSTGRES_URL='postgres://postgres:root@localhost:5432/postgres?sslmode=disable' + ports: + - 54322:5432 healthcheck: test: [ "CMD", "pg_isready" ] interval: 10s diff --git a/exercise7/blogging-platform/db/migrations/000001_create_blogs_table.down.sql b/exercise7/blogging-platform/db/migrations/000001_create_blogs_table.down.sql new file mode 100644 index 00000000..ee21ccfc --- /dev/null +++ b/exercise7/blogging-platform/db/migrations/000001_create_blogs_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS blogs; \ No newline at end of file diff --git a/exercise7/blogging-platform/db/migrations/000001_create_blogs_table.up.sql b/exercise7/blogging-platform/db/migrations/000001_create_blogs_table.up.sql new file mode 100644 index 00000000..df9a21eb --- /dev/null +++ b/exercise7/blogging-platform/db/migrations/000001_create_blogs_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS blogs ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + description TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) \ No newline at end of file diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index b1e38125..df6d25ee 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -4,4 +4,7 @@ go 1.23.3 require github.com/lib/pq v1.10.9 -require github.com/joho/godotenv v1.5.1 // indirect +require ( + github.com/golang-migrate/migrate v3.5.4+incompatible // indirect + github.com/joho/godotenv v1.5.1 // indirect +) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index ecb9035f..48a7e3f1 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,3 +1,5 @@ +github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= +github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= 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= From 91f1c13a91149ed3fff6e35497f6fd31530a05be Mon Sep 17 00:00:00 2001 From: tobirama Date: Tue, 10 Dec 2024 18:49:07 +0500 Subject: [PATCH 19/23] fix: refactoring --- .../blogging-platform/internal/api/api.go | 66 ------------------- .../blogging-platform/internal/api/main.go | 43 ++++++++++++ exercise7/blogging-platform/main.go | 27 ++++++-- 3 files changed, 66 insertions(+), 70 deletions(-) delete mode 100644 exercise7/blogging-platform/internal/api/api.go create mode 100644 exercise7/blogging-platform/internal/api/main.go diff --git a/exercise7/blogging-platform/internal/api/api.go b/exercise7/blogging-platform/internal/api/api.go deleted file mode 100644 index ca3dd303..00000000 --- a/exercise7/blogging-platform/internal/api/api.go +++ /dev/null @@ -1,66 +0,0 @@ -package api - -import ( - "context" - "errors" - "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/config" - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" - "log/slog" - "net" - "net/http" - "os" - "os/signal" -) - -type Api struct { - config *config.Config - model *db.DB - logger *slog.Logger -} - -func NewApi(config *config.Config, db *db.DB) *Api { - return &Api{ - config: config, - model: db, - logger: slog.With("service", "api"), - } -} - -func (a *Api) Start(ctx context.Context, cancel context.CancelFunc) { - h := handler.NewHandler(a.logger, a.model) - r := router.NewRouter(h) - mux := r.Start(ctx) - - srv := &http.Server{ - Addr: ":8080", - Handler: mux, - BaseContext: func(net.Listener) context.Context { return ctx }, - } - - go func() { - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - slog.ErrorContext(ctx, "server error", "error", err) - } - cancel() - }() - - slog.InfoContext( - ctx, - "starting api service", - "port", ":8080", - ) - - shutdown := make(chan os.Signal, 1) // Create channel to signify s signal being sent - signal.Notify(shutdown, os.Interrupt) // When an interrupt is sent, notify the channel - - go func() { - _ = <-shutdown - - slog.WarnContext(ctx, "gracefully shutting down...") - if err := srv.Shutdown(ctx); err != nil { - slog.ErrorContext(ctx, "server shutdown error", "error", err) - } - }() -} diff --git a/exercise7/blogging-platform/internal/api/main.go b/exercise7/blogging-platform/internal/api/main.go new file mode 100644 index 00000000..9d34f838 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/main.go @@ -0,0 +1,43 @@ +package api + +import ( + "context" + "errors" + "fmt" + "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" + "log/slog" + "net" + "net/http" +) + +type Api struct { + logger *slog.Logger + router *router.Router +} + +func NewApi(logger *slog.Logger, db *db.DB) *Api { + h := handler.NewHandler(logger, db) + r := router.NewRouter(h) + return &Api{ + logger: slog.With("service", "api"), + router: r, + } +} + +func (a *Api) Start(ctx context.Context) error { + mux := a.router.Start(ctx) + + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + BaseContext: func(net.Listener) context.Context { return ctx }, + } + + fmt.Println("Staring server on :8080") + if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 3f1baa5b..bc339624 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -3,16 +3,18 @@ package main import ( "context" "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api" - config2 "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/config" + conf "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/config" db "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" "log/slog" + "os" + "os/signal" ) func main() { ctx, cancel := context.WithCancel(context.Background()) //config - config, err := config2.NewConfig() + _, err := conf.NewConfig() if err != nil { slog.ErrorContext( ctx, @@ -35,8 +37,25 @@ func main() { } // api - a := api.NewApi(config, dbConnect) + a := api.NewApi(slog.With("service", "api"), dbConnect) slog.InfoContext(ctx, "initialize service", "service", "api") - a.Start(ctx, cancel) + + go func(ctx context.Context, cancelFunc context.CancelFunc) { + if err := a.Start(ctx); err != nil { + slog.ErrorContext(ctx, "failed to start api", "error", err.Error()) + } + cancelFunc() + }(ctx, cancel) + + go func(cancelFunc context.CancelFunc) { + shutdown := make(chan os.Signal, 1) // Create channel to signify s signal being sent + signal.Notify(shutdown, os.Interrupt) // When an interrupt is sent, notify the channel + + sig := <-shutdown + slog.WarnContext(ctx, "signal received - shutting down...", "signal", sig) + + cancelFunc() + }(cancel) + <-ctx.Done() } From bc0d22c7716f9ceb8c092f902f15546e351292e8 Mon Sep 17 00:00:00 2001 From: tobirama Date: Thu, 12 Dec 2024 19:46:09 +0500 Subject: [PATCH 20/23] fix: db init --- exercise7/blogging-platform/go.mod | 1 + exercise7/blogging-platform/go.sum | 2 ++ .../internal/config/config.go | 20 +++++++++++-- .../internal/config/service_db.go | 30 +++++++++++++++++++ exercise7/blogging-platform/internal/db/db.go | 9 +++++- exercise7/blogging-platform/main.go | 4 +-- 6 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 exercise7/blogging-platform/internal/config/service_db.go diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index df6d25ee..f1963b05 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -7,4 +7,5 @@ require github.com/lib/pq v1.10.9 require ( github.com/golang-migrate/migrate v3.5.4+incompatible // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/sethvargo/go-envconfig v1.1.0 // indirect ) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index 48a7e3f1..adac5039 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -4,3 +4,5 @@ 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= +github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= +github.com/sethvargo/go-envconfig v1.1.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= diff --git a/exercise7/blogging-platform/internal/config/config.go b/exercise7/blogging-platform/internal/config/config.go index 58880fc7..0faa5e9a 100644 --- a/exercise7/blogging-platform/internal/config/config.go +++ b/exercise7/blogging-platform/internal/config/config.go @@ -1,14 +1,30 @@ package config -import "github.com/joho/godotenv" +import ( + "context" + "flag" + "github.com/joho/godotenv" +) + +type SharedConfig struct { + Port int `env:"PORT"` + Host string `env:"HOST,default=localhost"` +} type Config struct { + DB *DBConfig } -func NewConfig() (*Config, error) { +func NewConfig(ctx context.Context) (*Config, error) { config := &Config{} if err := godotenv.Load(".env"); err != nil { return nil, err } + if c, err := NewConfigDB(ctx); err != nil { + return nil, err + } else { + config.DB = c + } + flag.Parse() return config, nil } diff --git a/exercise7/blogging-platform/internal/config/service_db.go b/exercise7/blogging-platform/internal/config/service_db.go new file mode 100644 index 00000000..bc3939fc --- /dev/null +++ b/exercise7/blogging-platform/internal/config/service_db.go @@ -0,0 +1,30 @@ +package config + +import ( + "context" + "flag" + "github.com/sethvargo/go-envconfig" +) + +type DBConfig struct { + *SharedConfig `env:",prefix=DB_"` + User string `env:"DB_USER"` + Password string `env:"DB_PASSWORD"` + DBName string `env:"DB_NAME"` +} + +func NewConfigDB(ctx context.Context) (*DBConfig, error) { + c := &DBConfig{} + + if err := envconfig.Process(ctx, c); err != nil { + return nil, err + } + + flag.StringVar(&c.Host, "db-host", c.Host, "database host [DB_HOST]") + flag.IntVar(&c.Port, "db-port", c.Port, "database port [DB_PORT]") + flag.StringVar(&c.User, "db-user", c.User, "database user [DB_USER]") + flag.StringVar(&c.Password, "db-password", c.Password, "database password [DB_PASSWORD]") + flag.StringVar(&c.DBName, "db-name", c.DBName, "database name [DB_NAME]") + + return c, nil +} diff --git a/exercise7/blogging-platform/internal/db/db.go b/exercise7/blogging-platform/internal/db/db.go index b98d474a..3c8e5517 100644 --- a/exercise7/blogging-platform/internal/db/db.go +++ b/exercise7/blogging-platform/internal/db/db.go @@ -1,8 +1,15 @@ package db +import ( + "database/sql" + conf "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/config" +) + type DB struct { + conf *conf.Config + db *sql.DB } -func New() (*DB, error) { +func New(conf *conf.Config) (*DB, error) { return &DB{}, nil } diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index bc339624..09391c03 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -14,7 +14,7 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) //config - _, err := conf.NewConfig() + config, err := conf.NewConfig(ctx) if err != nil { slog.ErrorContext( ctx, @@ -25,7 +25,7 @@ func main() { } // db - dbConnect, err := db.New() + dbConnect, err := db.New(config) if err != nil { slog.ErrorContext( ctx, From 75d6dc4a6e7013153315393fbc10b19eba4e1bc0 Mon Sep 17 00:00:00 2001 From: tobirama Date: Fri, 13 Dec 2024 19:09:49 +0500 Subject: [PATCH 21/23] fix: db init --- exercise7/blogging-platform/go.mod | 10 ++--- .../internal/api/handler/blogs/main.go | 4 +- .../internal/api/handler/main.go | 2 +- .../blogging-platform/internal/api/main.go | 2 +- .../internal/config/service_db.go | 1 - exercise7/blogging-platform/internal/db/db.go | 40 +++++++++++++++++-- 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index f1963b05..f76ff593 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -2,10 +2,10 @@ 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/golang-migrate/migrate v3.5.4+incompatible // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/sethvargo/go-envconfig v1.1.0 // indirect + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 // indirect + github.com/sethvargo/go-envconfig v1.1.0 ) + +require github.com/golang-migrate/migrate v3.5.4+incompatible // indirect diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/main.go b/exercise7/blogging-platform/internal/api/handler/blogs/main.go index 647c1d41..8ccbed40 100644 --- a/exercise7/blogging-platform/internal/api/handler/blogs/main.go +++ b/exercise7/blogging-platform/internal/api/handler/blogs/main.go @@ -7,9 +7,9 @@ import ( type Blogs struct { logger *slog.Logger - db *db.DB + db *db.ConfDB } -func New(logger *slog.Logger, db *db.DB) *Blogs { +func New(logger *slog.Logger, db *db.ConfDB) *Blogs { return &Blogs{logger: logger, db: db} } diff --git a/exercise7/blogging-platform/internal/api/handler/main.go b/exercise7/blogging-platform/internal/api/handler/main.go index 2dfa6777..b9e4814e 100644 --- a/exercise7/blogging-platform/internal/api/handler/main.go +++ b/exercise7/blogging-platform/internal/api/handler/main.go @@ -10,7 +10,7 @@ type Handler struct { *blogs.Blogs } -func NewHandler(logger *slog.Logger, db *db.DB) *Handler { +func NewHandler(logger *slog.Logger, db *db.ConfDB) *Handler { return &Handler{ blogs.New( logger, diff --git a/exercise7/blogging-platform/internal/api/main.go b/exercise7/blogging-platform/internal/api/main.go index 9d34f838..3142d327 100644 --- a/exercise7/blogging-platform/internal/api/main.go +++ b/exercise7/blogging-platform/internal/api/main.go @@ -17,7 +17,7 @@ type Api struct { router *router.Router } -func NewApi(logger *slog.Logger, db *db.DB) *Api { +func NewApi(logger *slog.Logger, db *db.ConfDB) *Api { h := handler.NewHandler(logger, db) r := router.NewRouter(h) return &Api{ diff --git a/exercise7/blogging-platform/internal/config/service_db.go b/exercise7/blogging-platform/internal/config/service_db.go index bc3939fc..630d924b 100644 --- a/exercise7/blogging-platform/internal/config/service_db.go +++ b/exercise7/blogging-platform/internal/config/service_db.go @@ -25,6 +25,5 @@ func NewConfigDB(ctx context.Context) (*DBConfig, error) { flag.StringVar(&c.User, "db-user", c.User, "database user [DB_USER]") flag.StringVar(&c.Password, "db-password", c.Password, "database password [DB_PASSWORD]") flag.StringVar(&c.DBName, "db-name", c.DBName, "database name [DB_NAME]") - return c, nil } diff --git a/exercise7/blogging-platform/internal/db/db.go b/exercise7/blogging-platform/internal/db/db.go index 3c8e5517..f1a8fda7 100644 --- a/exercise7/blogging-platform/internal/db/db.go +++ b/exercise7/blogging-platform/internal/db/db.go @@ -2,14 +2,48 @@ package db import ( "database/sql" + "fmt" conf "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/config" ) -type DB struct { +type ConfDB struct { conf *conf.Config db *sql.DB } -func New(conf *conf.Config) (*DB, error) { - return &DB{}, nil +func New(conf *conf.Config) (*ConfDB, error) { + db, err := NewDb( + conf.DB.Host, + conf.DB.Port, + conf.DB.User, + conf.DB.Password, + conf.DB.DBName, + ) + if err != nil { + return nil, err + } + + return &ConfDB{ + conf: conf, + db: db, + }, nil +} + +func NewDb(host string, port int, user string, password string, dbname string) (*sql.DB, error) { + fmt.Println(host, port, user, password, dbname) + 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 + } + + if err = db.Ping(); err != nil { + return nil, err + } + + return db, nil } From 81641b172916dd4403340ef5984bfa495abdd016 Mon Sep 17 00:00:00 2001 From: galyym Date: Thu, 2 Jan 2025 03:21:45 +0500 Subject: [PATCH 22/23] feat: exercise7 --- exercise7/blogging-platform/.env.example | 10 +- .../Blogs.postman_collection.json | 154 ++++++++++++++++++ exercise7/blogging-platform/Dockerfile | 54 ++---- exercise7/blogging-platform/compose.yaml | 1 + exercise7/blogging-platform/go.sum | 2 + .../internal/api/handler/blogs/create_blog.go | 45 +++++ .../internal/api/handler/blogs/delete_blog.go | 47 ++++++ .../internal/api/handler/blogs/get_blog.go | 37 +++++ .../internal/api/handler/blogs/get_blogs.go | 23 ++- .../internal/api/handler/blogs/update_blog.go | 55 +++++++ .../internal/api/router/blogs.go | 4 + .../internal/db/blog/main.go | 18 ++ .../internal/db/blog/model.go | 11 ++ .../internal/db/blog/repository.go | 127 +++++++++++++++ exercise7/blogging-platform/internal/db/db.go | 21 ++- exercise7/blogging-platform/main.go | 2 +- 16 files changed, 553 insertions(+), 58 deletions(-) create mode 100644 exercise7/blogging-platform/Blogs.postman_collection.json create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/delete_blog.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go create mode 100644 exercise7/blogging-platform/internal/db/blog/main.go create mode 100644 exercise7/blogging-platform/internal/db/blog/model.go create mode 100644 exercise7/blogging-platform/internal/db/blog/repository.go diff --git a/exercise7/blogging-platform/.env.example b/exercise7/blogging-platform/.env.example index eb3c2296..88134069 100644 --- a/exercise7/blogging-platform/.env.example +++ b/exercise7/blogging-platform/.env.example @@ -1,4 +1,6 @@ -POSTGRES_DB= -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_URL= \ No newline at end of file +DB_NAME=postgres +DB_USER=root +DB_PASSWORD=root +POSTGRES_URL='postgres://root:root@localhost:5432/postgres?sslmode=disable' +DB_PORT=5432 +DB_HOST=127.0.0.1 \ No newline at end of file diff --git a/exercise7/blogging-platform/Blogs.postman_collection.json b/exercise7/blogging-platform/Blogs.postman_collection.json new file mode 100644 index 00000000..dc3f5518 --- /dev/null +++ b/exercise7/blogging-platform/Blogs.postman_collection.json @@ -0,0 +1,154 @@ +{ + "info": { + "_postman_id": "cc87dc3b-23a2-4122-bfd2-ca454eab6f92", + "name": "Blogs", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "19738142" + }, + "item": [ + { + "name": "GetAll", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/blogs", + "host": [ + "{{url}}" + ], + "path": [ + "blogs" + ] + } + }, + "response": [] + }, + { + "name": "GetByID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/blog/1", + "host": [ + "{{url}}" + ], + "path": [ + "blog", + "1" + ] + } + }, + "response": [] + }, + { + "name": "CreateBlog", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"test1\",\n \"description\": \"test test test\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/blog", + "host": [ + "{{url}}" + ], + "path": [ + "blog" + ] + } + }, + "response": [] + }, + { + "name": "UpdateBlog", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"test1\",\n \"description\": \"test test test\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/blog/1", + "host": [ + "{{url}}" + ], + "path": [ + "blog", + "1" + ] + } + }, + "response": [] + }, + { + "name": "UpdateBlog Copy", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"test1\",\n \"description\": \"test test test\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/blog/1", + "host": [ + "{{url}}" + ], + "path": [ + "blog", + "1" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "url", + "value": "127.0.0.1:8080", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/exercise7/blogging-platform/Dockerfile b/exercise7/blogging-platform/Dockerfile index d8a8fa74..ae05a6eb 100644 --- a/exercise7/blogging-platform/Dockerfile +++ b/exercise7/blogging-platform/Dockerfile @@ -1,62 +1,30 @@ -# syntax=docker/dockerfile:1 - -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Dockerfile reference guide at -# https://docs.docker.com/go/dockerfile-reference/ - -# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 - -################################################################################ -# Create a stage for building the application. ARG GO_VERSION=1.23.3 FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build WORKDIR /src -# Download dependencies as a separate step to take advantage of Docker's caching. -# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. -# Leverage bind mounts to go.sum and go.mod to avoid having to copy them into -# the container. RUN --mount=type=cache,target=/go/pkg/mod/ \ --mount=type=bind,source=go.sum,target=go.sum \ --mount=type=bind,source=go.mod,target=go.mod \ go mod download -x -# This is the architecture you're building for, which is passed in by the builder. -# Placing it here allows the previous steps to be cached across architectures. ARG TARGETARCH -# Build the application. -# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. -# Leverage a bind mount to the current directory to avoid having to copy the -# source code into the container. RUN --mount=type=cache,target=/go/pkg/mod/ \ --mount=type=bind,target=. \ CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . -################################################################################ -# Create a new stage for running the application that contains the minimal -# runtime dependencies for the application. This often uses a different base -# image from the build stage where the necessary files are copied from the build -# stage. -# -# The example below uses the alpine image as the foundation for running the app. -# By specifying the "latest" tag, it will also use whatever happens to be the -# most recent version of that image when you build your Dockerfile. If -# reproducability is important, consider using a versioned tag -# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff). -FROM alpine:latest AS final +FROM debian:bullseye-slim AS final -# Install any runtime dependencies that are needed to run your application. -# Leverage a cache mount to /var/cache/apk/ to speed up subsequent builds. -RUN --mount=type=cache,target=/var/cache/apk \ - apk --update add \ +# Обновление списка пакетов и установка необходимых пакетов +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && \ + apt-get install -y --no-install-recommends \ ca-certificates \ tzdata \ - && \ - update-ca-certificates + curl \ + && \ + update-ca-certificates -# Create a non-privileged user that the app will run under. -# See https://docs.docker.com/go/dockerfile-user-best-practices/ ARG UID=10001 RUN adduser \ --disabled-password \ @@ -66,13 +34,11 @@ RUN adduser \ --no-create-home \ --uid "${UID}" \ appuser + USER appuser -# Copy the executable from the "build" stage. COPY --from=build /bin/server /bin/ -# Expose the port that the application listens on. EXPOSE 8080 -# What the container should run when it is started. -ENTRYPOINT [ "/bin/server" ] +ENTRYPOINT [ "/bin/server" ] \ No newline at end of file diff --git a/exercise7/blogging-platform/compose.yaml b/exercise7/blogging-platform/compose.yaml index 0b51c446..99b39795 100644 --- a/exercise7/blogging-platform/compose.yaml +++ b/exercise7/blogging-platform/compose.yaml @@ -7,6 +7,7 @@ services: # - 8080:8080 db: image: postgres + container_name: blog_db restart: always user: postgres volumes: diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index adac5039..dfda0a2f 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,5 +1,7 @@ github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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= diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go new file mode 100644 index 00000000..8cf6542c --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go @@ -0,0 +1,45 @@ +package blogs + +import ( + "encoding/json" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" +) + +type CreateBlogRequest struct { + Data *blog.BlogModel `json:"data"` +} + +type CreateBlogResponse struct { + Data *blog.BlogModel `json:"data"` +} + +func (b *Blogs) AddBlog(w http.ResponseWriter, r *http.Request) { + b.logger.Debug("method", "Create blog") + + decoder := json.NewDecoder(r.Body) + rBody := blog.BlogModel{} + err := decoder.Decode(&rBody) + if err != nil { + b.logger.Error("Error in request body") + return + } + + createdBlog, err := b.db.AddBlog(&rBody) + if err != nil { + b.logger.Error( + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + + response.JSON( + w, + http.StatusOK, + createdBlog, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/delete_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/delete_blog.go new file mode 100644 index 00000000..018e90ca --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/delete_blog.go @@ -0,0 +1,47 @@ +package blogs + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" + "strconv" +) + +type DeleteBlogRequest struct { + Data *blog.BlogModel `json:"data"` +} + +type DeleteBlogResponse struct { + Data *blog.BlogModel `json:"data"` +} + +func (b *Blogs) DeleteBlog(w http.ResponseWriter, r *http.Request) { + b.logger.Info("method", "Delete blog") + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + b.logger.Error( + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + + err = b.db.DeleteBlog(id) + if err != nil { + b.logger.Error( + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + + response.JSON( + w, + http.StatusOK, + "successfully deleted", + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go new file mode 100644 index 00000000..50363b76 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go @@ -0,0 +1,37 @@ +package blogs + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" + "strconv" +) + +func (b *Blogs) GetBlogById(w http.ResponseWriter, r *http.Request) { + b.logger.Debug("method", "Find blog") + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + b.logger.Error( + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + + blog, err := b.db.GetBlogById(id) + if err != nil { + b.logger.Error( + "Blog not found by id", + "error", err, + ) + http.Error(w, "Blog not found by id", http.StatusBadRequest) + return + } + response.JSON( + w, + http.StatusOK, + blog, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go b/exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go index b1ca7c4d..fca6bf8d 100644 --- a/exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go +++ b/exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go @@ -1,12 +1,27 @@ package blogs import ( - "fmt" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" "net/http" ) func (b *Blogs) GetBlogs(w http.ResponseWriter, r *http.Request) { - _ = r.Context() - b.logger.Debug("in GetBlogs") - fmt.Println("in GetBlogs") + _ = b.logger.With("method", "Find blog") + offset := 0 + limit := 10 + blogs, err := b.db.GetBlogs(offset, limit) + if err != nil { + b.logger.Error( + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + response.JSON( + w, + http.StatusOK, + blogs, + ) + return } diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go new file mode 100644 index 00000000..29a15522 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go @@ -0,0 +1,55 @@ +package blogs + +import ( + "encoding/json" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" + "strconv" +) + +type UpdateBlogRequest struct { + Data *blog.BlogModel `json:"data"` +} + +type UpdateBlogResponse struct { + Data *blog.BlogModel `json:"data"` +} + +func (b *Blogs) UpdateBlog(w http.ResponseWriter, r *http.Request) { + b.logger.Debug("method", "Update blog") + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + b.logger.Error( + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + decoder := json.NewDecoder(r.Body) + rBody := blog.BlogModel{} + err = decoder.Decode(&rBody) + if err != nil { + b.logger.Error("Error in request body") + return + } + + err = b.db.UpdateBlog(id, &rBody) + if err != nil { + b.logger.Error( + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + + response.JSON( + w, + http.StatusOK, + "successfully updated", + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/router/blogs.go b/exercise7/blogging-platform/internal/api/router/blogs.go index cb8be813..8d4e6047 100644 --- a/exercise7/blogging-platform/internal/api/router/blogs.go +++ b/exercise7/blogging-platform/internal/api/router/blogs.go @@ -6,4 +6,8 @@ import ( func (r *Router) blogs(ctx context.Context) { r.router.HandleFunc("GET /blogs", r.handler.GetBlogs) + r.router.HandleFunc("GET /blog/{id}", r.handler.GetBlogById) + r.router.HandleFunc("POST /blog", r.handler.AddBlog) + r.router.HandleFunc("PUT /blog/{id}", r.handler.UpdateBlog) + r.router.HandleFunc("DELETE /blog/{id}", r.handler.DeleteBlog) } 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..782fa5ee --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/main.go @@ -0,0 +1,18 @@ +package blog + +import ( + "database/sql" + "log/slog" +) + +type Blogs struct { + logger *slog.Logger + db *sql.DB +} + +func NewBlog(logger *slog.Logger, db *sql.DB) *Blogs { + return &Blogs{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/db/blog/model.go b/exercise7/blogging-platform/internal/db/blog/model.go new file mode 100644 index 00000000..bd25d197 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/model.go @@ -0,0 +1,11 @@ +package blog + +import "time" + +type BlogModel struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/exercise7/blogging-platform/internal/db/blog/repository.go b/exercise7/blogging-platform/internal/db/blog/repository.go new file mode 100644 index 00000000..af57da92 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/repository.go @@ -0,0 +1,127 @@ +package blog + +import "fmt" + +func (b *Blogs) GetBlogs(offset, limit int) ([]BlogModel, error) { + query := `SELECT * FROM blogs OFFSET $1 LIMIT $2` + + rows, err := b.db.Query(query, offset, limit) + if err != nil { + b.logger.Error("fail to query table movie", "error", err) + return nil, err + } + + blogs := make([]BlogModel, 0) + defer rows.Close() + + for rows.Next() { + blog := BlogModel{} + if err := rows.Scan( + &blog.ID, + &blog.Name, + &blog.Description, + &blog.CreatedAt, + &blog.UpdatedAt, + ); err != nil { + b.logger.Error("fail to query table movie", "error", err) + return nil, err + } + blogs = append(blogs, blog) + } + + if err := rows.Err(); err != nil { + b.logger.Error("fail to scan rows", "error", err) + return nil, err + } + + return blogs, nil +} + +func (b *Blogs) GetBlogById(id int) (*BlogModel, error) { + query := `SELECT * FROM blogs WHERE id = $1` + + rows := b.db.QueryRow(query, id) + if err := rows.Err(); err != nil { + b.logger.Error("fail to query table movie", "error", err) + return nil, err + } + + blogs := BlogModel{} + if err := rows.Scan( + &blogs.ID, + &blogs.Name, + &blogs.Description, + &blogs.CreatedAt, + &blogs.UpdatedAt, + ); err != nil { + b.logger.Error("fail to query table movie", "error", err) + return nil, err + } + return &blogs, nil +} + +func (b *Blogs) AddBlog(data *BlogModel) (*BlogModel, error) { + query := `INSERT INTO blogs (name, description) VALUES ($1, $2) RETURNING id, name, description, created_at, updated_at` + + rows := b.db.QueryRow(query, data.Name, data.Description) + if err := rows.Err(); err != nil { + b.logger.Error("fail to query table movie", "error", err) + return nil, err + } + blogs := BlogModel{} + if err := rows.Scan( + &blogs.ID, + &blogs.Name, + &blogs.Description, + &blogs.CreatedAt, + &blogs.UpdatedAt, + ); err != nil { + b.logger.Error("fail to query table movie", "error", err) + return nil, err + } + return &blogs, nil +} + +func (b *Blogs) UpdateBlog(id int, data *BlogModel) error { + query := `UPDATE blogs SET name = $2, description = $3 WHERE id = $1` + + resp, err := b.db.Exec(query, id, data.Name, data.Description) + if err != nil { + b.logger.Error("fail to query table movie", "error", err) + return err + } + + num, err := resp.RowsAffected() + if err != nil { + b.logger.Error("failed to RowsAffected") + return fmt.Errorf("failed to RowsAffected") + } + if num == 0 { + b.logger.Error("blogs not found", "id", id) + return fmt.Errorf("blogs not found id = %d", id) + } + b.logger.Info("blogs successfully updated") + return nil +} + +func (b *Blogs) DeleteBlog(id int) error { + query := `DELETE FROM blogs WHERE id = $1` + + resp, err := b.db.Exec(query, id) + if err != nil { + b.logger.Error("fail to query table movie", "error", err) + return err + } + + num, err := resp.RowsAffected() + if err != nil { + b.logger.Error("failed to RowsAffected") + return fmt.Errorf("failed to RowsAffected") + } + if num == 0 { + b.logger.Error("blogs not found", "id", id) + return fmt.Errorf("blogs not found id = %d", id) + } + b.logger.Info("blogs successfully deleted") + return nil +} diff --git a/exercise7/blogging-platform/internal/db/db.go b/exercise7/blogging-platform/internal/db/db.go index f1a8fda7..7d2dc322 100644 --- a/exercise7/blogging-platform/internal/db/db.go +++ b/exercise7/blogging-platform/internal/db/db.go @@ -3,15 +3,21 @@ package db import ( "database/sql" "fmt" + _ "github.com/lib/pq" conf "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/config" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "log/slog" + "os" + "strconv" ) type ConfDB struct { - conf *conf.Config - db *sql.DB + Conf *conf.Config + DB *sql.DB + *blog.Blogs } -func New(conf *conf.Config) (*ConfDB, error) { +func New(conf *conf.Config, logger *slog.Logger) (*ConfDB, error) { db, err := NewDb( conf.DB.Host, conf.DB.Port, @@ -24,13 +30,18 @@ func New(conf *conf.Config) (*ConfDB, error) { } return &ConfDB{ - conf: conf, - db: db, + Conf: conf, + DB: db, + Blogs: blog.NewBlog(logger, db), }, nil } func NewDb(host string, port int, user string, password string, dbname string) (*sql.DB, error) { fmt.Println(host, port, user, password, dbname) + port, err := strconv.Atoi(os.Getenv("DB_PORT")) + if err != nil { + return nil, err + } psqlInfo := fmt.Sprintf( "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname, diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 09391c03..8e7d0d49 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -25,7 +25,7 @@ func main() { } // db - dbConnect, err := db.New(config) + dbConnect, err := db.New(config, slog.With("service", "db")) if err != nil { slog.ErrorContext( ctx, From 96b987967e276c1ed3232b5bd847112887f3a26e Mon Sep 17 00:00:00 2001 From: galyym Date: Wed, 15 Jan 2025 02:35:02 +0500 Subject: [PATCH 23/23] feat: exercise8 --- exercise8/.env-example | 10 ++ exercise8/.gitignore | 8 ++ exercise8/README.md | 26 ++++ exercise8/docker-compose.yml | 50 +++++++ exercise8/go.mod | 10 ++ exercise8/go.sum | 6 + .../internal/api/handler/auth/access_token.go | 115 ++++++++++++++++ exercise8/internal/api/handler/auth/login.go | 124 ++++++++++++++++++ exercise8/internal/api/handler/auth/main.go | 29 ++++ .../internal/api/handler/auth/register.go | 107 +++++++++++++++ .../api/handler/expense/create_expense.go | 50 +++++++ .../api/handler/expense/delete_expense.go | 50 +++++++ .../api/handler/expense/find_expense.go | 54 ++++++++ .../api/handler/expense/find_expenses.go | 72 ++++++++++ .../internal/api/handler/expense/main.go | 18 +++ .../api/handler/expense/update_expense.go | 58 ++++++++ exercise8/internal/api/handler/main.go | 20 +++ exercise8/internal/api/main.go | 54 ++++++++ .../internal/api/middleware/authenticator.go | 63 +++++++++ exercise8/internal/api/middleware/main.go | 15 +++ exercise8/internal/api/router/auth.go | 12 ++ exercise8/internal/api/router/expense.go | 15 +++ exercise8/internal/api/router/main.go | 33 +++++ exercise8/internal/auth/hash.go | 71 ++++++++++ exercise8/internal/auth/main.go | 11 ++ exercise8/internal/auth/tokens.go | 83 ++++++++++++ exercise8/internal/db/auth/access_token.go | 45 +++++++ exercise8/internal/db/auth/login.go | 48 +++++++ exercise8/internal/db/auth/main.go | 28 ++++ exercise8/internal/db/auth/model.go | 14 ++ exercise8/internal/db/auth/register.go | 40 ++++++ .../internal/db/expense/create_expense.go | 38 ++++++ .../internal/db/expense/delete_expense.go | 19 +++ exercise8/internal/db/expense/find_expense.go | 36 +++++ .../internal/db/expense/find_expenses.go | 62 +++++++++ exercise8/internal/db/expense/main.go | 18 +++ exercise8/internal/db/expense/model.go | 14 ++ .../internal/db/expense/update_expense.go | 21 +++ exercise8/internal/db/main.go | 56 ++++++++ ...20250112104121_create_table_users.down.sql | 0 .../20250112104121_create_table_users.up.sql | 8 ++ ...50112133656_create_table_expenses.down.sql | 1 + ...0250112133656_create_table_expenses.up.sql | 7 + exercise8/main.go | 42 ++++++ exercise8/pkg/httputils/request/body.go | 76 +++++++++++ exercise8/pkg/httputils/response/body.go | 33 +++++ exercise8/pkg/httputils/statusError/main.go | 18 +++ 47 files changed, 1788 insertions(+) create mode 100644 exercise8/.env-example create mode 100644 exercise8/.gitignore create mode 100644 exercise8/README.md create mode 100644 exercise8/docker-compose.yml create mode 100644 exercise8/go.mod create mode 100644 exercise8/go.sum create mode 100644 exercise8/internal/api/handler/auth/access_token.go create mode 100644 exercise8/internal/api/handler/auth/login.go create mode 100644 exercise8/internal/api/handler/auth/main.go create mode 100644 exercise8/internal/api/handler/auth/register.go create mode 100644 exercise8/internal/api/handler/expense/create_expense.go create mode 100644 exercise8/internal/api/handler/expense/delete_expense.go create mode 100644 exercise8/internal/api/handler/expense/find_expense.go create mode 100644 exercise8/internal/api/handler/expense/find_expenses.go create mode 100644 exercise8/internal/api/handler/expense/main.go create mode 100644 exercise8/internal/api/handler/expense/update_expense.go create mode 100644 exercise8/internal/api/handler/main.go create mode 100644 exercise8/internal/api/main.go create mode 100644 exercise8/internal/api/middleware/authenticator.go create mode 100644 exercise8/internal/api/middleware/main.go create mode 100644 exercise8/internal/api/router/auth.go create mode 100644 exercise8/internal/api/router/expense.go create mode 100644 exercise8/internal/api/router/main.go create mode 100644 exercise8/internal/auth/hash.go create mode 100644 exercise8/internal/auth/main.go create mode 100644 exercise8/internal/auth/tokens.go create mode 100644 exercise8/internal/db/auth/access_token.go create mode 100644 exercise8/internal/db/auth/login.go create mode 100644 exercise8/internal/db/auth/main.go create mode 100644 exercise8/internal/db/auth/model.go create mode 100644 exercise8/internal/db/auth/register.go create mode 100644 exercise8/internal/db/expense/create_expense.go create mode 100644 exercise8/internal/db/expense/delete_expense.go create mode 100644 exercise8/internal/db/expense/find_expense.go create mode 100644 exercise8/internal/db/expense/find_expenses.go create mode 100644 exercise8/internal/db/expense/main.go create mode 100644 exercise8/internal/db/expense/model.go create mode 100644 exercise8/internal/db/expense/update_expense.go create mode 100644 exercise8/internal/db/main.go create mode 100755 exercise8/internal/db/migrations/20250112104121_create_table_users.down.sql create mode 100755 exercise8/internal/db/migrations/20250112104121_create_table_users.up.sql create mode 100755 exercise8/internal/db/migrations/20250112133656_create_table_expenses.down.sql create mode 100755 exercise8/internal/db/migrations/20250112133656_create_table_expenses.up.sql create mode 100644 exercise8/main.go create mode 100644 exercise8/pkg/httputils/request/body.go create mode 100644 exercise8/pkg/httputils/response/body.go create mode 100644 exercise8/pkg/httputils/statusError/main.go diff --git a/exercise8/.env-example b/exercise8/.env-example new file mode 100644 index 00000000..d64e862d --- /dev/null +++ b/exercise8/.env-example @@ -0,0 +1,10 @@ +APP_NAME=exercise8 +APP_PORT=8000 + +DB_HOST=127.0.0.1 +DB_NAME=exercise8 +DB_USER=exercise8 +DB_PASSWORD=Qwe123 +DB_PORT=5444 + +TOKEN_SECRET=hghhkjljjo878 \ No newline at end of file diff --git a/exercise8/.gitignore b/exercise8/.gitignore new file mode 100644 index 00000000..bcc49434 --- /dev/null +++ b/exercise8/.gitignore @@ -0,0 +1,8 @@ +.env +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +db-data \ No newline at end of file diff --git a/exercise8/README.md b/exercise8/README.md new file mode 100644 index 00000000..9850e33f --- /dev/null +++ b/exercise8/README.md @@ -0,0 +1,26 @@ +# Movie Reservation + +docker compose run + +```shell +$ docker compose --env-file=./.env up --build +``` + +curl for api request + +```shell +$ curl 'http://127.0.0.1:4013/movies' +``` + +migrations + +```shell +docker compose --profile tools run --rm migrate create -ext sql -dir ./migrations NAME_OF_MIGRATION_FILE +docker compose --profile tools run --rm migrate {up,down} +``` + +seeds + +```shell +go run ./internal/cli/... seed +``` diff --git a/exercise8/docker-compose.yml b/exercise8/docker-compose.yml new file mode 100644 index 00000000..1cff1004 --- /dev/null +++ b/exercise8/docker-compose.yml @@ -0,0 +1,50 @@ +services: +# server: +# build: +# context: . +# target: final +# environment: +# - API_PORT=80 +# - DB_HOST=db +# - DB_PORT=5432 +# - DB_NAME=$DB_NAME +# - DB_USER=$DB_USER +# - DB_PASSWORD=$DB_PASSWORD +# ports: +# - ${API_PORT}:80 +# depends_on: +# db: +# condition: service_healthy + + db: + image: postgres + restart: always + volumes: + - ./db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=$DB_NAME + - POSTGRES_USER=$DB_USER + - POSTGRES_PASSWORD=$DB_PASSWORD + ports: + - ${DB_PORT}:5432 + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + migrate: + image: migrate/migrate + profiles: [ "tools" ] + entrypoint: [ + "migrate", + "-path", + "./migrations", + "-database", + "postgres://$DB_USER:$DB_PASSWORD@db:5432/$DB_NAME?sslmode=disable" + ] + volumes: + - ./internal/db/migrations:/migrations + depends_on: + db: + condition: service_healthy diff --git a/exercise8/go.mod b/exercise8/go.mod new file mode 100644 index 00000000..5ad08780 --- /dev/null +++ b/exercise8/go.mod @@ -0,0 +1,10 @@ +module expense_tracker + +go 1.23.3 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 +) + +require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect diff --git a/exercise8/go.sum b/exercise8/go.sum new file mode 100644 index 00000000..f0b2cdb0 --- /dev/null +++ b/exercise8/go.sum @@ -0,0 +1,6 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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/exercise8/internal/api/handler/auth/access_token.go b/exercise8/internal/api/handler/auth/access_token.go new file mode 100644 index 00000000..a4e72448 --- /dev/null +++ b/exercise8/internal/api/handler/auth/access_token.go @@ -0,0 +1,115 @@ +package auth + +import ( + "fmt" + "net/http" + "os" + + "expense_tracker/internal/auth" + dbAuth "expense_tracker/internal/db/auth" + "expense_tracker/pkg/httputils/request" + "expense_tracker/pkg/httputils/response" +) + +type AccessTokenRefreshTokenRequest struct { + RefreshToken string `json:"refresh_token"` +} + +type AccessTokenRequest struct { + Data *AccessTokenRefreshTokenRequest `json:"data"` +} + +type AccessTokenResponse struct { + Data *AuthTokenPair `json:"data"` +} + +func (h *Auth) AccessToken(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "AccessToken") + + // request parse + requestBody := &AccessTokenRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + userData, err := auth.ParseToken(requestBody.Data.RefreshToken, os.Getenv("TOKEN_SECRET")) + if err != nil { + log.ErrorContext( + ctx, + "fail authentication", + "error", err, + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + // db request + dbResp, err := h.db.AccessToken(ctx, &dbAuth.AccessTokenInput{UserID: userData.ID}) + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + if dbResp == nil { + log.ErrorContext( + ctx, + "row is empty", + ) + http.Error(w, "row is empty", http.StatusInternalServerError) + return + } + + tokenPair, err := auth.GenerateTokenPair( + &auth.UserData{ + ID: fmt.Sprint(dbResp.ID), + Email: dbResp.Email, + }, + os.Getenv("TOKEN_SECRET"), + ) + if err != nil { + http.Error(w, fmt.Sprintf("invalid request %w", err), http.StatusBadRequest) + return + } + + // response + respBody := &AccessTokenResponse{ + Data: &AuthTokenPair{ + AccessToken: tokenPair.AccessToken, + RefreshToken: tokenPair.RefreshToken, + }, + } + if err := response.JSON( + w, + http.StatusOK, + respBody, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success generate access token", + ) + return +} diff --git a/exercise8/internal/api/handler/auth/login.go b/exercise8/internal/api/handler/auth/login.go new file mode 100644 index 00000000..be78f38a --- /dev/null +++ b/exercise8/internal/api/handler/auth/login.go @@ -0,0 +1,124 @@ +package auth + +import ( + "fmt" + "net/http" + "os" + + "expense_tracker/internal/auth" + "expense_tracker/pkg/httputils/request" + "expense_tracker/pkg/httputils/response" +) + +type LoginRequest struct { + Data *AuthUser `json:"data"` +} + +type LoginResponse struct { + Data *AuthTokenPair `json:"data"` +} + +func (h *Auth) Login(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "Login") + + // request parse + requestBody := &LoginRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // db request + dbResp, err := h.db.Login(ctx, requestBody.Data.Email) + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + + if dbResp == nil { + log.ErrorContext( + ctx, + "row is empty", + "email", requestBody.Data.Email, + ) + http.Error(w, "invalid credentials", http.StatusBadRequest) + return + } + + // passwordHash and salt + isValid, err := auth.VerifyPassword( + requestBody.Data.Password, + os.Getenv("TOKEN_PEPPER"), + dbResp.PasswordHash, + dbResp.Salt, + ) + if err != nil { + log.ErrorContext( + ctx, + "verify password failed", + "error", err, + ) + http.Error(w, "verify password failed", http.StatusInternalServerError) + return + } + if !isValid { + log.ErrorContext( + ctx, + "invalid credentials", + "email", requestBody.Data.Email, + ) + http.Error(w, "invalid credentials", http.StatusBadRequest) + return + } + + tokenPair, err := auth.GenerateTokenPair( + &auth.UserData{ + ID: fmt.Sprint(dbResp.ID), + Email: dbResp.Email, + }, + os.Getenv("TOKEN_SECRET"), + ) + if err != nil { + http.Error(w, fmt.Sprintf("invalid request %w", err), http.StatusBadRequest) + return + } + + // response + respBody := &LoginResponse{ + Data: &AuthTokenPair{ + AccessToken: tokenPair.AccessToken, + RefreshToken: tokenPair.RefreshToken, + }, + } + if err := response.JSON( + w, + http.StatusOK, + respBody, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success login user", + "email", dbResp.Email, + ) + return +} diff --git a/exercise8/internal/api/handler/auth/main.go b/exercise8/internal/api/handler/auth/main.go new file mode 100644 index 00000000..b0808190 --- /dev/null +++ b/exercise8/internal/api/handler/auth/main.go @@ -0,0 +1,29 @@ +package auth + +import ( + "log/slog" + + "expense_tracker/internal/db" +) + +type Auth struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Auth { + return &Auth{ + logger: logger, + db: db, + } +} + +type AuthUser struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type AuthTokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} diff --git a/exercise8/internal/api/handler/auth/register.go b/exercise8/internal/api/handler/auth/register.go new file mode 100644 index 00000000..2696c64f --- /dev/null +++ b/exercise8/internal/api/handler/auth/register.go @@ -0,0 +1,107 @@ +package auth + +import ( + "fmt" + "net/http" + "os" + + "expense_tracker/internal/auth" + dbAuth "expense_tracker/internal/db/auth" + "expense_tracker/pkg/httputils/request" + "expense_tracker/pkg/httputils/response" +) + +type RegisterRequest struct { + Data *AuthUser `json:"data"` +} + +type RegisterResponse struct { + Data *AuthTokenPair `json:"data"` +} + +func (h *Auth) Register(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "Register") + + // request parse + requestBody := &RegisterRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // passwordHash and salt + passHash, salt, err := auth.HashPassword(requestBody.Data.Password, os.Getenv("TOKEN_PEPPER")) + if err != nil { + log.ErrorContext( + ctx, + "hashing password failed", + "error", err, + ) + http.Error(w, "hashing password failed", http.StatusInternalServerError) + return + } + + // db request + userModel := &dbAuth.ModelUser{ + Email: requestBody.Data.Email, + PasswordHash: passHash, + Salt: salt, + } + dbResp, err := h.db.Register(ctx, &dbAuth.RegisterInput{userModel}) + if err != nil { + log.ErrorContext( + ctx, + "failed to insert user data", + "error", err, + ) + http.Error(w, "failed to insert user data", http.StatusInternalServerError) + return + } + + // create token pair + tokenPair, err := auth.GenerateTokenPair( + &auth.UserData{ + ID: fmt.Sprint(dbResp.ID), + Email: dbResp.Email, + }, + os.Getenv("TOKEN_SECRET"), + ) + + if err != nil { + http.Error(w, fmt.Sprintf("invalid request %w", err), http.StatusBadRequest) + return + } + + respBody := &RegisterResponse{ + Data: &AuthTokenPair{ + AccessToken: tokenPair.AccessToken, + RefreshToken: tokenPair.RefreshToken, + }, + } + if err := response.JSON( + w, + http.StatusOK, + respBody, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success register user", + "user email", userModel.Email, + ) + return +} diff --git a/exercise8/internal/api/handler/expense/create_expense.go b/exercise8/internal/api/handler/expense/create_expense.go new file mode 100644 index 00000000..afa088a9 --- /dev/null +++ b/exercise8/internal/api/handler/expense/create_expense.go @@ -0,0 +1,50 @@ +package expense + +import ( + "expense_tracker/internal/db/expense" + "expense_tracker/pkg/httputils/request" + "expense_tracker/pkg/httputils/response" + "net/http" +) + +type CreateExpenseResponse struct { + Data *expense.CreateModelExpense `json:"data"` +} + +func (h *Expense) CreateExpense(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + log := h.logger.With("CreateExpense") + + requestBody := &CreateExpenseResponse{} + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext(ctx, "failed to parse request body", "error", err) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + dbResp, err := h.db.CreateExpense(ctx, requestBody.Data) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + resp := FindExpenseResponse{ + Data: dbResp, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext(ctx, "success create expense") + return +} diff --git a/exercise8/internal/api/handler/expense/delete_expense.go b/exercise8/internal/api/handler/expense/delete_expense.go new file mode 100644 index 00000000..7fc7b23e --- /dev/null +++ b/exercise8/internal/api/handler/expense/delete_expense.go @@ -0,0 +1,50 @@ +package expense + +import ( + "expense_tracker/internal/db/expense" + "expense_tracker/pkg/httputils/response" + "net/http" + "strconv" +) + +type DeleteExpenseResponse struct { + Data *expense.ModelExpense `json:"data"` +} + +func (h *Expense) DeleteExpense(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + log := h.logger.With("DeleteExpense") + + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + log.ErrorContext( + ctx, + "failed parse query offset", + "error", err, + ) + http.Error(w, "invalid query offset", http.StatusBadRequest) + return + } + + err = h.db.DeleteExpense(ctx, id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + if err := response.JSON( + w, + http.StatusOK, + nil, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext(ctx, "success deleted expense") + return +} diff --git a/exercise8/internal/api/handler/expense/find_expense.go b/exercise8/internal/api/handler/expense/find_expense.go new file mode 100644 index 00000000..9bbd443b --- /dev/null +++ b/exercise8/internal/api/handler/expense/find_expense.go @@ -0,0 +1,54 @@ +package expense + +import ( + "expense_tracker/internal/db/expense" + "expense_tracker/pkg/httputils/response" + "net/http" + "strconv" +) + +type FindExpenseResponse struct { + Data *expense.ModelExpense `json:"data"` +} + +func (h *Expense) FindExpense(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + log := h.logger.With("FindExpense") + + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + log.ErrorContext( + ctx, + "failed parse query offset", + "error", err, + ) + http.Error(w, "invalid query offset", http.StatusBadRequest) + return + } + + dbData, err := h.db.FindExpense(ctx, id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + resp := FindExpenseResponse{ + Data: dbData, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext(ctx, "success find expense", "successfully get expense") + return +} diff --git a/exercise8/internal/api/handler/expense/find_expenses.go b/exercise8/internal/api/handler/expense/find_expenses.go new file mode 100644 index 00000000..a60dae47 --- /dev/null +++ b/exercise8/internal/api/handler/expense/find_expenses.go @@ -0,0 +1,72 @@ +package expense + +import ( + "expense_tracker/internal/db/expense" + "expense_tracker/pkg/httputils/response" + "net/http" + "strconv" + "time" +) + +type FindExpensesResponse struct { + Data []expense.ModelExpense `json:"data"` +} + +func (h *Expense) FindExpenses(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + log := h.logger.With("FindExpenses") + + query := r.URL.Query() + offset, err := strconv.Atoi(query.Get("offset")) + if err != nil { + log.ErrorContext( + ctx, + "failed parse query offset", + "error", err, + ) + http.Error(w, "invalid query offset", http.StatusBadRequest) + return + } + + limit, err := strconv.Atoi(query.Get("limit")) + if err != nil { + log.ErrorContext( + ctx, + "failed parse query limit", + "error", err, + ) + http.Error(w, "invalid query limit", http.StatusBadRequest) + return + } + + filter := query.Get("filter") + if filter == "" { + filter = time.Time{}.String() + } + + dbData, err := h.db.FindExpenses(ctx, limit, offset, filter) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + resp := FindExpensesResponse{ + Data: dbData, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext(ctx, "success find expenses", "number of expenses", len(resp.Data)) + return +} diff --git a/exercise8/internal/api/handler/expense/main.go b/exercise8/internal/api/handler/expense/main.go new file mode 100644 index 00000000..db615f92 --- /dev/null +++ b/exercise8/internal/api/handler/expense/main.go @@ -0,0 +1,18 @@ +package expense + +import ( + "expense_tracker/internal/db" + "log/slog" +) + +type Expense struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Expense { + return &Expense{ + logger: logger, + db: db, + } +} diff --git a/exercise8/internal/api/handler/expense/update_expense.go b/exercise8/internal/api/handler/expense/update_expense.go new file mode 100644 index 00000000..7b1437da --- /dev/null +++ b/exercise8/internal/api/handler/expense/update_expense.go @@ -0,0 +1,58 @@ +package expense + +import ( + "expense_tracker/internal/db/expense" + "expense_tracker/pkg/httputils/request" + "expense_tracker/pkg/httputils/response" + "net/http" + "strconv" +) + +type UpdateExpenseResponse struct { + Data *expense.ModelExpense `json:"data"` +} + +func (h *Expense) UpdateExpense(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + log := h.logger.With("UpdateExpense") + + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + log.ErrorContext( + ctx, + "failed parse query offset", + "error", err, + ) + http.Error(w, "invalid query offset", http.StatusBadRequest) + return + } + + requestBody := &UpdateExpenseResponse{} + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext(ctx, "failed to parse request body", "error", err) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + err = h.db.UpdateExpense(ctx, requestBody.Data, id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + if err := response.JSON( + w, + http.StatusOK, + nil, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext(ctx, "success create expense") + return +} diff --git a/exercise8/internal/api/handler/main.go b/exercise8/internal/api/handler/main.go new file mode 100644 index 00000000..dd1c0d8d --- /dev/null +++ b/exercise8/internal/api/handler/main.go @@ -0,0 +1,20 @@ +package handler + +import ( + "expense_tracker/internal/api/handler/auth" + "expense_tracker/internal/api/handler/expense" + "expense_tracker/internal/db" + "log/slog" +) + +type Handler struct { + *expense.Expense + *auth.Auth +} + +func New(logger *slog.Logger, db *db.DB) *Handler { + return &Handler{ + Expense: expense.New(logger, db), + Auth: auth.New(logger, db), + } +} diff --git a/exercise8/internal/api/main.go b/exercise8/internal/api/main.go new file mode 100644 index 00000000..b6ae62d9 --- /dev/null +++ b/exercise8/internal/api/main.go @@ -0,0 +1,54 @@ +package api + +import ( + "context" + "errors" + "expense_tracker/internal/api/handler" + "expense_tracker/internal/api/middleware" + "expense_tracker/internal/api/router" + "expense_tracker/internal/db" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "strconv" +) + +type Api struct { + logger *slog.Logger + router *router.Router +} + +func New(logger *slog.Logger, db *db.DB) *Api { + midd := middleware.New(logger) + h := handler.New(logger, db) + r := router.New(logger, h, midd) + + return &Api{ + logger: logger, + router: r, + } +} + +func (a *Api) Start(ctx context.Context) error { + mux := a.router.Start(ctx) + + port, err := strconv.Atoi(os.Getenv("APP_PORT")) + if err != nil { + return err + } + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + BaseContext: func(_ net.Listener) context.Context { return ctx }, + } + + fmt.Printf("Starting server on :%d\n", port) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + + return nil +} diff --git a/exercise8/internal/api/middleware/authenticator.go b/exercise8/internal/api/middleware/authenticator.go new file mode 100644 index 00000000..ac0f6e88 --- /dev/null +++ b/exercise8/internal/api/middleware/authenticator.go @@ -0,0 +1,63 @@ +package middleware + +import ( + "context" + "expense_tracker/internal/auth" + "net/http" + "os" + "strings" +) + +func (m *Middleware) Authenticator(next http.Handler) http.Handler { + h := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := m.logger.With("middleware", "Authorization") + + authenticatorHeader := r.Header.Get("Authorization") + if authenticatorHeader == "" { + log.ErrorContext( + ctx, + "authenticator header is empty", + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + if !strings.HasPrefix(authenticatorHeader, "Bearer ") { + log.ErrorContext( + ctx, + "invalid authenticator header", + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + tokenString := authenticatorHeader[len("Bearer "):] + userData, err := auth.ParseToken(tokenString, os.Getenv("TOKEN_SECRET")) + if err != nil { + log.ErrorContext( + ctx, + "fail authentication", + "error", err, + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + newCtx := context.WithValue(ctx, "user", userData) + next.ServeHTTP(w, r.WithContext(newCtx)) + } + return http.HandlerFunc(h) +} diff --git a/exercise8/internal/api/middleware/main.go b/exercise8/internal/api/middleware/main.go new file mode 100644 index 00000000..75cc9648 --- /dev/null +++ b/exercise8/internal/api/middleware/main.go @@ -0,0 +1,15 @@ +package middleware + +import ( + "log/slog" +) + +type Middleware struct { + logger *slog.Logger +} + +func New(logger *slog.Logger) *Middleware { + return &Middleware{ + logger: logger, + } +} diff --git a/exercise8/internal/api/router/auth.go b/exercise8/internal/api/router/auth.go new file mode 100644 index 00000000..a861e07b --- /dev/null +++ b/exercise8/internal/api/router/auth.go @@ -0,0 +1,12 @@ +package router + +import ( + "context" +) + +func (r *Router) auth(ctx context.Context) { + r.router.HandleFunc("POST /register", r.handler.Register) + r.router.HandleFunc("POST /login", r.handler.Login) + + r.router.HandleFunc("POST /access-token", r.handler.AccessToken) +} diff --git a/exercise8/internal/api/router/expense.go b/exercise8/internal/api/router/expense.go new file mode 100644 index 00000000..6a76c76f --- /dev/null +++ b/exercise8/internal/api/router/expense.go @@ -0,0 +1,15 @@ +package router + +import ( + "context" + "net/http" +) + +func (r *Router) expense(ctx context.Context) { + r.router.Handle("GET /expenses", r.mid.Authenticator(http.HandlerFunc(r.handler.FindExpenses))) + r.router.Handle("GET /expense/{id}", r.mid.Authenticator(http.HandlerFunc(r.handler.FindExpense))) + r.router.Handle("POST /expense", r.mid.Authenticator(http.HandlerFunc(r.handler.CreateExpense))) + r.router.Handle("PUT /expense/{id}", r.mid.Authenticator(http.HandlerFunc(r.handler.UpdateExpense))) + r.router.Handle("DELETE /expense/{id}", r.mid.Authenticator(http.HandlerFunc(r.handler.DeleteExpense))) + +} diff --git a/exercise8/internal/api/router/main.go b/exercise8/internal/api/router/main.go new file mode 100644 index 00000000..8fafc2b7 --- /dev/null +++ b/exercise8/internal/api/router/main.go @@ -0,0 +1,33 @@ +package router + +import ( + "context" + "expense_tracker/internal/api/handler" + "expense_tracker/internal/api/middleware" + "log/slog" + "net/http" +) + +type Router struct { + logger *slog.Logger + router *http.ServeMux + handler *handler.Handler + mid *middleware.Middleware +} + +func New(logger *slog.Logger, handler *handler.Handler, mid *middleware.Middleware) *Router { + mux := http.NewServeMux() + + return &Router{ + logger: logger, + router: mux, + handler: handler, + mid: mid, + } +} + +func (r *Router) Start(ctx context.Context) *http.ServeMux { + r.expense(ctx) + r.auth(ctx) + return r.router +} diff --git a/exercise8/internal/auth/hash.go b/exercise8/internal/auth/hash.go new file mode 100644 index 00000000..016883aa --- /dev/null +++ b/exercise8/internal/auth/hash.go @@ -0,0 +1,71 @@ +package auth + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "fmt" +) + +const ( + // Recommended minimum lengths for security + SaltLength = 16 // 16 bytes = 128 bits + PepperLength = 32 // 32 bytes = 256 bits + HashLength = 64 // SHA-512 outputs 64 bytes + Iterations = 210000 // Recommended minimum iterations for PBKDF2 +) + +func HashPassword(password string, pepper string) (string, string, error) { + // Generate random salt + salt := make([]byte, SaltLength) + if _, err := rand.Read(salt); err != nil { + return "", "", fmt.Errorf("error generating salt: %w", err) + } + + // Hash the password + hash := hashWithSaltAndPepper([]byte(password), salt, []byte(pepper)) + + return base64.StdEncoding.EncodeToString(hash), base64.StdEncoding.EncodeToString(salt), nil +} + +func hashWithSaltAndPepper(password, salt, pepper []byte) []byte { + // Combine password and pepper + pepperedPassword := make([]byte, len(password)+len(pepper)) + copy(pepperedPassword, password) + copy(pepperedPassword[len(password):], pepper) + + // Initialize HMAC-SHA512 + hash := hmac.New(sha512.New, salt) + + // Perform multiple iterations of hashing + result := pepperedPassword + for i := 0; i < Iterations; i++ { + hash.Reset() + hash.Write(result) + result = hash.Sum(nil) + } + + return result +} + +// VerifyPassword checks if a password matches its hash +func VerifyPassword(password, pepper, hash, salt string) (bool, error) { + // Decode salt and hash from base64 + decodedSalt, err := base64.StdEncoding.DecodeString(salt) + if err != nil { + return false, fmt.Errorf("error decoding salt: %w", err) + } + + decodedHash, err := base64.StdEncoding.DecodeString(hash) + if err != nil { + return false, fmt.Errorf("error decoding hash: %w", err) + } + + // Hash the provided password with the same salt + newHash := hashWithSaltAndPepper([]byte(password), decodedSalt, []byte(pepper)) + + // Compare hashes in constant time to prevent timing attacks + return subtle.ConstantTimeCompare(newHash, decodedHash) == 1, nil +} diff --git a/exercise8/internal/auth/main.go b/exercise8/internal/auth/main.go new file mode 100644 index 00000000..3cf5f8ba --- /dev/null +++ b/exercise8/internal/auth/main.go @@ -0,0 +1,11 @@ +package auth + +type Tokens struct { + RefreshToken string `json:"refresh_token"` + AccessToken string `json:"access_token"` +} + +type UserData struct { + ID string `json:"id"` + Email string `json:"email"` +} diff --git a/exercise8/internal/auth/tokens.go b/exercise8/internal/auth/tokens.go new file mode 100644 index 00000000..a28ba08b --- /dev/null +++ b/exercise8/internal/auth/tokens.go @@ -0,0 +1,83 @@ +package auth + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +func GenerateTokenPair(user *UserData, secret string) (*Tokens, error) { + // Create token + token := jwt.New(jwt.SigningMethodHS256) + + // Set claims + // This is the information which frontend can use + // The backend can also decode the token and get admin etc. + claims := token.Claims.(jwt.MapClaims) + claims["sub"] = user.ID + claims["email"] = user.Email + claims["exp"] = time.Now().Add(time.Minute * 600).Unix() + + // Generate encoded token and send it as response. + // The signing string should be secret (a generated UUID works too) + t, err := token.SignedString([]byte(secret)) + if err != nil { + return nil, err + } + + refreshToken := jwt.New(jwt.SigningMethodHS256) + rtClaims := refreshToken.Claims.(jwt.MapClaims) + rtClaims["sub"] = user.ID + rtClaims["exp"] = time.Now().Add(time.Hour * 24).Unix() + + rt, err := refreshToken.SignedString([]byte(secret)) + if err != nil { + return nil, err + } + + return &Tokens{ + AccessToken: t, + RefreshToken: rt, + }, nil +} + +func ParseToken(token string, secret string) (*UserData, error) { + t, err := jwt.Parse( + token, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }, + ) + + switch { + case t == nil: + return nil, fmt.Errorf("invalid token") + case t.Valid: + claims, ok := t.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid token") + } + + id, err := claims.GetSubject() + if err != nil { + return nil, fmt.Errorf("invalid token") + } + email, ok := claims["email"].(string) + + return &UserData{ + ID: id, + Email: email, + }, nil + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, fmt.Errorf("invalid token") + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + // Invalid signature + return nil, fmt.Errorf("Invalid signature") + case errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet): + // Token is either expired or not active yet + return nil, err + default: + return nil, fmt.Errorf("invalid token") + } +} diff --git a/exercise8/internal/db/auth/access_token.go b/exercise8/internal/db/auth/access_token.go new file mode 100644 index 00000000..b3862feb --- /dev/null +++ b/exercise8/internal/db/auth/access_token.go @@ -0,0 +1,45 @@ +package auth + +import ( + "context" + "database/sql" + "errors" +) + +type AccessTokenInput struct { + UserID string +} + +func (m *Auth) AccessToken(ctx context.Context, inp *AccessTokenInput) (*ModelUser, error) { + log := m.logger.With("method", "AccessToken") + + stmt := ` +SELECT id, email +FROM users +WHERE id = $1; +` + + row := m.db.QueryRowContext(ctx, stmt, inp.UserID) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table user", "error", err) + return nil, err + } + + user := ModelUser{} + + if err := row.Scan( + &user.ID, + &user.Email, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.ErrorContext(ctx, "no user found", "error", err) + return nil, nil + } + + log.ErrorContext(ctx, "fail to scan user", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success select user") + return &user, nil +} diff --git a/exercise8/internal/db/auth/login.go b/exercise8/internal/db/auth/login.go new file mode 100644 index 00000000..a9a920d9 --- /dev/null +++ b/exercise8/internal/db/auth/login.go @@ -0,0 +1,48 @@ +package auth + +import ( + "context" + "database/sql" + "errors" +) + +type LoginInput struct { + User *ModelUser +} + +func (m *Auth) Login(ctx context.Context, email string) (*ModelUser, error) { + log := m.logger.With("method", "Login") + + stmt := ` +SELECT id, email, password_hash, salt +FROM users +WHERE email = $1; +` + + row := m.db.QueryRowContext(ctx, stmt, email) + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table user", "error", err) + return nil, err + } + + user := ModelUser{} + + if err := row.Scan( + &user.ID, + &user.Email, + &user.PasswordHash, + &user.Salt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.WarnContext(ctx, "no user found", "error", err) + return nil, nil + } + + log.ErrorContext(ctx, "fail to scan user", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success login user") + return &user, nil +} diff --git a/exercise8/internal/db/auth/main.go b/exercise8/internal/db/auth/main.go new file mode 100644 index 00000000..d92f78f0 --- /dev/null +++ b/exercise8/internal/db/auth/main.go @@ -0,0 +1,28 @@ +package auth + +import ( + "database/sql" + "log/slog" +) + +type Auth struct { + logger *slog.Logger + db *sql.DB +} + +func New(logger *slog.Logger, db *sql.DB) *Auth { + return &Auth{ + logger: logger, + db: db, + } +} + +type AuthUser struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type AuthTokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} diff --git a/exercise8/internal/db/auth/model.go b/exercise8/internal/db/auth/model.go new file mode 100644 index 00000000..3d9b81f9 --- /dev/null +++ b/exercise8/internal/db/auth/model.go @@ -0,0 +1,14 @@ +package auth + +import ( + "time" +) + +type ModelUser struct { + ID int `json:"id"` + Email string `json:"email"` + PasswordHash string `json:"-"` + Salt string `json:"-"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/exercise8/internal/db/auth/register.go b/exercise8/internal/db/auth/register.go new file mode 100644 index 00000000..d4c9c725 --- /dev/null +++ b/exercise8/internal/db/auth/register.go @@ -0,0 +1,40 @@ +package auth + +import ( + "context" +) + +type RegisterInput struct { + User *ModelUser +} + +func (m *Auth) Register(ctx context.Context, inp *RegisterInput) (*ModelUser, error) { + log := m.logger.With("method", "Register") + + stmt := ` +INSERT INTO users (email, password_hash, salt) +VALUES ($1, $2, $3) +RETURNING id, email, password_hash, salt; +` + + row := m.db.QueryRowContext(ctx, stmt, inp.User.Email, inp.User.PasswordHash, inp.User.Salt) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table user", "error", err) + return nil, err + } + + user := ModelUser{} + + if err := row.Scan( + &user.ID, + &user.Email, + &user.PasswordHash, + &user.Salt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan user", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success register user") + return &user, nil +} diff --git a/exercise8/internal/db/expense/create_expense.go b/exercise8/internal/db/expense/create_expense.go new file mode 100644 index 00000000..00467a35 --- /dev/null +++ b/exercise8/internal/db/expense/create_expense.go @@ -0,0 +1,38 @@ +package expense + +import ( + "context" +) + +func (d *Expense) CreateExpense(ctx context.Context, expenseData *CreateModelExpense) (*ModelExpense, error) { + log := d.logger.With("method", "CreateExpenses") + + stmt := `INSERT INTO expenses (amount, description) + VALUES ($1, $2) + RETURNING id, amount, description, created_at, updated_at` + + row := d.db.QueryRowContext(ctx, stmt, expenseData.Amount, expenseData.Description) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table expenses", "error", err) + return nil, err + } + + expense := ModelExpense{} + if err := row.Scan( + &expense.ID, + &expense.Amount, + &expense.Description, + &expense.CreatedAt, + &expense.UpdatedAt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan expense", "error", err) + return nil, err + } + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + return &expense, nil +} diff --git a/exercise8/internal/db/expense/delete_expense.go b/exercise8/internal/db/expense/delete_expense.go new file mode 100644 index 00000000..c77f3a4d --- /dev/null +++ b/exercise8/internal/db/expense/delete_expense.go @@ -0,0 +1,19 @@ +package expense + +import ( + "context" +) + +func (d *Expense) DeleteExpense(ctx context.Context, id int) error { + log := d.logger.With("method", "DeleteExpense") + + stmt := `DELETE FROM expenses WHERE id = $1` + + row := d.db.QueryRowContext(ctx, stmt, id) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table expenses", "error", err) + return err + } + + return nil +} diff --git a/exercise8/internal/db/expense/find_expense.go b/exercise8/internal/db/expense/find_expense.go new file mode 100644 index 00000000..1da7d7c4 --- /dev/null +++ b/exercise8/internal/db/expense/find_expense.go @@ -0,0 +1,36 @@ +package expense + +import ( + "context" +) + +func (d *Expense) FindExpense(ctx context.Context, id int) (*ModelExpense, error) { + log := d.logger.With("method", "FindExpenses") + + stmt := `SELECT * FROM expenses WHERE id = $1` + + row := d.db.QueryRowContext(ctx, stmt, id) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table expenses", "error", err) + return nil, err + } + + expense := ModelExpense{} + if err := row.Scan( + &expense.ID, + &expense.Amount, + &expense.Description, + &expense.CreatedAt, + &expense.UpdatedAt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan expense", "error", err) + return nil, err + } + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + return &expense, nil +} diff --git a/exercise8/internal/db/expense/find_expenses.go b/exercise8/internal/db/expense/find_expenses.go new file mode 100644 index 00000000..33979b79 --- /dev/null +++ b/exercise8/internal/db/expense/find_expenses.go @@ -0,0 +1,62 @@ +package expense + +import ( + "context" + "time" +) + +func (d *Expense) FindExpenses(ctx context.Context, limit, offset int, filter string) ([]ModelExpense, error) { + log := d.logger.With("method", "FindExpenses") + + expenses := make([]ModelExpense, 0) + + stmt := `SELECT * FROM expenses ` + var filterTime time.Time + if filter != "" { + stmt += `WHERE created_at > $3 ` + now := time.Now() + switch filter { + case "past_week": + filterTime = now.AddDate(0, 0, -7) + break + case "past_month": + filterTime = now.AddDate(0, -1, 0) + break + case "last_3_months": + filterTime = now.AddDate(0, -3, 0) + break + } + } + stmt += `ORDER BY id DESC OFFSET $1 LIMIT $2` + + rows, err := d.db.QueryContext(ctx, stmt, offset, limit, filterTime) + if err != nil { + log.ErrorContext(ctx, "fail to query table expenses", "error", err) + return nil, err + } + + defer rows.Close() + + for rows.Next() { + expense := ModelExpense{} + + if err := rows.Scan( + &expense.ID, + &expense.Amount, + &expense.Description, + &expense.CreatedAt, + &expense.UpdatedAt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan expense", "error", err) + return nil, err + } + expenses = append(expenses, expense) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + return expenses, nil +} diff --git a/exercise8/internal/db/expense/main.go b/exercise8/internal/db/expense/main.go new file mode 100644 index 00000000..05e5761e --- /dev/null +++ b/exercise8/internal/db/expense/main.go @@ -0,0 +1,18 @@ +package expense + +import ( + "database/sql" + "log/slog" +) + +type Expense struct { + logger *slog.Logger + db *sql.DB +} + +func New(logger *slog.Logger, db *sql.DB) *Expense { + return &Expense{ + logger: logger, + db: db, + } +} diff --git a/exercise8/internal/db/expense/model.go b/exercise8/internal/db/expense/model.go new file mode 100644 index 00000000..efba0114 --- /dev/null +++ b/exercise8/internal/db/expense/model.go @@ -0,0 +1,14 @@ +package expense + +type ModelExpense struct { + ID int `json:"id"` + Amount float64 `json:"amount"` + Description string `json:"description"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type CreateModelExpense struct { + Amount float64 `json:"amount"` + Description string `json:"description"` +} diff --git a/exercise8/internal/db/expense/update_expense.go b/exercise8/internal/db/expense/update_expense.go new file mode 100644 index 00000000..2a790dbd --- /dev/null +++ b/exercise8/internal/db/expense/update_expense.go @@ -0,0 +1,21 @@ +package expense + +import ( + "context" +) + +func (d *Expense) UpdateExpense(ctx context.Context, expenseData *ModelExpense, id int) error { + log := d.logger.With("method", "UpdateExpense") + + stmt := `UPDATE expenses + SET amount = $2, description = $3 + WHERE id = $1` + + row := d.db.QueryRowContext(ctx, stmt, id, expenseData.Amount, expenseData.Description) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table expenses", "error", err) + return err + } + + return nil +} diff --git a/exercise8/internal/db/main.go b/exercise8/internal/db/main.go new file mode 100644 index 00000000..d4244c0b --- /dev/null +++ b/exercise8/internal/db/main.go @@ -0,0 +1,56 @@ +package db + +import ( + "database/sql" + "expense_tracker/internal/db/auth" + "expense_tracker/internal/db/expense" + "fmt" + _ "github.com/lib/pq" + "log/slog" + "os" + "strconv" +) + +type DB struct { + logger *slog.Logger + pg *sql.DB + *auth.Auth + *expense.Expense +} + +func New(logger *slog.Logger) (*DB, error) { + pgSql, err := NewPgSQL() + if err != nil { + return nil, err + } + + return &DB{ + logger: logger, + pg: pgSql, + Auth: auth.New(logger, pgSql), + Expense: expense.New(logger, pgSql), + }, 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") + + pgInfo := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname, + ) + + db, err := sql.Open("postgres", pgInfo) + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/exercise8/internal/db/migrations/20250112104121_create_table_users.down.sql b/exercise8/internal/db/migrations/20250112104121_create_table_users.down.sql new file mode 100755 index 00000000..e69de29b diff --git a/exercise8/internal/db/migrations/20250112104121_create_table_users.up.sql b/exercise8/internal/db/migrations/20250112104121_create_table_users.up.sql new file mode 100755 index 00000000..97183958 --- /dev/null +++ b/exercise8/internal/db/migrations/20250112104121_create_table_users.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/exercise8/internal/db/migrations/20250112133656_create_table_expenses.down.sql b/exercise8/internal/db/migrations/20250112133656_create_table_expenses.down.sql new file mode 100755 index 00000000..b6b6cc55 --- /dev/null +++ b/exercise8/internal/db/migrations/20250112133656_create_table_expenses.down.sql @@ -0,0 +1 @@ +DROP TABLE expenses; \ No newline at end of file diff --git a/exercise8/internal/db/migrations/20250112133656_create_table_expenses.up.sql b/exercise8/internal/db/migrations/20250112133656_create_table_expenses.up.sql new file mode 100755 index 00000000..e4f7bbe8 --- /dev/null +++ b/exercise8/internal/db/migrations/20250112133656_create_table_expenses.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE expenses ( + id SERIAL PRIMARY KEY, + amount DECIMAL(10, 2) NOT NULL, + description TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/exercise8/main.go b/exercise8/main.go new file mode 100644 index 00000000..4c87c14d --- /dev/null +++ b/exercise8/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "expense_tracker/internal/api" + "expense_tracker/internal/db" + "github.com/joho/godotenv" + "log/slog" + "os" + "os/signal" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + _ = godotenv.Load() + + d, err := db.New(slog.With("service", "db")) + if err != nil { + panic(err) + } + + a := api.New(slog.With("service", "api"), d) + + go func(ctx context.Context, cancelFunc context.CancelFunc) { + if err := a.Start(ctx); err != nil { + slog.ErrorContext(ctx, "failed to start api", "error", err.Error()) + } + cancelFunc() + }(ctx, cancel) + + go func(cancelFunc context.CancelFunc) { + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, os.Interrupt) + + sig := <-shutdown + slog.WarnContext(ctx, "signal received - shutting down...", "signal", sig) + + cancelFunc() + }(cancel) + + <-ctx.Done() +} diff --git a/exercise8/pkg/httputils/request/body.go b/exercise8/pkg/httputils/request/body.go new file mode 100644 index 00000000..2c829e60 --- /dev/null +++ b/exercise8/pkg/httputils/request/body.go @@ -0,0 +1,76 @@ +package request + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "expense_tracker/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/exercise8/pkg/httputils/response/body.go b/exercise8/pkg/httputils/response/body.go new file mode 100644 index 00000000..e1fd78a8 --- /dev/null +++ b/exercise8/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/exercise8/pkg/httputils/statusError/main.go b/exercise8/pkg/httputils/statusError/main.go new file mode 100644 index 00000000..6cf4e1b6 --- /dev/null +++ b/exercise8/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 +}