From aee8a9ad9d38d7195b90f64937a37ac12b8b5eb4 Mon Sep 17 00:00:00 2001 From: Alinurmyrzakhanov Date: Tue, 24 Sep 2024 13:54:57 +0500 Subject: [PATCH 1/6] Alinur Myrzakhanov --- exercise1/README.md | 2 +- exercise1/problem1/main.go | 8 +++++++- exercise1/problem10/main.go | 19 ++++++++++++++++- exercise1/problem2/main.go | 19 ++++++++++++++++- exercise1/problem3/main.go | 8 +++++++- exercise1/problem4/main.go | 12 ++++++++++- exercise1/problem5/main.go | 6 +++++- exercise1/problem6/main.go | 16 ++++++++++++++- exercise1/problem7/main.go | 13 +++++++++++- exercise1/problem8/main.go | 13 +++++++++++- exercise1/problem9/main.go | 12 ++++++++--- exercise2/problem1/problem1.go | 15 +++++++++++++- exercise2/problem10/problem10.go | 11 +++++++++- exercise2/problem11/problem11.go | 14 ++++++++++++- exercise2/problem12/problem12.go | 23 ++++++++++++++++++++- exercise2/problem2/problem2.go | 8 +++++++- exercise2/problem3/problem3.go | 35 +++++++++++++++++++++++++++++++- exercise2/problem4/problem4.go | 7 ++++++- exercise2/problem5/problem5.go | 21 ++++++++++++++++++- exercise2/problem6/problem6.go | 13 +++++++++++- exercise2/problem7/problem7.go | 5 ++++- exercise2/problem8/problem8.go | 5 +---- exercise2/problem9/problem9.go | 10 ++++++++- 23 files changed, 267 insertions(+), 28 deletions(-) diff --git a/exercise1/README.md b/exercise1/README.md index 888bfca8..24c5bb88 100644 --- a/exercise1/README.md +++ b/exercise1/README.md @@ -1,6 +1,6 @@ # Exercise 1 -Please provide solution for the following problems * are mandatory +Please provide solution for the following problems * is mandatory 1. [problem1](./problem1/README.md) * 2. [problem2](./problem2/README.md) * diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..5840b2b6 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,9 @@ package main -func addUp() {} +func addUp(a int) int { + sum := 0 + for i := 0; i <= a; i++ { + sum += i + } + return sum +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..f100b7b8 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,20 @@ package main -func sum() {} +import ( + "fmt" + "strconv" +) + +func sum(a, b string) (string, error) { + numA, errA := strconv.Atoi(a) + if errA != nil { + return "", fmt.Errorf("string: %s cannot be converted", a) + } + numB, errB := strconv.Atoi(b) + if errB != nil { + return "", fmt.Errorf("string: %s cannot be converted", b) + } + sum := numA + numB + + return strconv.Itoa(sum), nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..a7d45c27 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,20 @@ package main -func binary() {} +import "fmt" + +func binary(a int) string { + bi := "" + if a == 0 { + return "0" + } + for a > 0 { + bit := a % 2 + bi += fmt.Sprintf("%d", bit) + a /= 2 + } + runedbi := []rune(bi) + for i := 0; i < len(runedbi)/2; i++ { + runedbi[i], runedbi[len(runedbi)-1-i] = runedbi[len(runedbi)-1-i], runedbi[i] + } + return string(runedbi) +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..7204877e 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,9 @@ package main -func numberSquares() {} +func numberSquares(n int) int { + squares := 0 + for i := 1; i <= n; i++ { + squares += (n - i + 1) * (n - i + 1) + } + return squares +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..27b3a3a3 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,13 @@ package main -func detectWord() {} +import "unicode" + +func detectWord(a string) string { + sum := "" + for _, char := range a { + if unicode.IsLower(char) { + sum += string(char) + } + } + return sum +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..55c12381 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,7 @@ package main -func potatoes() {} +import "strings" + +func potatoes(a string) int { + return strings.Count(a, "potato") +} diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..097b62cf 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,17 @@ package main -func emojify() {} +import "strings" + +func emojify(a string) string { + slovavsmile := map[string]string{ + "smile": "🙂", + "grin": "😀", + "sad": "😥", + "mad": "😠", + } + for i, slovo := range slovavsmile { + a = strings.ReplaceAll(a, i, slovo) + } + + return a +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..fdd79680 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,14 @@ package main -func highestDigit() {} +func highestDigit(a int) int { + max := 0 + m := 0 + for a > 0 { + m = a % 10 + if m > max { + max = m + } + a /= 10 + } + return max +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..d31235bf 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,14 @@ package main -func countVowels() {} +import "strings" + +func countVowels(a string) int { + vowels := "aeiouAEIOU" + sum := 0 + for _, ch := range a { + if strings.ContainsRune(vowels, ch) { + sum++ + } + } + return sum +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..64f74a97 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,13 @@ package main -func bitwiseAND() {} +func bitwiseAND(a, b int) int { + return a & b +} -func bitwiseOR() {} +func bitwiseOR(a, b int) int { + return a | b +} -func bitwiseXOR() {} +func bitwiseXOR(a, b int) int { + return a ^ b +} diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..de882b0f 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,17 @@ package problem1 -func isChangeEnough() { +func isChangeEnough(change [4]int, amountDue float32) bool { + quartersValue := 25 + dimesValue := 10 + nickelsValue := 5 + pennieValue := 1 + + totalCents := (change[0] * quartersValue) + + (change[1] * dimesValue) + + (change[2] * nickelsValue) + + (change[3] * pennieValue) + + amountDueCents := int(amountDue * 100) + + return totalCents >= amountDueCents } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..4d7857f7 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,12 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(string) func(int)) { + brands := make(map[string]int) + makeBrand := func(brand string) func(int) { + brands[brand] = 0 + return func(count int) { + brands[brand] += count + } + } + return brands, makeBrand +} diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..a490e27b 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,15 @@ package problem11 -func removeDups() {} +func removeDups[T comparable](items []T) []T { + seen := make(map[T]bool) + result := []T{} + + for _, item := range items { + if !seen[item] { + seen[item] = true + result = append(result, item) + } + } + + return result +} diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..b8f4a121 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,24 @@ package problem11 -func keysAndValues() {} +import ( + "fmt" + "sort" +) + +func keysAndValues[K comparable, V any](m map[K]V) ([]K, []V) { + keys := make([]K, 0, len(m)) + values := make([]V, 0, len(m)) + for k, v := range m { + keys = append(keys, k) + values = append(values, v) + } + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprintf("%v", keys[i]) < fmt.Sprintf("%v", keys[j]) + }) + sortedValues := make([]V, len(keys)) + for i, k := range keys { + sortedValues[i] = m[k] + } + + return keys, sortedValues +} diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..7e2cb35d 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,10 @@ package problem2 -func capitalize() { +import "strings" + +func capitalize(names []string) []string { + for i, name := range names { + names[i] = strings.Title(strings.ToLower(name)) + } + return names } diff --git a/exercise2/problem3/problem3.go b/exercise2/problem3/problem3.go index f183fafb..03a722e0 100644 --- a/exercise2/problem3/problem3.go +++ b/exercise2/problem3/problem3.go @@ -9,5 +9,38 @@ const ( lr dir = "lr" ) -func diagonalize() { +func diagonalize(n int, direction dir) [][]int { + matrix := make([][]int, n) + for i := range matrix { + matrix[i] = make([]int, n) + } + + switch direction { + case ul: + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = i + j + } + } + case ur: + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = (n - 1 - j) + i + } + } + case ll: + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = (n - 1 - i) + j + } + } + case lr: + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = (n - 1 - i) + (n - 1 - j) + } + } + } + + return matrix } diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..03b83532 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,9 @@ package problem4 -func mapping() { +func mapping(letters []string) map[string]string { + result := make(map[string]string) + for _, letter := range letters { + result[letter] = string(letter[0] - 32) + } + return result } diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..b013d281 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,23 @@ package problem5 -func products() { +import "sort" + +func products(prices map[string]int, minPrice int) []string { + + var result []string + + for product, price := range prices { + if price >= minPrice { + result = append(result, product) + } + } + + sort.Slice(result, func(i, j int) bool { + if prices[result[i]] == prices[result[j]] { + return result[i] < result[j] + } + return prices[result[i]] < prices[result[j]] + }) + + return result } diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..1333e313 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,15 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(a []int, b []int, target int) bool { + complements := make(map[int]bool) + for _, num := range a { + complements[target-num] = true + } + for _, num := range b { + if complements[num] { + return true + } + } + + return false } diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..6c9884d7 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,7 @@ package problem7 -func swap() { +func swap(x *int, y *int) { + temp := *x + *x = *y + *y = temp } diff --git a/exercise2/problem8/problem8.go b/exercise2/problem8/problem8.go index 9389d3b0..3d2e169f 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -1,11 +1,8 @@ package problem8 func simplify(list []string) map[string]int { - var indMap map[string]int - - indMap = make(map[string]int) + indMap := make(map[string]int) load(&indMap, &list) - return indMap } diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..9be1b79a 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,11 @@ package problem9 -func factory() {} +func factory(factor int) func(...int) []int { + return func(nums ...int) []int { + results := make([]int, len(nums)) + for i, num := range nums { + results[i] = num * factor + } + return results + } +} From eeb1078f5443422ae614b90c237c75f22534e125 Mon Sep 17 00:00:00 2001 From: Alinurmyrzakhanov Date: Mon, 7 Oct 2024 19:16:53 +0500 Subject: [PATCH 2/6] AlinurMyrzakhanov --- exercise3/problem1/problem1.go | 36 +++++++++++- exercise3/problem2/problem2.go | 36 +++++++++++- exercise3/problem3/problem3.go | 90 ++++++++++++++++++++++++++++- exercise3/problem4/problem4.go | 100 ++++++++++++++++++++++++++++++++- exercise3/problem5/problem5.go | 17 +++++- exercise3/problem6/problem6.go | 30 +++++++++- exercise3/problem7/problem7.go | 55 ++++++++++++++++++ 7 files changed, 356 insertions(+), 8 deletions(-) diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..a0bc591d 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,37 @@ package problem1 -type Queue struct{} +import ( + "errors" +) + +type Queue struct { + items []any +} + +func (q *Queue) Enqueue(val any) { + q.items = append(q.items, val) +} + +func (q *Queue) Dequeue() (any, error) { + if len(q.items) == 0 { + return nil, errors.New("queue is empty") + } + element := q.items[0] + q.items = q.items[1:] + return element, nil +} + +func (q *Queue) Peek() (any, error) { + if len(q.items) == 0 { + return nil, errors.New("queue is empty") + } + return q.items[0], nil +} + +func (q *Queue) Size() int { + return len(q.items) +} + +func (q *Queue) IsEmpty() bool { + return len(q.items) == 0 +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..f5aa7418 100644 --- a/exercise3/problem2/problem2.go +++ b/exercise3/problem2/problem2.go @@ -1,3 +1,37 @@ package problem2 -type Stack struct{} +import ( + "errors" +) + +type Stack struct { + items []any +} + +func (s *Stack) Push(val any) { + s.items = append(s.items, val) +} + +func (s *Stack) Pop() (any, error) { + if len(s.items) == 0 { + return nil, errors.New("stack is empty") + } + element := s.items[len(s.items)-1] + s.items = s.items[:len(s.items)-1] + return element, nil +} + +func (s *Stack) Peek() (any, error) { + if len(s.items) == 0 { + return nil, errors.New("stack is empty") + } + return s.items[len(s.items)-1], nil +} + +func (s *Stack) Size() int { + return len(s.items) +} + +func (s *Stack) IsEmpty() bool { + return len(s.items) == 0 +} diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..cf771df1 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,91 @@ package problem3 -type Set struct{} +type Set struct { + items map[any]struct{} +} + +func NewSet() *Set { + return &Set{items: make(map[any]struct{})} +} + +func (s *Set) Add(val any) { + s.items[val] = struct{}{} +} + +func (s *Set) Remove(val any) { + delete(s.items, val) +} + +func (s *Set) IsEmpty() bool { + return len(s.items) == 0 +} + +func (s *Set) Size() int { + return len(s.items) +} + +func (s *Set) List() []any { + list := make([]any, 0, len(s.items)) + for k := range s.items { + list = append(list, k) + } + return list +} + +func (s *Set) Has(val any) bool { + _, exists := s.items[val] + return exists +} + +func (s *Set) Copy() *Set { + newSet := NewSet() + for k := range s.items { + newSet.Add(k) + } + return newSet +} + +func (s *Set) Difference(other *Set) *Set { + diff := NewSet() + for k := range s.items { + if !other.Has(k) { + diff.Add(k) + } + } + return diff +} + +func (s *Set) IsSubset(other *Set) bool { + for k := range s.items { + if !other.Has(k) { + return false + } + } + return true +} + +func Union(sets ...*Set) *Set { + unionSet := NewSet() + for _, set := range sets { + for k := range set.items { + unionSet.Add(k) + } + } + return unionSet +} + +func Intersect(sets ...*Set) *Set { + if len(sets) == 0 { + return NewSet() + } + + intersectSet := sets[0].Copy() + for _, set := range sets[1:] { + for k := range intersectSet.items { + if !set.Has(k) { + intersectSet.Remove(k) + } + } + } + return intersectSet +} diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..db1b1747 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,101 @@ package problem4 -type LinkedList struct{} +import ( + "errors" +) + +type Element[T comparable] struct { + value T + next *Element[T] +} + +type LinkedList[T comparable] struct { + head *Element[T] + size int +} + +func (ll *LinkedList[T]) Add(el *Element[T]) { + if ll.head == nil { + ll.head = el + } else { + current := ll.head + for current.next != nil { + current = current.next + } + current.next = el + } + ll.size++ +} + +func (ll *LinkedList[T]) Insert(el *Element[T], pos int) error { + if pos < 0 || pos > ll.size { + return errors.New("index out of range") + } + if pos == 0 { + el.next = ll.head + ll.head = el + } else { + current := ll.head + for i := 0; i < pos-1; i++ { + current = current.next + } + el.next = current.next + current.next = el + } + ll.size++ + return nil +} + +func (ll *LinkedList[T]) Delete(el *Element[T]) error { + if ll.head == nil { + return errors.New("list is empty") + } + + if ll.head.value == el.value { + ll.head = ll.head.next + ll.size-- + return nil + } + + current := ll.head + for current.next != nil && current.next.value != el.value { + current = current.next + } + + if current.next == nil { + return errors.New("element not found") + } + + current.next = current.next.next + ll.size-- + return nil +} + +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, errors.New("element not found") +} + +func (ll *LinkedList[T]) List() []T { + var result []T + 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/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..bd1777c2 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,18 @@ package problem5 -type Person struct{} +import "fmt" + +type Person struct { + name string + age int +} + +func (p *Person) compareAge(other *Person) string { + if p.age < other.age { + return fmt.Sprintf("%s is older than me.", other.name) + } else if p.age > other.age { + return fmt.Sprintf("%s is younger than me.", other.name) + } else { + return fmt.Sprintf("%s is the same age as me.", other.name) + } +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..aba44cb4 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,31 @@ package problem6 -type Animal struct{} +type LegsProvider interface { + GetLegsNum() int +} -type Insect struct{} +type Animal struct { + name string + legsNum int +} -func sumOfAllLegsNum() {} +func (a *Animal) GetLegsNum() int { + return a.legsNum +} + +type Insect struct { + name string + legsNum int +} + +func (i *Insect) GetLegsNum() int { + return i.legsNum +} + +func sumOfAllLegsNum(entities ...LegsProvider) int { + totalLegs := 0 + for _, entity := range entities { + totalLegs += entity.GetLegsNum() + } + return totalLegs +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..0b6b988e 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,65 @@ package problem7 +import "fmt" + +type MoneyWithdrawable interface { + Withdraw(amount int) error +} + +type PackageSendable interface { + SendPackage(recipient string) +} + type BankAccount struct { + name string + balance int +} + +func (ba *BankAccount) Withdraw(amount int) error { + if ba.balance < amount { + return fmt.Errorf("insufficient funds") + } + ba.balance -= amount + return nil } type FedexAccount struct { + name string + packages []string +} + +func (fa *FedexAccount) SendPackage(recipient string) { + message := fmt.Sprintf("%s send package to %s", fa.name, recipient) + fa.packages = append(fa.packages, message) } type KazPostAccount struct { + name string + balance int + packages []string +} + +func (ka *KazPostAccount) Withdraw(amount int) error { + if ka.balance < amount { + return fmt.Errorf("insufficient funds") + } + ka.balance -= amount + return nil +} + +func (ka *KazPostAccount) SendPackage(recipient string) { + message := fmt.Sprintf("%s send package to %s", ka.name, recipient) + ka.packages = append(ka.packages, message) +} + +func withdrawMoney(amount int, accounts ...MoneyWithdrawable) { + for _, account := range accounts { + _ = account.Withdraw(amount) + } +} + +func sendPackagesTo(recipient string, accounts ...PackageSendable) { + for _, account := range accounts { + account.SendPackage(recipient) + } } From 9377a9279fefea75854f0f874c63142bb94f7133 Mon Sep 17 00:00:00 2001 From: alinurmyrzakhanov Date: Mon, 21 Oct 2024 16:03:43 +0500 Subject: [PATCH 3/6] done --- exercise4/bot/handler.go | 44 ++++++++++++++++++++++++++++++++++++++++ exercise4/bot/join.go | 44 ++++++++++++++++++++++++++++++++++++++++ exercise4/bot/main.go | 11 ++++++---- exercise4/bot/server.go | 3 +++ 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 exercise4/bot/handler.go create mode 100644 exercise4/bot/join.go diff --git a/exercise4/bot/handler.go b/exercise4/bot/handler.go new file mode 100644 index 00000000..113ea244 --- /dev/null +++ b/exercise4/bot/handler.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type MoveRequest struct { + GameID string `json:"game_id"` + Board []int `json:"board"` +} + +type MoveResponse struct { + Position int `json:"position"` +} + +func handlePing(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "pong") +} + +func handleMove(w http.ResponseWriter, r *http.Request) { + var req MoveRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + + move := calculateMove(req.Board) + + resp := MoveResponse{Position: move} + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +func calculateMove(board []int) int { + for i, val := range board { + if val == 0 { + return i + } + } + return -1 +} diff --git a/exercise4/bot/join.go b/exercise4/bot/join.go new file mode 100644 index 00000000..776750f8 --- /dev/null +++ b/exercise4/bot/join.go @@ -0,0 +1,44 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" +) + +type JoinRequest struct { + Name string `json:"name"` + URL string `json:"url"` +} + +func joinGame() error { + joinURL := "http://localhost:4444/join" + botName := "MyTicTacToeBot" + botURL := "http://localhost:" + os.Getenv("PORT") + + reqBody := JoinRequest{ + Name: botName, + URL: botURL, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("failed to marshal join request: %w", err) + } + + resp, err := http.Post(joinURL, "application/json", bytes.NewBuffer(jsonBody)) + if err != nil { + return fmt.Errorf("failed to send join request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("failed to join the game, status: %d", resp.StatusCode) + } + + log.Println("Bot successfully joined the game!") + return nil +} diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..067d7232 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -1,21 +1,24 @@ package main import ( - "context" + "log" "os" "os/signal" "syscall" ) func main() { - ctx := context.Background() ready := startServer() <-ready - // TODO after server start + // Присоединяемся к игре + err := joinGame() + if err != nil { + log.Fatalf("Failed to join the game: %v", err) + } stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - <-stop // Wait for SIGINT or SIGTERM + <-stop // Ожидание завершения по SIGINT или SIGTERM } diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go index e6760ec5..6b29fa0b 100644 --- a/exercise4/bot/server.go +++ b/exercise4/bot/server.go @@ -34,6 +34,9 @@ func startServer() <-chan struct{} { IdleTimeout: 2 * time.Minute, } + http.HandleFunc("/ping", handlePing) // Пинг + http.HandleFunc("/move", handleMove) // Ходы + go func() { err := srv.Serve(list) if !errors.Is(err, http.ErrServerClosed) { From fa1eafab643184416f925e623f8d5c9fb1b478f2 Mon Sep 17 00:00:00 2001 From: Alinurmyrzakhanov Date: Mon, 4 Nov 2024 21:06:47 +0500 Subject: [PATCH 4/6] done --- exercise5.zip | Bin 0 -> 10845 bytes exercise5/problem1/problem1.go | 7 ++++++- exercise5/problem2/problem2.go | 31 +++++++++++++++++++++++++++++++ exercise5/problem3/problem3.go | 8 +++----- exercise5/problem4/problem4.go | 2 +- exercise5/problem5/problem5.go | 26 +++++++++++++++++--------- exercise5/problem6/problem6.go | 30 +++++++++++++++++++++++++++--- exercise5/problem7/problem7.go | 21 ++++++++++++++++++++- exercise5/problem8/problem8.go | 12 +++++++++++- 9 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 exercise5.zip diff --git a/exercise5.zip b/exercise5.zip new file mode 100644 index 0000000000000000000000000000000000000000..6a40a69f604c520ca1cc9023e256da332794289a GIT binary patch literal 10845 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 len(numbers) { + end = len(numbers) + } + + go func(start, end int) { + defer wg.Done() + var localSum int64 + for _, n := range numbers[start:end] { + localSum += int64(n) + } + mu.Lock() + sum += localSum + mu.Unlock() + }(start, end) + } + wg.Wait() return sum } diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..97111869 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -1,11 +1,9 @@ package problem3 func sum(a, b int) int { - var c int - + resultChan := make(chan int) go func(a, b int) { - c = a + b + resultChan <- a + b }(a, b) - - return c + return <-resultChan } diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index b5899ddf..7e30f42e 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -4,11 +4,11 @@ func iter(ch chan<- int, nums []int) { for _, n := range nums { ch <- n } + close(ch) // Close the channel after sending all numbers } func sum(nums []int) int { ch := make(chan int) - go iter(ch, nums) var sum int diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..94666e85 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,17 +1,25 @@ package problem5 -func producer() {} +func producer(words []string, ch chan<- string) { + for _, word := range words { + ch <- word + } + close(ch) +} -func consumer() {} +func consumer(ch <-chan string) string { + var message string + for word := range ch { + if message != "" { + message += " " + } + message += word + } + return message +} -func send( - words []string, - pr func([]string, chan<- string), - cons func(<-chan string) string, -) string { +func send(words []string, pr func([]string, chan<- string), cons func(<-chan string) string) string { ch := make(chan string) - go pr(words, ch) - return cons(ch) } diff --git a/exercise5/problem6/problem6.go b/exercise5/problem6/problem6.go index e1beea87..36bdee8d 100644 --- a/exercise5/problem6/problem6.go +++ b/exercise5/problem6/problem6.go @@ -2,8 +2,32 @@ package problem6 type pipe func(in <-chan int) <-chan int -var multiplyBy2 pipe = func() {} +var multiplyBy2 pipe = func(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for num := range in { + out <- num * 2 + } + }() + return out +} -var add5 pipe = func() {} +var add5 pipe = func(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for num := range in { + out <- num + 5 + } + }() + return out +} -func piper(in <-chan int, pipes []pipe) <-chan int {} +func piper(in <-chan int, pipes []pipe) <-chan int { + out := in + for _, p := range pipes { + out = p(out) + } + return out +} diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..b3f12a2a 100644 --- a/exercise5/problem7/problem7.go +++ b/exercise5/problem7/problem7.go @@ -1,3 +1,22 @@ package problem7 -func multiplex(ch1 <-chan string, ch2 <-chan string) []string {} +func multiplex(ch1 <-chan string, ch2 <-chan string) []string { + var result []string + for ch1 != nil || ch2 != nil { + select { + case msg, ok := <-ch1: + if ok { + result = append(result, msg) + } else { + ch1 = nil // Mark channel as closed + } + case msg, ok := <-ch2: + if ok { + result = append(result, msg) + } else { + ch2 = nil // Mark channel as closed + } + } + } + return result +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..999a0d0b 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -4,4 +4,14 @@ import ( "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + timer := time.NewTimer(ttl) + defer timer.Stop() + + select { + case msg := <-ch: + return msg + case <-timer.C: + return "timeout" + } +} From bab312909addedfd342d835865512985d45ad701 Mon Sep 17 00:00:00 2001 From: alinurmyrzakhanov <44428877+alinurmyrzakhanov@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:21:40 +0500 Subject: [PATCH 5/6] bloggingplatfrom by Alinurmyrzakhanov --- exercise7/blogging-platform/Dockerfile | 15 ++ exercise7/blogging-platform/MakeFile | 17 +++ exercise7/blogging-platform/README.md | 63 +++++++- exercise7/blogging-platform/cmd/main.go | 18 +++ .../controllers/post_controller.go | 135 ++++++++++++++++++ .../blogging-platform/database/database.go | 41 ++++++ exercise7/blogging-platform/go.mod | 37 ++++- exercise7/blogging-platform/go.sum | 90 +++++++++++- exercise7/blogging-platform/main.go | 49 ------- exercise7/blogging-platform/models/post.go | 16 +++ .../pkg/httputils/request/body.go | 76 ---------- .../pkg/httputils/response/body.go | 33 ----- .../pkg/httputils/statusError/main.go | 18 --- .../repositories/post_repository.go | 88 ++++++++++++ exercise7/blogging-platform/routes/routes.go | 19 +++ 15 files changed, 533 insertions(+), 182 deletions(-) create mode 100644 exercise7/blogging-platform/Dockerfile create mode 100644 exercise7/blogging-platform/MakeFile create mode 100644 exercise7/blogging-platform/cmd/main.go create mode 100644 exercise7/blogging-platform/controllers/post_controller.go create mode 100644 exercise7/blogging-platform/database/database.go delete mode 100644 exercise7/blogging-platform/main.go create mode 100644 exercise7/blogging-platform/models/post.go delete mode 100644 exercise7/blogging-platform/pkg/httputils/request/body.go delete mode 100644 exercise7/blogging-platform/pkg/httputils/response/body.go delete mode 100644 exercise7/blogging-platform/pkg/httputils/statusError/main.go create mode 100644 exercise7/blogging-platform/repositories/post_repository.go create mode 100644 exercise7/blogging-platform/routes/routes.go diff --git a/exercise7/blogging-platform/Dockerfile b/exercise7/blogging-platform/Dockerfile new file mode 100644 index 00000000..413777b2 --- /dev/null +++ b/exercise7/blogging-platform/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.23-alpine + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY . . + +RUN go build -o main ./cmd/main.go + +EXPOSE 8080 + +CMD ["/app/main"] diff --git a/exercise7/blogging-platform/MakeFile b/exercise7/blogging-platform/MakeFile new file mode 100644 index 00000000..a6902b7a --- /dev/null +++ b/exercise7/blogging-platform/MakeFile @@ -0,0 +1,17 @@ +IMAGE_NAME = myblog:latest + +.PHONY: build +build: + go build -o bin/myblog ./cmd/main.go + +.PHONY: run +run: + go run ./cmd/main.go + +.PHONY: docker-build +docker-build: + docker build -t $(IMAGE_NAME) -f docker/Dockerfile . + +.PHONY: docker-run +docker-run: + docker run --name myblog_container -p 8080:8080 --rm $(IMAGE_NAME) diff --git a/exercise7/blogging-platform/README.md b/exercise7/blogging-platform/README.md index e6ef7017..24229636 100644 --- a/exercise7/blogging-platform/README.md +++ b/exercise7/blogging-platform/README.md @@ -1,3 +1,62 @@ -# Blogging Platform +# MyBlog -Please check https://roadmap.sh/projects/blogging-platform-api. +Простое RESTful API для личной блог-платформы. +Реализован полный CRUD для постов, а также поиск по термину. + +## Структура проекта + +- **cmd/main.go**: Точка входа в приложение +- **controllers/**: HTTP-хендлеры (CRUD-операции) +- **database/**: Инициализация и миграция БД (в примере — SQLite) +- **models/**: Модели данных (структуры GORM) +- **repositories/**: Логика работы с БД (CRUD) +- **routes/**: Определение маршрутов и привязка к контроллерам +- **docker/Dockerfile**: Для сборки Docker-образа +- **Makefile**: Упрощённая сборка/запуск +- **go.mod** / **go.sum**: Go-модули + +## Запуск локально + +Убедитесь, что у вас установлен Go (>= 1.18). Затем: + +```bash +go mod tidy + +go run ./cmd/main.go +``` +или через Make +```bash +make run +make docker-build +make docker-run +``` + +## Примеры запросов + +``` +curl -X POST -H "Content-Type: application/json" \ + -d '{ + "title": "My First Blog Post", + "content": "Hello World!", + "category": "General", + "tags": ["Welcome","First"] + }' \ + http://localhost:8080/posts + +curl http://localhost:8080/posts + +curl "http://localhost:8080/posts?term=welcome" + +curl http://localhost:8080/posts/1 + +curl -X PUT -H "Content-Type: application/json" \ + -d '{ + "title": "My Updated Post", + "content": "Updated content!", + "category": "Updates", + "tags": ["Update","News"] + }' \ + http://localhost:8080/posts/1 + +curl -X DELETE http://localhost:8080/posts/1 +``` \ No newline at end of file diff --git a/exercise7/blogging-platform/cmd/main.go b/exercise7/blogging-platform/cmd/main.go new file mode 100644 index 00000000..23532720 --- /dev/null +++ b/exercise7/blogging-platform/cmd/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "alinurmyrzakhanov/database" + "alinurmyrzakhanov/routes" + + "github.com/gin-gonic/gin" +) + +func main() { + database.InitDB() + + r := gin.Default() + + routes.SetupRoutes(r) + + r.Run(":8080") +} diff --git a/exercise7/blogging-platform/controllers/post_controller.go b/exercise7/blogging-platform/controllers/post_controller.go new file mode 100644 index 00000000..14a37140 --- /dev/null +++ b/exercise7/blogging-platform/controllers/post_controller.go @@ -0,0 +1,135 @@ +// controllers/post_controller.go +package controllers + +import ( + "net/http" + "strconv" + "time" + + "alinurmyrzakhanov/models" + "alinurmyrzakhanov/repositories" + + "github.com/gin-gonic/gin" +) + +func CreatePost(c *gin.Context) { + var input struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + Category string `json:"category" binding:"required"` + Tags []string `json:"tags"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + post := models.Post{ + Title: input.Title, + Content: input.Content, + Category: input.Category, + Tags: input.Tags, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := repositories.CreatePost(&post); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось создать пост"}) + return + } + c.JSON(http.StatusCreated, post) +} + +func GetPost(c *gin.Context) { + idParam := c.Param("id") + id, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + + post, err := repositories.GetPostByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Пост не найден"}) + return + } + + c.JSON(http.StatusOK, post) +} + +func GetAllPosts(c *gin.Context) { + term := c.Query("term") + if term != "" { + posts, err := repositories.SearchPostsByTerm(term) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при поиске постов"}) + return + } + c.JSON(http.StatusOK, posts) + } else { + posts, err := repositories.GetAllPosts() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при получении постов"}) + return + } + c.JSON(http.StatusOK, posts) + } +} + +func UpdatePost(c *gin.Context) { + idParam := c.Param("id") + id, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + existingPost, err := repositories.GetPostByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Пост не найден"}) + return + } + + var input struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + Category string `json:"category" binding:"required"` + Tags []string `json:"tags"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + existingPost.Title = input.Title + existingPost.Content = input.Content + existingPost.Category = input.Category + existingPost.Tags = input.Tags + existingPost.UpdatedAt = time.Now() + + if err := repositories.UpdatePost(existingPost); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось обновить пост"}) + return + } + + c.JSON(http.StatusOK, existingPost) +} + +func DeletePost(c *gin.Context) { + idParam := c.Param("id") + id, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + + _, getErr := repositories.GetPostByID(uint(id)) + if getErr != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Пост не найден"}) + return + } + + if err := repositories.DeletePost(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось удалить пост"}) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/exercise7/blogging-platform/database/database.go b/exercise7/blogging-platform/database/database.go new file mode 100644 index 00000000..a20a2ea3 --- /dev/null +++ b/exercise7/blogging-platform/database/database.go @@ -0,0 +1,41 @@ +// database/database.go +package database + +import ( + "fmt" + "log" + "os" + "time" + + "alinurmyrzakhanov/models" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func InitDB() { + dbPath := "myblog.db" + if path := os.Getenv("DB_PATH"); path != "" { + dbPath = path + } + + dsn := fmt.Sprintf("%s", dbPath) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("Не удалось подключиться к БД:", err) + } + + err = db.AutoMigrate(&models.Post{}) + if err != nil { + log.Fatal("Ошибка миграции:", err) + } + + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + DB = db +} diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..271b7488 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -1,5 +1,38 @@ -module github.com/talgat-ruby/exercises-go/exercise7/blogging-platform +module alinurmyrzakhanov go 1.23.3 -require github.com/lib/pq v1.10.9 +require ( + github.com/bytedance/sonic v1.12.8 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.14.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlite v1.5.7 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..1ed1b599 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,2 +1,88 @@ -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= +github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go deleted file mode 100644 index 1ffa1477..00000000 --- a/exercise7/blogging-platform/main.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "context" - "log/slog" - "os" - "os/signal" - - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api" - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - - // db - _, err := db.New() - if err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "db", - "error", err, - ) - panic(err) - } - - // api - a := api.New() - if err := a.Start(ctx); err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "api", - "error", err, - ) - panic(err) - } - - go func() { - shutdown := make(chan os.Signal, 1) // Create channel to signify s signal being sent - signal.Notify(shutdown, os.Interrupt) // When an interrupt is sent, notify the channel - - sig := <-shutdown - slog.WarnContext(ctx, "signal received - shutting down...", "signal", sig) - - cancel() - }() -} diff --git a/exercise7/blogging-platform/models/post.go b/exercise7/blogging-platform/models/post.go new file mode 100644 index 00000000..9ad33042 --- /dev/null +++ b/exercise7/blogging-platform/models/post.go @@ -0,0 +1,16 @@ +package models + +import ( + "time" +) + +type Post struct { + ID uint `gorm:"primaryKey" json:"id"` + Title string `gorm:"not null" json:"title"` + Content string `gorm:"not null" json:"content"` + Category string `gorm:"not null" json:"category"` + Tags []string `gorm:"-" json:"tags"` + TagsRaw string `json:"-"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/exercise7/blogging-platform/pkg/httputils/request/body.go b/exercise7/blogging-platform/pkg/httputils/request/body.go deleted file mode 100644 index 92d639f4..00000000 --- a/exercise7/blogging-platform/pkg/httputils/request/body.go +++ /dev/null @@ -1,76 +0,0 @@ -package request - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/statusError" -) - -func JSON(w http.ResponseWriter, r *http.Request, dst interface{}) error { - 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/exercise7/blogging-platform/pkg/httputils/response/body.go b/exercise7/blogging-platform/pkg/httputils/response/body.go deleted file mode 100644 index e1fd78a8..00000000 --- a/exercise7/blogging-platform/pkg/httputils/response/body.go +++ /dev/null @@ -1,33 +0,0 @@ -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/exercise7/blogging-platform/pkg/httputils/statusError/main.go b/exercise7/blogging-platform/pkg/httputils/statusError/main.go deleted file mode 100644 index 6cf4e1b6..00000000 --- a/exercise7/blogging-platform/pkg/httputils/statusError/main.go +++ /dev/null @@ -1,18 +0,0 @@ -package statusError - -type StatusError struct { - status int - msg string -} - -func New(status int, msg string) error { - return &StatusError{status, msg} -} - -func (st *StatusError) Error() string { - return st.msg -} - -func (st *StatusError) Status() int { - return st.status -} diff --git a/exercise7/blogging-platform/repositories/post_repository.go b/exercise7/blogging-platform/repositories/post_repository.go new file mode 100644 index 00000000..0d18801b --- /dev/null +++ b/exercise7/blogging-platform/repositories/post_repository.go @@ -0,0 +1,88 @@ +// repositories/post_repository.go +package repositories + +import ( + "errors" + "strings" + + "alinurmyrzakhanov/database" + "alinurmyrzakhanov/models" +) + +func CreatePost(post *models.Post) error { + post.TagsRaw = strings.Join(post.Tags, ",") // массив тегов => строка + if err := database.DB.Create(post).Error; err != nil { + return err + } + return nil +} + +func GetPostByID(id uint) (*models.Post, error) { + var post models.Post + if err := database.DB.First(&post, id).Error; err != nil { + return nil, err + } + if post.TagsRaw != "" { + post.Tags = strings.Split(post.TagsRaw, ",") + } else { + post.Tags = []string{} + } + + return &post, nil +} + +func UpdatePost(post *models.Post) error { + post.TagsRaw = strings.Join(post.Tags, ",") + if err := database.DB.Save(post).Error; err != nil { + return err + } + return nil +} + +func DeletePost(id uint) error { + result := database.DB.Delete(&models.Post{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return errors.New("post not found") + } + return nil +} +func GetAllPosts() ([]models.Post, error) { + var posts []models.Post + if err := database.DB.Find(&posts).Error; err != nil { + return nil, err + } + + for i := range posts { + if posts[i].TagsRaw != "" { + posts[i].Tags = strings.Split(posts[i].TagsRaw, ",") + } else { + posts[i].Tags = []string{} + } + } + return posts, nil +} + +func SearchPostsByTerm(term string) ([]models.Post, error) { + var posts []models.Post + likeTerm := "%" + term + "%" + + if err := database.DB.Where( + "title LIKE ? OR content LIKE ? OR category LIKE ?", + likeTerm, likeTerm, likeTerm). + Find(&posts).Error; err != nil { + return nil, err + } + + for i := range posts { + if posts[i].TagsRaw != "" { + posts[i].Tags = strings.Split(posts[i].TagsRaw, ",") + } else { + posts[i].Tags = []string{} + } + } + + return posts, nil +} diff --git a/exercise7/blogging-platform/routes/routes.go b/exercise7/blogging-platform/routes/routes.go new file mode 100644 index 00000000..8b694dc8 --- /dev/null +++ b/exercise7/blogging-platform/routes/routes.go @@ -0,0 +1,19 @@ +// routes/routes.go +package routes + +import ( + "alinurmyrzakhanov/controllers" + + "github.com/gin-gonic/gin" +) + +func SetupRoutes(r *gin.Engine) { + postRoutes := r.Group("/posts") + { + postRoutes.POST("", controllers.CreatePost) + postRoutes.GET("", controllers.GetAllPosts) + postRoutes.GET("/:id", controllers.GetPost) + postRoutes.PUT("/:id", controllers.UpdatePost) + postRoutes.DELETE("/:id", controllers.DeletePost) + } +} From d839b2ce783965ba261d1584871e0dddb7cb6f9b Mon Sep 17 00:00:00 2001 From: alinurmyrzakhanov <44428877+alinurmyrzakhanov@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:42:31 +0500 Subject: [PATCH 6/6] Alinur Myrzakhanov Simple Workout Tracker --- exercise9/Dockerfile | 14 ++ exercise9/Makefile | 18 ++ exercise9/README.md | 100 +++++++- exercise9/cmd/main.go | 20 ++ exercise9/controllers/auth_controller.go | 159 +++++++++++++ exercise9/controllers/exercise_controller.go | 118 ++++++++++ exercise9/controllers/workout_controller.go | 202 ++++++++++++++++ exercise9/database/database.go | 45 ++++ exercise9/database/seed.go | 30 +++ exercise9/go.mod | 43 +++- exercise9/go.sum | 93 ++++++++ exercise9/models/exercise.go | 14 ++ exercise9/models/user.go | 13 ++ exercise9/models/workout.go | 32 +++ exercise9/repositories/exercise_repository.go | 36 +++ exercise9/repositories/user_repository.go | 34 +++ exercise9/repositories/workout_repository.go | 95 ++++++++ exercise9/routes/routes.go | 32 +++ exercise9/tests/auth_test.go | 219 ++++++++++++++++++ 19 files changed, 1305 insertions(+), 12 deletions(-) create mode 100644 exercise9/Dockerfile create mode 100644 exercise9/Makefile create mode 100644 exercise9/cmd/main.go create mode 100644 exercise9/controllers/auth_controller.go create mode 100644 exercise9/controllers/exercise_controller.go create mode 100644 exercise9/controllers/workout_controller.go create mode 100644 exercise9/database/database.go create mode 100644 exercise9/database/seed.go create mode 100644 exercise9/go.sum create mode 100644 exercise9/models/exercise.go create mode 100644 exercise9/models/user.go create mode 100644 exercise9/models/workout.go create mode 100644 exercise9/repositories/exercise_repository.go create mode 100644 exercise9/repositories/user_repository.go create mode 100644 exercise9/repositories/workout_repository.go create mode 100644 exercise9/routes/routes.go create mode 100644 exercise9/tests/auth_test.go diff --git a/exercise9/Dockerfile b/exercise9/Dockerfile new file mode 100644 index 00000000..0a6f5b26 --- /dev/null +++ b/exercise9/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.23-alpine + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o main ./cmd/main.go + +EXPOSE 8080 + +CMD ["/app/main"] diff --git a/exercise9/Makefile b/exercise9/Makefile new file mode 100644 index 00000000..bd7334e3 --- /dev/null +++ b/exercise9/Makefile @@ -0,0 +1,18 @@ +IMAGE_NAME = myworkout:latest +CONTAINER_NAME = myworkout_container + +.PHONY: build +build: + go build -o bin/myworkout ./cmd/main.go + +.PHONY: run +run: + go run ./cmd/main.go + +.PHONY: docker-build +docker-build: + docker build -t $(IMAGE_NAME) -f docker/Dockerfile . + +.PHONY: docker-run +docker-run: + docker run --name $(CONTAINER_NAME) -p 8080:8080 --rm $(IMAGE_NAME) diff --git a/exercise9/README.md b/exercise9/README.md index ef1d468e..3a4ca0c9 100644 --- a/exercise9/README.md +++ b/exercise9/README.md @@ -1,14 +1,94 @@ -# Exercise 9 +# MyWorkout — Трекер тренировок -Project +Это бэкенд-приложение на Go, позволяющее: +- Регистрацию пользователей (Sign Up) +- Аутентификацию через JWT (Login, Logout) +- CRUD по тренировкам (Workouts), включая + - Планирование (Scheduled date/time) + - Упражнения внутри тренировки (sets, reps, weight) +- Генерацию отчётов о прошлых тренировках (по диапазону дат) +- Предзаполнение списка упражнений (Seeder) +- Защиту эндпоинтов авторизационным middleware -## Teams +## Структура -Team 1 +- **cmd/main.go**: Точка входа (запуск сервера) +- **controllers/**: Обработчики (Handlers) для Auth, Workouts, Exercises +- **database/**: Подключение к SQLite и миграции, а также сидер +- **models/**: Определение структур (User, Exercise, Workout) +- **repositories/**: Прямая работа с БД (CRUD) +- **routes/**: Маршруты (Endpoints) +- **tests/**: Юнит-тесты (пример) +- **docker/Dockerfile**: Сборка Docker-образа +- **Makefile**: Упрощённые команды для сборки и запуска -1. Тұрарова Айзада (api) -2. Манкенов Арай (api) -3. Усербай Асылбек (controller) -4. Кемалатдин Ғалымжан (controller) -5. Имангали Аскар (db) -6. Кабдылкак Арнур (db) +## Запуск локально + + +```bash +go mod tidy + +go run ./cmd/main.go +``` +или через Make +```bash +make run +make docker-build +make docker-run +``` + +## Примеры запросов + +``` +curl -X POST -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"secret"}' \ + http://localhost:8080/signup + +curl -X POST -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"secret"}' \ + http://localhost:8080/login + +curl -X POST -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Leg Day", + "scheduled": "2025-02-10T10:00:00Z", + "comment": "Focus on squats", + "exercises": [ + {"exerciseId": 2, "sets": 4, "reps": 12, "weight": 60}, + {"exerciseId": 1, "sets": 3, "reps": 10, "weight": 20} + ] + }' \ + http://localhost:8080/workouts + +curl -H "Authorization: Bearer " \ + http://localhost:8080/workouts?pending=true + +curl -H "Authorization: Bearer " \ + "http://localhost:8080/workouts/report?from=2025-02-01&to=2025-02-28" + + +curl -X POST -H "Content-Type: application/json" \ + -d '{ + "name": "Bench Press", + "description": "Chest exercise with barbell", + "category": "strength" + }' \ + http://localhost:8080/exercises + +curl http://localhost:8080/exercises + +curl http://localhost:8080/exercises/1 + + +curl -X PUT -H "Content-Type: application/json" \ + -d '{ + "name": "Barbell Bench Press", + "description": "Bench press with barbell", + "category": "strength" + }' \ + http://localhost:8080/exercises/1 + +curl -X DELETE http://localhost:8080/exercises/1 + +``` diff --git a/exercise9/cmd/main.go b/exercise9/cmd/main.go new file mode 100644 index 00000000..ca2ebb2a --- /dev/null +++ b/exercise9/cmd/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "alinurmyrzakhanov/database" + "alinurmyrzakhanov/routes" + + "github.com/gin-gonic/gin" +) + +func main() { + database.InitDB() + + database.SeedExercises() + + r := gin.Default() + + routes.SetupRoutes(r) + + r.Run(":8080") +} diff --git a/exercise9/controllers/auth_controller.go b/exercise9/controllers/auth_controller.go new file mode 100644 index 00000000..9676f997 --- /dev/null +++ b/exercise9/controllers/auth_controller.go @@ -0,0 +1,159 @@ +package controllers + +import ( + "fmt" + "net/http" + "time" + + "alinurmyrzakhanov/models" + "alinurmyrzakhanov/repositories" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "golang.org/x/crypto/bcrypt" +) + +var jwtSecret = []byte("MY_SUPER_SECRET") + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func generateJWT(userID uint) (string, error) { + claims := jwt.MapClaims{ + "user_id": userID, + "exp": time.Now().Add(time.Hour * 24).Unix(), + "iat": time.Now().Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Токен не найден"}) + c.Abort() + return + } + + tokenStr := "" + _, err := fmt.Sscanf(authHeader, "Bearer %s", &tokenStr) + if err != nil || tokenStr == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Некорректный заголовок"}) + c.Abort() + return + } + + token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("неверный метод подписи") + } + return jwtSecret, nil + }) + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Недействительный токен"}) + c.Abort() + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Недействительный payload"}) + c.Abort() + return + } + + userIDFloat, ok := claims["user_id"].(float64) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "user_id не найден в токене"}) + c.Abort() + return + } + c.Set("user_id", uint(userIDFloat)) + + c.Next() + } +} + +func SignUp(c *gin.Context) { + var input struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + hashed, err := HashPassword(input.Password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось захешировать пароль"}) + return + } + user := models.User{ + Email: input.Email, + PasswordHash: hashed, + } + if err := repositories.CreateUser(&user); err != nil { + c.JSON(http.StatusConflict, gin.H{"error": "Пользователь уже существует или другая ошибка"}) + return + } + c.JSON(http.StatusCreated, gin.H{ + "message": "Пользователь успешно создан", + "user": user.Email, + }) +} + +func Login(c *gin.Context) { + var input struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := repositories.GetUserByEmail(input.Email) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Неверный логин или пароль"}) + return + } + + if !CheckPasswordHash(input.Password, user.PasswordHash) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Неверный логин или пароль"}) + return + } + + token, err := generateJWT(user.ID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при генерации токена"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "token": token, + }) +} + +func Logout(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "Вы успешно вышли из системы (токен считать недействительным на клиенте).", + }) +} + +func GetUserID(c *gin.Context) uint { + val, exists := c.Get("user_id") + if !exists { + return 0 + } + userID, _ := val.(uint) + return userID +} diff --git a/exercise9/controllers/exercise_controller.go b/exercise9/controllers/exercise_controller.go new file mode 100644 index 00000000..a75d7562 --- /dev/null +++ b/exercise9/controllers/exercise_controller.go @@ -0,0 +1,118 @@ +package controllers + +import ( + "net/http" + "strconv" + "time" + + "alinurmyrzakhanov/models" + "alinurmyrzakhanov/repositories" + + "github.com/gin-gonic/gin" +) + +func CreateExercise(c *gin.Context) { + var input struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Category string `json:"category" binding:"required"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ex := models.Exercise{ + Name: input.Name, + Description: input.Description, + Category: input.Category, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := repositories.CreateExercise(&ex); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось создать упражнение"}) + return + } + + c.JSON(http.StatusCreated, ex) +} + +func GetExercises(c *gin.Context) { + exercises, err := repositories.GetAllExercises() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при получении упражнений"}) + return + } + c.JSON(http.StatusOK, exercises) +} + +func GetExerciseByID(c *gin.Context) { + idParam := c.Param("id") + id, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + + ex, err := repositories.GetExerciseByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Упражнение не найдено"}) + return + } + + c.JSON(http.StatusOK, ex) +} + +func UpdateExercise(c *gin.Context) { + idParam := c.Param("id") + id, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + + existing, err := repositories.GetExerciseByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Упражнение не найдено"}) + return + } + + var input struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Category string `json:"category" binding:"required"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + existing.Name = input.Name + existing.Description = input.Description + existing.Category = input.Category + existing.UpdatedAt = time.Now() + + if err := repositories.UpdateExercise(existing); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при обновлении упражнения"}) + return + } + + c.JSON(http.StatusOK, existing) +} + +func DeleteExercise(c *gin.Context) { + idParam := c.Param("id") + id, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + + if err := repositories.DeleteExercise(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при удалении упражнения"}) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/exercise9/controllers/workout_controller.go b/exercise9/controllers/workout_controller.go new file mode 100644 index 00000000..dba9d9ad --- /dev/null +++ b/exercise9/controllers/workout_controller.go @@ -0,0 +1,202 @@ +package controllers + +import ( + "net/http" + "strconv" + "time" + + "alinurmyrzakhanov/models" + "alinurmyrzakhanov/repositories" + + "github.com/gin-gonic/gin" +) + +func CreateWorkout(c *gin.Context) { + userID := GetUserID(c) + if userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Нет user_id"}) + return + } + + var input struct { + Title string `json:"title" binding:"required"` + Scheduled string `json:"scheduled"` + Comment string `json:"comment"` + WorkoutExercises []struct { + ExerciseID uint `json:"exerciseId" binding:"required"` + Sets int `json:"sets" binding:"required"` + Reps int `json:"reps" binding:"required"` + Weight int `json:"weight"` + Comment string `json:"comment"` + } `json:"exercises"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var scheduled time.Time + if input.Scheduled != "" { + t, err := time.Parse(time.RFC3339, input.Scheduled) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный формат времени. Используйте RFC3339."}) + return + } + scheduled = t + } + + workout := models.Workout{ + UserID: userID, + Title: input.Title, + Scheduled: scheduled, + Comment: input.Comment, + IsDone: false, + } + for _, wex := range input.WorkoutExercises { + workout.WorkoutExercises = append(workout.WorkoutExercises, models.WorkoutExercise{ + ExerciseID: wex.ExerciseID, + Sets: wex.Sets, + Reps: wex.Reps, + Weight: wex.Weight, + Comment: wex.Comment, + }) + } + + if err := repositories.CreateWorkout(&workout); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при создании тренировки"}) + return + } + c.JSON(http.StatusCreated, workout) +} + +func UpdateWorkout(c *gin.Context) { + userID := GetUserID(c) + idParam := c.Param("id") + workoutID, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + + existing, err := repositories.GetWorkoutByID(uint(workoutID), userID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Тренировка не найдена"}) + return + } + + var input struct { + Title string `json:"title" binding:"required"` + Scheduled string `json:"scheduled"` + Comment string `json:"comment"` + IsDone bool `json:"isDone"` + WorkoutExercises []struct { + ExerciseID uint `json:"exerciseId" binding:"required"` + Sets int `json:"sets" binding:"required"` + Reps int `json:"reps" binding:"required"` + Weight int `json:"weight"` + Comment string `json:"comment"` + } `json:"exercises"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + existing.Title = input.Title + existing.Comment = input.Comment + existing.IsDone = input.IsDone + + if input.Scheduled != "" { + t, err := time.Parse(time.RFC3339, input.Scheduled) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный формат времени"}) + return + } + existing.Scheduled = t + } + var newWex []models.WorkoutExercise + for _, wex := range input.WorkoutExercises { + newWex = append(newWex, models.WorkoutExercise{ + ExerciseID: wex.ExerciseID, + Sets: wex.Sets, + Reps: wex.Reps, + Weight: wex.Weight, + Comment: wex.Comment, + }) + } + existing.WorkoutExercises = newWex + + if err := repositories.UpdateWorkout(existing); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при обновлении"}) + return + } + + c.JSON(http.StatusOK, existing) +} + +func DeleteWorkout(c *gin.Context) { + userID := GetUserID(c) + idParam := c.Param("id") + workoutID, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Некорректный ID"}) + return + } + + if err := repositories.DeleteWorkout(uint(workoutID), userID); err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Тренировка не найдена или ошибка при удалении"}) + return + } + c.Status(http.StatusNoContent) +} + +func ListWorkouts(c *gin.Context) { + userID := GetUserID(c) + onlyPending := c.Query("pending") == "true" + + workouts, err := repositories.ListWorkouts(userID, onlyPending) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при получении списка"}) + return + } + c.JSON(http.StatusOK, workouts) +} + +func GetWorkoutReport(c *gin.Context) { + userID := GetUserID(c) + fromStr := c.Query("from") + toStr := c.Query("to") + + if fromStr == "" || toStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Параметры ?from=YYYY-MM-DD&to=YYYY-MM-DD обязательны"}) + return + } + + from, errFrom := time.Parse("2006-01-02", fromStr) + to, errTo := time.Parse("2006-01-02", toStr) + if errFrom != nil || errTo != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Неверный формат дат (используйте YYYY-MM-DD)"}) + return + } + if from.After(to) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Дата 'from' не может быть позже 'to'"}) + return + } + + workouts, err := repositories.GetWorkoutsInRange(userID, from, to) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при формировании отчёта"}) + return + } + + totalWorkouts := len(workouts) + + c.JSON(http.StatusOK, gin.H{ + "from": fromStr, + "to": toStr, + "totalRecords": totalWorkouts, + "workouts": workouts, + }) +} diff --git a/exercise9/database/database.go b/exercise9/database/database.go new file mode 100644 index 00000000..807e0c44 --- /dev/null +++ b/exercise9/database/database.go @@ -0,0 +1,45 @@ +package database + +import ( + "fmt" + "log" + "os" + "time" + + "alinurmyrzakhanov/models" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func InitDB() { + dbPath := "myworkout.db" + if envPath := os.Getenv("DB_PATH"); envPath != "" { + dbPath = envPath + } + + dsn := fmt.Sprintf("%s", dbPath) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("Не удалось подключиться к БД:", err) + } + + err = db.AutoMigrate( + &models.User{}, + &models.Exercise{}, + &models.Workout{}, + &models.WorkoutExercise{}, + ) + if err != nil { + log.Fatal("Ошибка миграции:", err) + } + + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + DB = db +} diff --git a/exercise9/database/seed.go b/exercise9/database/seed.go new file mode 100644 index 00000000..b465512d --- /dev/null +++ b/exercise9/database/seed.go @@ -0,0 +1,30 @@ +package database + +import ( + "log" + "time" + + "alinurmyrzakhanov/models" +) + +func SeedExercises() { + var count int64 + DB.Model(&models.Exercise{}).Count(&count) + if count > 0 { + log.Println("Exercises уже существуют, сидер пропущен.") + return + } + + exercises := []models.Exercise{ + {Name: "Push-up", Description: "Classic push-up for chest", Category: "strength", CreatedAt: time.Now(), UpdatedAt: time.Now()}, + {Name: "Squat", Description: "Bodyweight squat for legs", Category: "strength", CreatedAt: time.Now(), UpdatedAt: time.Now()}, + {Name: "Running", Description: "Running cardio exercise", Category: "cardio", CreatedAt: time.Now(), UpdatedAt: time.Now()}, + {Name: "Plank", Description: "Core strength exercise", Category: "core", CreatedAt: time.Now(), UpdatedAt: time.Now()}, + } + + if err := DB.Create(&exercises).Error; err != nil { + log.Println("Ошибка при создании упражнений:", err) + } else { + log.Println("Exercises успешно засеяны в БД.") + } +} diff --git a/exercise9/go.mod b/exercise9/go.mod index 72f28b6f..126d0334 100644 --- a/exercise9/go.mod +++ b/exercise9/go.mod @@ -1,3 +1,42 @@ -module github.com/talgat-ruby/exercises-go/exercise9 +module alinurmyrzakhanov -go 1.23.5 +go 1.23.3 + +require ( + github.com/bytedance/sonic v1.12.8 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.14.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlite v1.5.7 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/exercise9/go.sum b/exercise9/go.sum new file mode 100644 index 00000000..4e15a3bb --- /dev/null +++ b/exercise9/go.sum @@ -0,0 +1,93 @@ +github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= +github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/exercise9/models/exercise.go b/exercise9/models/exercise.go new file mode 100644 index 00000000..8ab90182 --- /dev/null +++ b/exercise9/models/exercise.go @@ -0,0 +1,14 @@ +package models + +import ( + "time" +) + +type Exercise struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` + Description string `json:"description"` + Category string `json:"category"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/exercise9/models/user.go b/exercise9/models/user.go new file mode 100644 index 00000000..0249b559 --- /dev/null +++ b/exercise9/models/user.go @@ -0,0 +1,13 @@ +package models + +import ( + "time" +) + +type User struct { + ID uint `gorm:"primaryKey" json:"id"` + Email string `gorm:"unique; not null" json:"email"` + PasswordHash string `json:"-"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/exercise9/models/workout.go b/exercise9/models/workout.go new file mode 100644 index 00000000..b7e7ed6a --- /dev/null +++ b/exercise9/models/workout.go @@ -0,0 +1,32 @@ +package models + +import ( + "time" +) + +type Workout struct { + ID uint `gorm:"primaryKey" json:"id"` + UserID uint `gorm:"index" json:"userId"` + Title string `gorm:"not null" json:"title"` + Scheduled time.Time `json:"scheduled"` + Comment string `json:"comment"` + IsDone bool `json:"isDone"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + WorkoutExercises []WorkoutExercise `json:"exercises"` +} + +type WorkoutExercise struct { + ID uint `gorm:"primaryKey" json:"id"` + WorkoutID uint `gorm:"index" json:"workoutId"` + ExerciseID uint `gorm:"index" json:"exerciseId"` + Sets int `json:"sets"` + Reps int `json:"reps"` + Weight int `json:"weight"` + Comment string `json:"comment"` + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + + Exercise Exercise `gorm:"foreignKey:ID;references:ExerciseID" json:"exercise"` +} diff --git a/exercise9/repositories/exercise_repository.go b/exercise9/repositories/exercise_repository.go new file mode 100644 index 00000000..770c0fc2 --- /dev/null +++ b/exercise9/repositories/exercise_repository.go @@ -0,0 +1,36 @@ +package repositories + +import ( + "alinurmyrzakhanov/database" + "alinurmyrzakhanov/models" +) + +func CreateExercise(ex *models.Exercise) error { + return database.DB.Create(ex).Error +} + +func GetExerciseByID(id uint) (*models.Exercise, error) { + var ex models.Exercise + err := database.DB.First(&ex, id).Error + if err != nil { + return nil, err + } + return &ex, nil +} + +func GetAllExercises() ([]models.Exercise, error) { + var exercises []models.Exercise + err := database.DB.Find(&exercises).Error + if err != nil { + return nil, err + } + return exercises, nil +} + +func UpdateExercise(ex *models.Exercise) error { + return database.DB.Save(ex).Error +} + +func DeleteExercise(id uint) error { + return database.DB.Delete(&models.Exercise{}, id).Error +} diff --git a/exercise9/repositories/user_repository.go b/exercise9/repositories/user_repository.go new file mode 100644 index 00000000..091f8278 --- /dev/null +++ b/exercise9/repositories/user_repository.go @@ -0,0 +1,34 @@ +package repositories + +import ( + "alinurmyrzakhanov/database" + "alinurmyrzakhanov/models" + + "gorm.io/gorm" +) + +func CreateUser(user *models.User) error { + return database.DB.Create(user).Error +} + +func GetUserByEmail(email string) (*models.User, error) { + var user models.User + err := database.DB.Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func GetUserByID(id uint) (*models.User, error) { + var user models.User + err := database.DB.First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func IsRecordNotFound(err error) bool { + return err == gorm.ErrRecordNotFound +} diff --git a/exercise9/repositories/workout_repository.go b/exercise9/repositories/workout_repository.go new file mode 100644 index 00000000..0442fd37 --- /dev/null +++ b/exercise9/repositories/workout_repository.go @@ -0,0 +1,95 @@ +package repositories + +import ( + "alinurmyrzakhanov/database" + "alinurmyrzakhanov/models" + "time" + + "gorm.io/gorm" +) + +func CreateWorkout(workout *models.Workout) error { + return database.DB.Transaction(func(tx *gorm.DB) error { + // Сначала создаём сам Workout + if err := tx.Create(workout).Error; err != nil { + return err + } + for i := range workout.WorkoutExercises { + workout.WorkoutExercises[i].WorkoutID = workout.ID + if err := tx.Create(&workout.WorkoutExercises[i]).Error; err != nil { + return err + } + } + return nil + }) +} + +func GetWorkoutByID(id uint, userID uint) (*models.Workout, error) { + var workout models.Workout + err := database.DB.Preload("WorkoutExercises.Exercise"). + Where("id = ? AND user_id = ?", id, userID). + First(&workout).Error + if err != nil { + return nil, err + } + return &workout, nil +} + +func UpdateWorkout(workout *models.Workout) error { + return database.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(workout).Error; err != nil { + return err + } + if err := tx.Where("workout_id = ?", workout.ID).Delete(&models.WorkoutExercise{}).Error; err != nil { + return err + } + for i := range workout.WorkoutExercises { + workout.WorkoutExercises[i].WorkoutID = workout.ID + if err := tx.Create(&workout.WorkoutExercises[i]).Error; err != nil { + return err + } + } + return nil + }) +} + +func DeleteWorkout(id uint, userID uint) error { + return database.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("id = ? AND user_id = ?", id, userID). + Delete(&models.Workout{}).Error; err != nil { + return err + } + if err := tx.Where("workout_id = ?", id).Delete(&models.WorkoutExercise{}).Error; err != nil { + return err + } + return nil + }) +} + +func ListWorkouts(userID uint, onlyPending bool) ([]models.Workout, error) { + var workouts []models.Workout + query := database.DB.Preload("WorkoutExercises.Exercise"). + Where("user_id = ?", userID) + + if onlyPending { + now := time.Now() + query = query.Where("scheduled >= ? OR is_done = false", now) + } + err := query.Order("scheduled ASC").Find(&workouts).Error + if err != nil { + return nil, err + } + return workouts, nil +} + +func GetWorkoutsInRange(userID uint, from, to time.Time) ([]models.Workout, error) { + var workouts []models.Workout + err := database.DB.Preload("WorkoutExercises.Exercise"). + Where("user_id = ? AND scheduled BETWEEN ? AND ?", userID, from, to). + Order("scheduled ASC"). + Find(&workouts).Error + if err != nil { + return nil, err + } + return workouts, nil +} diff --git a/exercise9/routes/routes.go b/exercise9/routes/routes.go new file mode 100644 index 00000000..8220b48a --- /dev/null +++ b/exercise9/routes/routes.go @@ -0,0 +1,32 @@ +package routes + +import ( + "alinurmyrzakhanov/controllers" + + "github.com/gin-gonic/gin" +) + +func SetupRoutes(r *gin.Engine) { + r.POST("/signup", controllers.SignUp) + r.POST("/login", controllers.Login) + r.POST("/logout", controllers.AuthMiddleware(), controllers.Logout) + + exerciseGroup := r.Group("/exercises") + { + exerciseGroup.POST("", controllers.CreateExercise) + exerciseGroup.GET("", controllers.GetExercises) + exerciseGroup.GET("/:id", controllers.GetExerciseByID) + exerciseGroup.PUT("/:id", controllers.UpdateExercise) + exerciseGroup.DELETE("/:id", controllers.DeleteExercise) + } + + authGroup := r.Group("/workouts") + authGroup.Use(controllers.AuthMiddleware()) + { + authGroup.POST("", controllers.CreateWorkout) + authGroup.GET("", controllers.ListWorkouts) + authGroup.GET("/report", controllers.GetWorkoutReport) + authGroup.PUT("/:id", controllers.UpdateWorkout) + authGroup.DELETE("/:id", controllers.DeleteWorkout) + } +} diff --git a/exercise9/tests/auth_test.go b/exercise9/tests/auth_test.go new file mode 100644 index 00000000..ef7da154 --- /dev/null +++ b/exercise9/tests/auth_test.go @@ -0,0 +1,219 @@ +package tests + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "alinurmyrzakhanov/database" + "alinurmyrzakhanov/models" + "alinurmyrzakhanov/repositories" + "alinurmyrzakhanov/routes" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func setupTestDB(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("Не удалось открыть in-memory DB: %v", err) + } + + err = db.AutoMigrate( + &models.User{}, + &models.Exercise{}, + &models.Workout{}, + &models.WorkoutExercise{}, + ) + if err != nil { + t.Fatalf("Ошибка миграции: %v", err) + } + + database.DB = db +} + +func setupRouter() *gin.Engine { + r := gin.Default() + routes.SetupRoutes(r) + return r +} + +func createTestExercise(t *testing.T, name, category string) *models.Exercise { + ex := models.Exercise{ + Name: name, + Description: "test desc", + Category: category, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + err := repositories.CreateExercise(&ex) + if err != nil { + t.Fatalf("Не удалось создать упражнение в тесте: %v", err) + } + return &ex +} + +func TestCreateExercise(t *testing.T) { + setupTestDB(t) + router := setupRouter() + + body := `{ + "name": "Bench Press", + "description": "Chest exercise", + "category": "strength" + }` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/exercises", bytes.NewBuffer([]byte(body))) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code, "Должен вернуться статус 201") + + var created models.Exercise + err := json.Unmarshal(w.Body.Bytes(), &created) + assert.Nil(t, err) + + assert.Equal(t, "Bench Press", created.Name) + assert.Equal(t, "strength", created.Category) + assert.NotZero(t, created.ID, "ID должен быть сгенерирован") +} + +func TestGetAllExercises(t *testing.T) { + setupTestDB(t) + router := setupRouter() + + createTestExercise(t, "Push-up", "strength") + createTestExercise(t, "Squat", "strength") + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/exercises", nil) + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, "Статус должен быть 200") + + var exercises []models.Exercise + err := json.Unmarshal(w.Body.Bytes(), &exercises) + assert.Nil(t, err) + + assert.GreaterOrEqual(t, len(exercises), 2, "Должно вернуться как минимум 2 упражнения") +} + +func TestGetExerciseByID(t *testing.T) { + setupTestDB(t) + router := setupRouter() + + testEx := createTestExercise(t, "Push-up", "strength") + + w := httptest.NewRecorder() + + url := "/exercises/" + strconv.Itoa(int(testEx.ID)) + req, _ := http.NewRequest("GET", url, nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var fetched models.Exercise + err := json.Unmarshal(w.Body.Bytes(), &fetched) + assert.Nil(t, err) + + assert.Equal(t, testEx.ID, fetched.ID) + assert.Equal(t, "Push-up", fetched.Name) +} + +func TestUpdateExercise(t *testing.T) { + setupTestDB(t) + router := setupRouter() + + testEx := createTestExercise(t, "Bench Press", "strength") + + updateBody := `{ + "name": "Bench Press (updated)", + "description": "New desc", + "category": "upper body" + }` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", + "/exercises/"+(func() string { + return string(rune(testEx.ID + '0')) + })(), + bytes.NewBuffer([]byte(updateBody)), + ) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var updated models.Exercise + err := json.Unmarshal(w.Body.Bytes(), &updated) + assert.Nil(t, err) + + assert.Equal(t, testEx.ID, updated.ID) + assert.Equal(t, "Bench Press (updated)", updated.Name) + assert.Equal(t, "New desc", updated.Description) + assert.Equal(t, "upper body", updated.Category) +} + +func TestDeleteExercise(t *testing.T) { + setupTestDB(t) + router := setupRouter() + + testEx := createTestExercise(t, "Plank", "core") + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", + "/exercises/"+(func() string { + return string(rune(testEx.ID + '0')) + })(), + nil, + ) + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNoContent, w.Code, "Ожидаем 204 No Content") + + w2 := httptest.NewRecorder() + req2, _ := http.NewRequest("GET", + "/exercises/"+(func() string { + return string(rune(testEx.ID + '0')) + })(), + nil, + ) + router.ServeHTTP(w2, req2) + + assert.Equal(t, http.StatusNotFound, w2.Code, "После удаления GET должен вернуть 404") +} + +func TestGetNonExistentExercise(t *testing.T) { + setupTestDB(t) + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/exercises/9999", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code, "Ожидаем 404 для несуществующего ID") +} + +func TestCreateExercise_ValidationError(t *testing.T) { + setupTestDB(t) + router := setupRouter() + body := `{ + "description": "Missing name", + "category": "strength" + }` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/exercises", bytes.NewBuffer([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, "Ожидаем 400 Bad Request") +}