diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..6c8644a8 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,10 @@ package main -func addUp() {} +func addUp(a int) int { + res := 0 + + for i:=1; i<=a; i++ { + res +=i + } + return res +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..1e40c1f4 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,19 @@ package main -func sum() {} +import "strconv" + +func sum(a, b string) (string, error) { + numA, err := strconv.Atoi(a) + if err != nil { + return "", err + } + + numB, err := strconv.Atoi(b) + if err != nil { + return "", err + } + + res := numA + numB + str := strconv.Itoa(res) + return str, nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..2163c7cc 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,18 @@ package main -func binary() {} +import "strconv" + +func binary(a int) string { + if a == 0 { + return "0" + } + res := "" + + for a != 0 { + b := a % 2 + c := strconv.Itoa(b) + res = c + res + a = a / 2 + } + return res +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..a5c0dae3 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,13 @@ package main -func numberSquares() {} +func numberSquares(a int) int { + res := 0 + + for i:=1; i<=a; i++ { + b := a - i + 1 + b = b * b + res+= b + } + + return res +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..5b78a056 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,12 @@ package main -func detectWord() {} +func detectWord(a string) string { + res := "" + + for _, i := range a { + if i >='a' && i <= 'z' { + res += string(i) + } + } + return res +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..62f04f97 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,18 @@ package main -func potatoes() {} +func potatoes(a string) int { + res := 0 + str := "" + for i := 0; i 'z' { + char = string(a[len(a)-1]) + a = a[:len(a)-1] + } + + for j, i := range b { + if i == a { + if char != "" { + return c[j]+char, true + } + return c[j], true + } + } + return "", false +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..6331b510 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,14 @@ package main -func highestDigit() {} +func highestDigit(a int) int { + res := 0 + + for a != 0 { + b := a%10 + if b > res { + res = b + } + a = a/10 + } + return res +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..5149f931 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,17 @@ package main -func countVowels() {} +func countVowels(a string) int { + res := 0 + + for _, i := range a { + if vowels(i) { + res++ + } + } + + return res +} + +func vowels(a rune) bool { + return a == 'a' || a == 'e' || a == 'i' || a == 'o' || a == 'u' || a == 'A' || a == 'E' || a == 'I' || a == 'O' || a == 'U' +} \ No newline at end of file diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..b1023eff 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,97 @@ package main -func bitwiseAND() {} +import ( + "strconv" +) -func bitwiseOR() {} +func bitwiseAND(a int, b int) int { + joinab := "" + ares,bres := backJob(a,b) -func bitwiseXOR() {} + for i := range bres { + if bres[i] == '1' && ares[i] == '1' { + joinab += "1" + } else { + joinab += "0" + } + } + + num, err := strconv.ParseInt(joinab, 2, 64) + if err != nil { + return 0 + } + res := int(num) + return res + +} + +func bitwiseOR(a int, b int) int { + joinab := "" + ares,bres := backJob(a,b) + + for i := range bres { + if bres[i] == '1' || ares[i] == '1' { + joinab += "1" + } else { + joinab += "0" + } + } + num, err := strconv.ParseInt(joinab, 2, 64) + if err != nil { + return 0 + } + res := int(num) + return res +} + +func bitwiseXOR(a int, b int) int { + joinab := "" + ares,bres := backJob(a,b) + + for i := range bres { + if bres[i] != ares[i] { + joinab += "1" + } else { + joinab += "0" + } + } + num, err := strconv.ParseInt(joinab, 2, 64) + if err != nil { + return 0 + } + res := int(num) + return res + +} + +func backJob(a int, b int) (string, string) { + ares := "" + count := 0 + for a != 0 { + k := a % 2 + c := strconv.Itoa(k) + ares = c + ares + a = a / 2 + } + bres := "" + for b != 0 { + k := b % 2 + c := strconv.Itoa(k) + bres = c + bres + b = b / 2 + } + + if len(bres) > len(ares) { + count = len(bres) - len(ares) + for i := 0; i < count; i++ { + ares = "0" + ares + } + } else { + count = len(ares) - len(bres) + for i := 0; i < count; i++ { + bres = "0" + bres + } + } + return ares, bres + +} \ No newline at end of file diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..6c531ffc 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,13 @@ package problem1 -func isChangeEnough() { +func isChangeEnough(nums [4]int, num float32) bool { + + var res float32 + n := [4]float32{0.25, 0.10, 0.05, 0.01} + for i := 0; i < len(nums); i++ { + count := float32(nums[i]) * n[i] + res += count + } + + return res >= num } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..01392330 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,17 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(brand string) func(int)) { + brands := make(map[string]int) + + makeBrand := func(brand string) func(int) { + if _, exists := brands[brand]; !exists { + 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..446900bd 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,18 @@ package problem11 -func removeDups() {} +func removeDups[T comparable](input []T) []T { + var res []T + for _, i := range input { + count := 0 + for _, j := range res { + if j == i { + count++ + } + } + if count == 0 { + res = append(res, i) + } + } + return res +} + diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..e1782ad4 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)) + for k := range m { + keys = append(keys, k) + } + + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprint(keys[i]) < fmt.Sprint(keys[j]) + }) + + values := make([]V, 0, len(m)) + for _, k := range keys { + values = append(values, m[k]) + } + + return keys, values +} diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..d0d9d04f 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,25 @@ package problem2 -func capitalize() { +func capitalize(str []string) []string { + res := []string{} + for i := 0; i < len(str); i++ { + name := "" + if len(str[i]) > 0 { + if str[i][0] > 96 && str[i][0] < 123 { + name += string(str[i][0] - 32) + } else { + name += string(str[i][0]) + } + for j := 1; j < len(str[i]); j++ { + if str[i][j] > 64 && str[i][j] < 91 { + name += string(str[i][j] + 32) + } else { + name += string(str[i][j]) + } + + } + } + res = append(res, name) + } + return res } diff --git a/exercise2/problem3/problem3.go b/exercise2/problem3/problem3.go index f183fafb..533e29a4 100644 --- a/exercise2/problem3/problem3.go +++ b/exercise2/problem3/problem3.go @@ -9,5 +9,56 @@ const ( lr dir = "lr" ) -func diagonalize() { +func diagonalize(n int, d dir) [][]int { + nums := []int{} + res := [][]int{} + if d == ul { + count := 0 + for i := 0; i = b { + list = append(list, Product{Name: name, Price: price}) + } + } + + sort.Slice(list, func(i, j int) bool { + if list[i].Price == list[j].Price { + return list[i].Name < list[j].Name + } + return list[i].Price > list[j].Price + }) + + var res []string + + for _, i := range list { + res = append(res, i.Name) + } + + return res } diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..bd5dbc13 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,12 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(a []int, b []int, c int) bool { + for _, i := range a { + for _, j := range b { + if i + j == c { + return true + } + } + } + return false } diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..b6cdf173 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,5 @@ package problem7 -func swap() { +func swap(a *int, b *int) { + *a, *b = *b, *a } diff --git a/exercise2/problem8/problem8.go b/exercise2/problem8/problem8.go index 9389d3b0..866aef6c 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -1,16 +1,14 @@ package problem8 func simplify(list []string) map[string]int { - var indMap map[string]int - - indMap = make(map[string]int) - load(&indMap, &list) + indMap := make(map[string]int) + load(indMap, list) return indMap } -func load(m *map[string]int, students *[]string) { - for i, name := range *students { - (*m)[name] = i +func load(m map[string]int, students []string) { + for i, name := range students { + m[name] = i } } diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..89686dd9 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,11 @@ package problem9 -func factory() {} +func factory(a int) func (nums ...int) []int { + return func(nums ...int) []int { + res := make([]int, len(nums)) + for i, num := range nums { + res[i] = num * a + } + return res + } +} diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..5609bbef 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,38 @@ package problem1 -type Queue struct{} +import "errors" + +type Queue struct { + Elements []any +} + +func (q *Queue) Enqueue(element any) { + q.Elements = append(q.Elements, element) +} + +func (q *Queue) Dequeue() (any, error) { + if len(q.Elements) == 0 { + return 0, errors.New("Error") + } + + element := q.Elements[0] + q.Elements = q.Elements[1:] + return element, nil +} + +func (q *Queue) Peek() (any, error) { + if len(q.Elements) == 0 { + return 0, errors.New("Error") + } + + element := q.Elements[0] + return element, nil +} + +func (q *Queue) Size() any { + return len(q.Elements) +} + +func (q *Queue) IsEmpty() bool { + return len(q.Elements) == 0 +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..a1807b96 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 { + Elements []any +} + +func (s *Stack) Push(element any) { + s.Elements = append(s.Elements, element) +} + +func (s *Stack) Pop() (any, error) { + if len(s.Elements) == 0 { + return 0, errors.New("Error") + } + element := s.Elements[len(s.Elements)-1] + s.Elements = s.Elements[:len(s.Elements)-1] + return element, nil +} + +func (s *Stack)Peek() (any, error) { + if len(s.Elements) == 0 { + return 0, errors.New("Error") + } + + return s.Elements[len(s.Elements)-1], nil +} + +func (s *Stack)Size() int { + return len(s.Elements) +} + +func (s *Stack)IsEmpty() bool { + return len(s.Elements) == 0 +} + diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..87c10761 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,128 @@ package problem3 -type Set struct{} +type Set struct{ + Elements []any +} + +func (s *Set)Add(element any) { + if !s.Has(element) { + s.Elements = append(s.Elements, element) + } +} + +func (s *Set)Remove(element any) { + for k, i := range s.Elements { + if i == element { + s.Elements =append(s.Elements[:k], s.Elements[k+1:]... ) + } + } +} + +func (s *Set)IsEmpty() bool { + return len(s.Elements) == 0 +} + +func (s *Set)Size() int { + return len(s.Elements) +} + +func (s *Set)List() []any { + elements := []any{} + elements = append(elements, s.Elements...) + return elements +} + +func (s *Set)Has(element any) bool { + for _, i := range s.Elements { + if i == element { + return true + } + } + return false +} + +func (s *Set)Copy() *Set { + return s +} + +func (s *Set)Difference(elements *Set) *Set { + element := NewSet() + + for _, i := range s.Elements { + count := 0 + for _, j := range elements.Elements { + if i == j { + count++ + } + } + if count == 0 { + element.Elements = append(element.Elements, i) + } + } + + return element +} + +func (s *Set)IsSubset(elements *Set) bool { + + for _, i := range s.Elements { + count := 0 + for _, j := range elements.Elements { + if i == j { + count++ + } + } + if count == 0 { + return false + } + } + + return true +} + +func Union(s ...*Set) *Set { + newElements := &Set{} + + for _, i:= range s { + for _, j := range i.Elements { + if !newElements.Has(j) { + newElements.Elements = append(newElements.Elements, j) + } + } + } + return newElements +} + +func Intersect(s ...*Set) *Set { + newElements := &Set{} + + s1 := s[0] + s2 := s[1:] + + for _, i:= range s1.Elements { + count1 := 0 + for _, j := range s2 { + count2 := 0 + for _, k := range j.Elements { + if i == k { + count2++ + } + } + if count2 != 0 { + count1++ + } + } + if count1 == len(s2) { + newElements.Elements = append(newElements.Elements, i) + } + } + return newElements +} + +func NewSet() *Set { + return &Set{ + Elements: []any{}, + } +} + + diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..4e87a632 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,120 @@ package problem4 -type LinkedList struct{} +import ( + "errors" +) + +type Element[T comparable] struct { + value T + next *Element[T] +} + +type LinkedList[T comparable] struct { + Head *Element[T] + size int +} + +func (l *LinkedList[T]) Add(n *Element[T]) { + if l.Head == nil { + l.Head = n + } else { + current := l.Head + for current.next != nil { + current = current.next + } + current.next = n + } + l.size++ +} + +func (l *LinkedList[T]) Size() int { + return l.size +} + +func (l *LinkedList[T]) IsEmpty() bool { + return l.Head == nil +} + +func (l *LinkedList[T]) Insert(n *Element[T], pos int) error { + if pos < 0 || pos > l.size { + return errors.New("Error") + } + + if pos == 0 { + n.next = l.Head + l.Head = n + } else { + current := l.Head + for i := 0; i < pos-1; i++ { + current = current.next + } + n.next = current.next + current.next = n + } + + l.size++ + return nil +} + +func (l *LinkedList[T]) Delete(n *Element[T]) error { + if l.Head == nil { + return errors.New("Empty") + } + + if l.Head.value == n.value { + l.Head = l.Head.next + l.size-- + return nil + } + + previous := l.Head + current := l.Head.next + + for current != nil { + + if current.value == n.value { + previous.next = current.next + l.size-- + return nil + } + + previous = current + current = current.next + } + return errors.New("Error") +} + +func (l *LinkedList[T]) Find(n any) (*Element[T], error) { + + if l.Head == nil { + return nil, errors.New("Empty") + } + + current := l.Head + + for current != nil { + + if current.value == n { + + return current, nil + } + + current = current.next + } + + return nil, errors.New("not find") +} + +func (l *LinkedList[T]) List() []T { + elements := []T{} + + current := l.Head + + for current != nil { + elements = append(elements, current.value) + current = current.next + } + + return elements + +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..a694a735 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,18 @@ package problem5 -type Person struct{} +type Person struct { + Name string + Age int +} + +func (p *Person) compareAge(b *Person) string { + if b.Age > p.Age { + return b.Name + " is older than me." + } + + if b.Age < p.Age { + return b.Name + " is younger than me." + } + + return b.Name + " is the same age as me." +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..22f5539a 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,21 @@ package problem6 -type Animal struct{} +type Animal struct{ + name string + legsNum int +} -type Insect struct{} +type Insect struct{ + name string + legsNum int +} -func sumOfAllLegsNum() {} +func sumOfAllLegsNum(horse, kangaroo *Animal, ant, spider *Insect) int { + count := 0 + + count += horse.legsNum + count += kangaroo.legsNum + count += ant.legsNum + count += spider.legsNum + return count +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..6e9bff3d 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,27 @@ package problem7 type BankAccount struct { + name string + balance int } type FedexAccount struct { + name string + packages []string } type KazPostAccount struct { + name string + balance int + packages []string +} + +func withdrawMoney(num int, client1 *BankAccount, client2 *KazPostAccount) { + client1.balance -= num + client2.balance -= num +} + +func sendPackagesTo(name string, client1 *FedexAccount, client2 *KazPostAccount) { + client1.packages = append(client1.packages, client1.name + " send package to " + name) + client2.packages = append(client2.packages, client2.name + " send package to " + name) } diff --git a/exercise4/bot/internal/api/handler/handler.go b/exercise4/bot/internal/api/handler/handler.go new file mode 100644 index 00000000..28e84831 --- /dev/null +++ b/exercise4/bot/internal/api/handler/handler.go @@ -0,0 +1,15 @@ +package handler + +import ( + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/model" +) + +type Handler struct { + game *model.Game +} + +func New() *Handler { + return &Handler{ + game: model.NewGame(), + } +} diff --git a/exercise4/bot/internal/api/handler/move.go b/exercise4/bot/internal/api/handler/move.go new file mode 100644 index 00000000..1679c0e3 --- /dev/null +++ b/exercise4/bot/internal/api/handler/move.go @@ -0,0 +1,33 @@ +package handler + +import ( + "encoding/json" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/dto" + "log/slog" + "net/http" +) + +func (h *Handler) Move(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := slog.With( + "handler", "Move", + "path", r.URL.Path, + ) + log.InfoContext( + ctx, + "move", + ) + moveRequest := dto.RequestMove{} + err := json.NewDecoder(r.Body).Decode(&moveRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + ind := moveRequest.Board.CalculateNewPosition(moveRequest.Token) + w.Header().Set("Content-Type", "application/json") + moveResponse := dto.ResponseMove{Index: ind} + err = json.NewEncoder(w).Encode(moveResponse) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/exercise4/bot/internal/api/handler/ping.go b/exercise4/bot/internal/api/handler/ping.go new file mode 100644 index 00000000..e8c0ecc9 --- /dev/null +++ b/exercise4/bot/internal/api/handler/ping.go @@ -0,0 +1,20 @@ +package handler + +import ( + "log/slog" + "net/http" +) + +func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := slog.With( + "handler", "Ping", + "path", r.URL.Path, + ) + + log.InfoContext( + ctx, + "pinged", + ) + w.WriteHeader(http.StatusOK) +} diff --git a/exercise4/bot/internal/api/router/router.go b/exercise4/bot/internal/api/router/router.go new file mode 100644 index 00000000..61adbb23 --- /dev/null +++ b/exercise4/bot/internal/api/router/router.go @@ -0,0 +1,17 @@ +package router + +import ( + "net/http" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api/handler" +) + +func New() *http.ServeMux { + han := handler.New() + mux := http.NewServeMux() + + mux.Handle("GET /ping", http.HandlerFunc(han.Ping)) + mux.Handle("POST /move", http.HandlerFunc(han.Move)) + + return mux +} diff --git a/exercise4/bot/internal/config/api.go b/exercise4/bot/internal/config/api.go new file mode 100644 index 00000000..9ffed754 --- /dev/null +++ b/exercise4/bot/internal/config/api.go @@ -0,0 +1,58 @@ +package config + +import ( + "context" + "errors" + "fmt" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api/router" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/constants" + "log" + "log/slog" + "net" + "net/http" +) + +type Api struct { + srv *http.Server + port string + ctx context.Context +} + +func (api *Api) Start(preInitFun func()) { + r := router.New() + ctx := context.Background() + // start up HTTP server + api.port = constants.PORT + api.srv = &http.Server{ + Addr: fmt.Sprintf(":%s", api.port), + Handler: r, + BaseContext: func(_ net.Listener) context.Context { + return ctx + }, + } + api.ctx = ctx + + slog.InfoContext( + api.ctx, + "starting service", + "port", api.port, + ) + + go preInitFun() + + if err := api.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatal(api.ctx, "service error", "error", err) + } + +} + +func (api *Api) Stop() { + slog.InfoContext( + api.ctx, + "closing service", + "port", api.port, + ) + if err := api.srv.Shutdown(api.ctx); err != nil { + log.Fatal(api.ctx, "server shutdown error", "error", err) + } +} diff --git a/exercise4/bot/internal/config/config.go b/exercise4/bot/internal/config/config.go new file mode 100644 index 00000000..649f5e79 --- /dev/null +++ b/exercise4/bot/internal/config/config.go @@ -0,0 +1,13 @@ +package config + +type Config struct { + api *Api +} + +func New() *Config { + return &Config{api: &Api{}} +} + +func (c *Config) GetApi() *Api { + return c.api +} diff --git a/exercise4/bot/internal/constants/client.go b/exercise4/bot/internal/constants/client.go new file mode 100644 index 00000000..d0c6b77c --- /dev/null +++ b/exercise4/bot/internal/constants/client.go @@ -0,0 +1,8 @@ +package constants + +import "os" + +var NAME = os.Getenv("NAME") +var PORT = os.Getenv("PORT") +var URL = "localhost:" + PORT +var FullUrl = "http://" + URL diff --git a/exercise4/bot/internal/constants/server.go b/exercise4/bot/internal/constants/server.go new file mode 100644 index 00000000..78fb3eff --- /dev/null +++ b/exercise4/bot/internal/constants/server.go @@ -0,0 +1,7 @@ +package constants + +import "os" + +const JoinPath string = "/join" + +var Host = os.Getenv("HOST") diff --git a/exercise4/bot/internal/dto/request.go b/exercise4/bot/internal/dto/request.go new file mode 100644 index 00000000..91e77905 --- /dev/null +++ b/exercise4/bot/internal/dto/request.go @@ -0,0 +1,15 @@ +package dto + +import ( + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/model" +) + +type RequestJoin struct { + Name string `json:"name"` + URL string `json:"url"` +} + +type RequestMove struct { + Board *model.Board `json:"board"` + Token model.Token `json:"token"` +} diff --git a/exercise4/bot/internal/dto/response.go b/exercise4/bot/internal/dto/response.go new file mode 100644 index 00000000..578fd47c --- /dev/null +++ b/exercise4/bot/internal/dto/response.go @@ -0,0 +1,5 @@ +package dto + +type ResponseMove struct { + Index int `json:"index"` +} diff --git a/exercise4/bot/internal/game/join.go b/exercise4/bot/internal/game/join.go new file mode 100644 index 00000000..76308c44 --- /dev/null +++ b/exercise4/bot/internal/game/join.go @@ -0,0 +1,51 @@ +package game + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + constants2 "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/constants" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/dto" + "log/slog" + "net/http" + "time" +) + +func JoinGame() error { + + // timeout after 5 sec + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + body := dto.RequestJoin{Name: constants2.NAME, URL: constants2.FullUrl} + + jsonData, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("failed to marshal join request: %w", err) + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + constants2.Host+constants2.JoinPath, + bytes.NewBuffer(jsonData), + ) + + if err != nil { + return fmt.Errorf("error creating join request %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("error making join request %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed making join request %d - %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + slog.Info("successfully joined the game") + + return nil +} diff --git a/exercise4/bot/internal/model/board.go b/exercise4/bot/internal/model/board.go new file mode 100644 index 00000000..c52f9c68 --- /dev/null +++ b/exercise4/bot/internal/model/board.go @@ -0,0 +1,183 @@ +package model + +import "fmt" + +type Token string + +const ( + TokenEmpty Token = " " + TokenX Token = "x" + TokenO Token = "o" +) + +const ( + Cols = 3 + Rows = 3 +) + +var motion int + +type Board [Cols * Rows]Token + +func (b *Board) CalculateNewPosition(token Token) int { + ind := b.calculateNewPosition(token) + b.logMove(ind, token) + return ind +} + +func (b *Board) logMove(pos int, token Token) { + b[pos] = token + fmt.Println(b.String()) +} + +func (b *Board) String() string { + str := fmt.Sprintf( + ` + %s|%s|%s + ----- + %s|%s|%s + ----- + %s|%s|%s +`, + b[0], + b[1], + b[2], + b[3], + b[4], + b[5], + b[6], + b[7], + b[8], + ) + + return str +} + +func (b *Board) calculateNewPosition(token Token) int { + // Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ счётчик Ρ…ΠΎΠ΄ΠΎΠ² + motion++ + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ количСство Π·Π°ΠΏΠΎΠ»Π½Π΅Π½Π½Ρ‹Ρ… ячССк Π½Π° доскС + count := 0 + for _, p := range b { + if p != TokenEmpty { + count++ + } + } + + // Если Π½Π° доскС Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ Ρ…ΠΎΠ΄ ΠΈ Ρ†Π΅Π½Ρ‚Ρ€Π°Π»ΡŒΠ½Π°Ρ ячСйка пуста, ставим Ρ‚ΠΎΠΊΠ΅Π½ Π² Ρ†Π΅Π½Ρ‚Ρ€ + if count == 1 && b[4] == TokenEmpty { + return 4 + } + + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ всС Π²ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹Π΅ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚Ρ‹ Π²Ρ‹ΠΈΠ³Ρ€Ρ‹ΡˆΠ½Ρ‹Ρ… ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΉ + alloptions := b.allWin() + // Находим ΠΎΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Ρ…ΠΎΠ΄Ρ‹ для Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ ΠΈΠ³Ρ€ΠΎΠΊΠ° + optmoves := optimalMoves(alloptions) + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π΅ΡΡ‚ΡŒ Π»ΠΈ Π²Ρ‹ΠΈΠ³Ρ€Ρ‹ΡˆΠ½Ρ‹Π΅ ΠΈΠ»ΠΈ Π·Π°Ρ‰ΠΈΡ‚Π½Ρ‹Π΅ Ρ…ΠΎΠ΄Ρ‹ + if len(optmoves) > 0 { + // Если Π΅ΡΡ‚ΡŒ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π²Ρ‹ΠΈΠ³Ρ€Π°Ρ‚ΡŒ, Π²Ρ‹Π±ΠΈΡ€Π°Π΅ΠΌ этот Ρ…ΠΎΠ΄. + // Если Π½Π΅Ρ‚, Π²Ρ‹Π±ΠΈΡ€Π°Π΅ΠΌ Ρ…ΠΎΠ΄ для Π·Π°Ρ‰ΠΈΡ‚Ρ‹. + emptyToken := findEmptySlotForToken(optmoves, token) + return emptyToken + } + + // Если Π½Π΅Ρ‚ Π²Ρ‹ΠΈΠ³Ρ€Ρ‹ΡˆΠ½Ρ‹Ρ… ΠΈΠ»ΠΈ Π·Π°Ρ‰ΠΈΡ‚Π½Ρ‹Ρ… Ρ…ΠΎΠ΄ΠΎΠ², ΠΈΡ‰Π΅ΠΌ пустыС ΡƒΠ³Π»Ρ‹ + emtyCorner := b.findEmptyCorners() + return emtyCorner +} + +func (b *Board) allWin() []map[int]Token { + // ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹Π΅ Π²Ρ‹ΠΈΠ³Ρ€Ρ‹ΡˆΠ½Ρ‹Π΅ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΈ + winCombinations := [][]int{ + {0, 1, 2}, // Π“ΠΎΡ€ΠΈΠ·ΠΎΠ½Ρ‚Π°Π»ΡŒΠ½Ρ‹Π΅ Π»ΠΈΠ½ΠΈΠΈ + {3, 4, 5}, + {6, 7, 8}, + {0, 3, 6}, // Π’Π΅Ρ€Ρ‚ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Π΅ Π»ΠΈΠ½ΠΈΠΈ + {1, 4, 7}, + {2, 5, 8}, + {0, 4, 8}, // Π”ΠΈΠ°Π³ΠΎΠ½Π°Π»ΠΈ + {2, 4, 6}, + } + + // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ массив для хранСния всСх Π²Ρ‹ΠΈΠ³Ρ€Ρ‹ΡˆΠ½Ρ‹Ρ… ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΉ + alloptions := []map[int]Token{} + + // ЗаполняСм массив Π²Ρ‹ΠΈΠ³Ρ€Ρ‹ΡˆΠ½Ρ‹Ρ… ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΉ Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΌΠΈ значСниями с доски + for _, combination := range winCombinations { + option := make(map[int]Token) + for _, index := range combination { + option[index] = b[index] + } + alloptions = append(alloptions, option) + } + + return alloptions +} + +func optimalMoves(alloptions []map[int]Token) []map[int]Token { + // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ массив для хранСния ΠΎΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Ρ… Ρ…ΠΎΠ΄ΠΎΠ² + optmoves := []map[int]Token{} + + // ΠŸΠ΅Ρ€Π΅Π±ΠΈΡ€Π°Π΅ΠΌ всС Π²Ρ‹ΠΈΠ³Ρ€Ρ‹ΡˆΠ½Ρ‹Π΅ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΈ + for _, option := range alloptions { + a := []Token{} + for _, value := range option { + a = append(a, value) + } + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ условия для ΠΏΠΎΠ±Π΅Π΄Ρ‹ ΠΈΠ»ΠΈ Π·Π°Ρ‰ΠΈΡ‚Ρ‹ + if (a[0] == a[1] && a[0] != TokenEmpty && a[2] == TokenEmpty) || + (a[0] == a[2] && a[0] != TokenEmpty && a[1] == TokenEmpty) || + (a[1] == a[2] && a[1] != TokenEmpty && a[0] == TokenEmpty) { + // ДобавляСм Ρ‚Π΅ΠΊΡƒΡ‰ΡƒΡŽ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΡŽ Π² ΠΎΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Ρ…ΠΎΠ΄Ρ‹ + optmoves = append(optmoves, option) + } + } + return optmoves +} + +func findEmptySlotForToken(variants []map[int]Token, token Token) int { + var emptyIndex int + + // ΠŸΠ΅Ρ€Π΅Π±ΠΈΡ€Π°Π΅ΠΌ всС Π²Π°Ρ€ΠΈΠ°Π½Ρ‚Ρ‹ + for _, variant := range variants { + hasToken := false + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π΅ΡΡ‚ΡŒ Π»ΠΈ Ρ‚ΠΎΠΊΠ΅Π½ ΠΈ Π·Π°ΠΏΠΎΠΌΠΈΠ½Π°Π΅ΠΌ индСкс пустой ячСйки + for key, value := range variant { + if value == token { + hasToken = true + } else if value == TokenEmpty { + emptyIndex = key + } + } + + // Если Π½Π°ΠΉΠ΄Π΅Π½ Ρ‚ΠΎΠΊΠ΅Π½, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ индСкс пустой ячСйки + if hasToken { + return emptyIndex + } + } + + return emptyIndex // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ индСкс пустой ячСйки +} + +func (b *Board) findEmptyCorners() int { + // ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ индСксы ΡƒΠ³Π»ΠΎΠ²Ρ‹Ρ… ячССк + corners := []int{0, 2, 6, 8} + + // ΠŸΠ΅Ρ€Π΅Π±ΠΈΡ€Π°Π΅ΠΌ ΡƒΠ³Π»ΠΎΠ²Ρ‹Π΅ ячСйки + for _, index := range corners { + if b[index] == TokenEmpty { + // ΠŸΡ€ΠΎΠΏΡƒΡΠΊΠ°Π΅ΠΌ индСкс 2, Ссли Motion == 2 + if index == 2 && motion == 2 { + continue + } + return index // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ индСкс пустой ΡƒΠ³Π»ΠΎΠ²ΠΎΠΉ ячСйки + } + } + + return -1 // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ -1, Ссли всС ΡƒΠ³Π»ΠΎΠ²Ρ‹Π΅ ячСйки заняты +} + diff --git a/exercise4/bot/internal/model/game.go b/exercise4/bot/internal/model/game.go new file mode 100644 index 00000000..2c66c305 --- /dev/null +++ b/exercise4/bot/internal/model/game.go @@ -0,0 +1,11 @@ +package model + +type Game struct { + State string `json:"state"` +} + +func NewGame() *Game { + return &Game{ + State: "start", + } +} diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..2c38ebca 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -1,21 +1,27 @@ package main import ( - "context" + "fmt" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/config" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/constants" + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/game" + "log/slog" "os" - "os/signal" - "syscall" ) func main() { - ctx := context.Background() - - ready := startServer() - <-ready - - // TODO after server start + apiConfig := config.New().GetApi() + preInitFun := preInitFunc(apiConfig.Stop) + apiConfig.Start(preInitFun) +} - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - <-stop // Wait for SIGINT or SIGTERM +func preInitFunc(internalModifier func()) func() { + return func() { + err := game.JoinGame() + if err != nil { + slog.Error(fmt.Sprintf("could not join game with host %s: %s", constants.Host, err)) + internalModifier() + os.Exit(1) + } + } } diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go deleted file mode 100644 index e6760ec5..00000000 --- a/exercise4/bot/server.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net" - "net/http" - "os" - "sync" - "time" -) - -type readyListener struct { - net.Listener - ready chan struct{} - once sync.Once -} - -func (l *readyListener) Accept() (net.Conn, error) { - l.once.Do(func() { close(l.ready) }) - return l.Listener.Accept() -} - -func startServer() <-chan struct{} { - ready := make(chan struct{}) - - listener, err := net.Listen("tcp", fmt.Sprintf(":%s", os.Getenv("PORT"))) - if err != nil { - panic(err) - } - - list := &readyListener{Listener: listener, ready: ready} - srv := &http.Server{ - IdleTimeout: 2 * time.Minute, - } - - go func() { - err := srv.Serve(list) - if !errors.Is(err, http.ErrServerClosed) { - panic(err) - } - }() - - return ready -} diff --git a/exercise4/judge/internal/ticTacToe/game/leaderboard.go b/exercise4/judge/internal/ticTacToe/game/leaderboard.go index 21f4b23d..b43b0e4f 100644 --- a/exercise4/judge/internal/ticTacToe/game/leaderboard.go +++ b/exercise4/judge/internal/ticTacToe/game/leaderboard.go @@ -44,16 +44,13 @@ func (g *Game) getAllPlayersWithPoints() []*PlayerWithPoints { plsM[r.Players[0].URL].points += PointsDraw plsM[r.Players[1].URL].points += PointsDraw break - } - - if r.Winner.URL == r.Players[0].URL { + } else if r.Winner.URL == r.Players[0].URL { plsM[r.Players[0].URL].points += PointsWin plsM[r.Players[1].URL].points += PointsLose - break + } else { + plsM[r.Players[0].URL].points += PointsLose + plsM[r.Players[1].URL].points += PointsWin } - - plsM[r.Players[0].URL].points += PointsLose - plsM[r.Players[1].URL].points += PointsWin } } diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..5da8ea96 100644 --- a/exercise5/problem1/problem1.go +++ b/exercise5/problem1/problem1.go @@ -1,9 +1,16 @@ package problem1 +import "sync" + func incrementConcurrently(num int) int { + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() num++ }() + wg.Wait() + return num } diff --git a/exercise5/problem2/problem2.go b/exercise5/problem2/problem2.go index 16d38e1d..5133119a 100644 --- a/exercise5/problem2/problem2.go +++ b/exercise5/problem2/problem2.go @@ -1,5 +1,11 @@ package problem2 +import ( + "runtime" + "sync" + "sync/atomic" +) + // add - sequential code to add numbers, don't update it, just to illustrate concept func add(numbers []int) int64 { var sum int64 @@ -11,6 +17,31 @@ func add(numbers []int) int64 { func addConcurrently(numbers []int) int64 { var sum int64 + numCPUs := runtime.NumCPU() + chunkSize := len(numbers) / numCPUs + + var wg sync.WaitGroup + wg.Add(numCPUs) + + for i := 0; i < numCPUs; i++ { + start := i * chunkSize + end := start + chunkSize + if i == numCPUs-1 { + end = len(numbers) + } + + go func(nums []int) { + defer wg.Done() + + var partialSum int64 + for _, n := range nums { + partialSum += int64(n) + } + + atomic.AddInt64(&sum, partialSum) + }(numbers[start:end]) + } + wg.Wait() return sum } diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..5ae3b427 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -2,10 +2,14 @@ package problem3 func sum(a, b int) int { var c int - + ch := make(chan struct{}) go func(a, b int) { c = a + b + close(ch) }(a, b) - + <-ch return c } + + + diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index b5899ddf..6c05ac2e 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -4,6 +4,7 @@ func iter(ch chan<- int, nums []int) { for _, n := range nums { ch <- n } + close(ch) } func sum(nums []int) int { diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..8a8d2e3e 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,8 +1,20 @@ package problem5 -func producer() {} +func producer(words []string, ch chan<- string) { + for _, i := range words { + ch <- i + } + close(ch) +} + +func consumer(ch <-chan string) string { + var words string -func consumer() {} + for i := range ch { + words += i + " " + } + return words[:len(words)-1] +} func send( words []string, diff --git a/exercise5/problem6/problem6.go b/exercise5/problem6/problem6.go index e1beea87..0b40f652 100644 --- a/exercise5/problem6/problem6.go +++ b/exercise5/problem6/problem6.go @@ -2,8 +2,31 @@ 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 v := range in { + out <- v * 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 v := range in { + out <- v + 5 + } + }() + return out +} -func piper(in <-chan int, pipes []pipe) <-chan int {} +func piper(in <-chan int, pipes []pipe) <-chan int { + for _, pipeFunc := range pipes { + in = pipeFunc(in) + } + return in +} diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..f9e9b479 100644 --- a/exercise5/problem7/problem7.go +++ b/exercise5/problem7/problem7.go @@ -1,3 +1,28 @@ package problem7 -func multiplex(ch1 <-chan string, ch2 <-chan string) []string {} +func multiplex(ch1 <-chan string, ch2 <-chan string) []string { + words := []string{} + + for { + select { + case word, ok := <-ch1: + if ok { + words = append(words, word) + } else { + ch1 = nil + } + case word, ok := <-ch2: + if ok { + words = append(words, word) + } else { + ch2 = nil + } + } + + if ch1 == nil && ch2 == nil { + break + } + } + + return words +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..a87b72df 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -4,4 +4,15 @@ import ( "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + + select { + case message, ok := <-ch: + if ok { + return message + } + case <-time.After(ttl): + break + } + return "timeout" +} diff --git a/exercise6/problem1/problem1.go b/exercise6/problem1/problem1.go index ee453b24..74ebfe1b 100644 --- a/exercise6/problem1/problem1.go +++ b/exercise6/problem1/problem1.go @@ -1,9 +1,34 @@ package problem1 +import "sync" + type bankAccount struct { + sync.Mutex blnc int } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + return &bankAccount{blnc:blnc} +} + +func (b *bankAccount) deposit(cash int) { + b.Mutex.Lock() + defer b.Mutex.Unlock() + b.blnc += cash } + +func (b *bankAccount) withdraw(cash int) bool { + b.Mutex.Lock() + defer b.Mutex.Unlock() + if b.blnc < cash { + return false + } + b.blnc -= cash + return true +} + +func (b *bankAccount) getBalance() int { + b.Mutex.Lock() + defer b.Mutex.Unlock() + return b.blnc +} \ No newline at end of file diff --git a/exercise6/problem2/problem2.go b/exercise6/problem2/problem2.go index 97e02368..85fdf07a 100644 --- a/exercise6/problem2/problem2.go +++ b/exercise6/problem2/problem2.go @@ -1,20 +1,46 @@ package problem2 import ( + "sync" "time" ) var readDelay = 10 * time.Millisecond type bankAccount struct { + sync.Mutex blnc int } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + return &bankAccount{blnc:blnc} } func (b *bankAccount) balance() int { time.Sleep(readDelay) - return 0 + b.Mutex.Lock() + defer b.Mutex.Unlock() + return b.blnc +} + +func (b *bankAccount) deposit(cash int) { + b.Mutex.Lock() + defer b.Mutex.Unlock() + b.blnc += cash +} + +func (b *bankAccount) withdraw(cash int) bool { + b.Mutex.Lock() + defer b.Mutex.Unlock() + if b.blnc < cash { + return false + } + b.blnc -= cash + return true +} + +func (b *bankAccount) getBalance() int { + b.Mutex.Lock() + defer b.Mutex.Unlock() + return b.blnc } diff --git a/exercise6/problem3/problem3.go b/exercise6/problem3/problem3.go index b34b90bb..acb946e2 100644 --- a/exercise6/problem3/problem3.go +++ b/exercise6/problem3/problem3.go @@ -1,5 +1,7 @@ package problem3 +import "sync/atomic" + type counter struct { val int64 } @@ -9,3 +11,15 @@ func newCounter() *counter { val: 0, } } + +func (c *counter) inc() { + atomic.AddInt64(&c.val, 1) +} + +func (c *counter) dec() { + atomic.AddInt64(&c.val, -1) +} + +func (c *counter) value() int64 { + return atomic.LoadInt64(&c.val) +} diff --git a/exercise6/problem4/problem4.go b/exercise6/problem4/problem4.go index 793449c9..266be938 100644 --- a/exercise6/problem4/problem4.go +++ b/exercise6/problem4/problem4.go @@ -1,31 +1,40 @@ package problem4 import ( + "sync" "time" ) -func worker(id int, _ *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +func worker(id int, shoppingList *[]string, cond *sync.Cond, ch chan<- int) { + cond.L.Lock() + defer cond.L.Unlock() + for len(*shoppingList) == 0 { + cond.Wait() + } + ch <- id } - -func updateShopList(shoppingList *[]string) { +func updateShopList(shoppingList *[]string, cond *sync.Cond) { time.Sleep(10 * time.Millisecond) - + cond.L.Lock() + defer cond.L.Unlock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + + cond.Signal() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - + m := sync.Mutex{} + cond := sync.NewCond(&m) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, cond, notifier) time.Sleep(time.Millisecond) // order matters } - go updateShopList(shoppingList) + go updateShopList(shoppingList, cond) return notifier } diff --git a/exercise6/problem5/problem5.go b/exercise6/problem5/problem5.go index 8e4a1703..642bb657 100644 --- a/exercise6/problem5/problem5.go +++ b/exercise6/problem5/problem5.go @@ -1,31 +1,40 @@ package problem5 import ( + "sync" "time" ) -func worker(id int, shoppingList *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +func worker(id int, shoppingList *[]string, cond *sync.Cond, ch chan<- int) { + cond.L.Lock() + defer cond.L.Unlock() + for len(*shoppingList) == 0 { + cond.Wait() + } + ch <- id } - -func updateShopList(shoppingList *[]string) { +func updateShopList(shoppingList *[]string, cond *sync.Cond) { time.Sleep(10 * time.Millisecond) - + cond.L.Lock() + defer cond.L.Unlock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + + cond.Broadcast() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - + m := sync.Mutex{} + cond := sync.NewCond(&m) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, cond, notifier) time.Sleep(time.Millisecond) // order matters } - go updateShopList(shoppingList) + go updateShopList(shoppingList, cond) return notifier } diff --git a/exercise6/problem6/problem6.go b/exercise6/problem6/problem6.go index 0c1122b9..53d904e5 100644 --- a/exercise6/problem6/problem6.go +++ b/exercise6/problem6/problem6.go @@ -6,14 +6,14 @@ import ( func runTasks(init func()) { var wg sync.WaitGroup - + var once sync.Once for range 10 { wg.Add(1) go func() { defer wg.Done() //TODO: modify so that load function gets called only once. - init() + once.Do(init) }() } wg.Wait() diff --git a/exercise6/problem7/problem7.go b/exercise6/problem7/problem7.go index ef49497b..ced9b941 100644 --- a/exercise6/problem7/problem7.go +++ b/exercise6/problem7/problem7.go @@ -3,19 +3,33 @@ package problem7 import ( "fmt" "math/rand" + "sync" "time" ) func task() { start := time.Now() + var m sync.Mutex var t *time.Timer + + resetTimer := func() { + m.Lock() + defer m.Unlock() + if t != nil { + t.Reset(randomDuration()) + } + } + + m.Lock() t = time.AfterFunc( randomDuration(), func() { - fmt.Println(time.Now().Sub(start)) - t.Reset(randomDuration()) + fmt.Println(time.Since(start)) + resetTimer() }, ) + m.Unlock() + time.Sleep(5 * time.Second) } diff --git a/exercise6/problem8/problem8.go b/exercise6/problem8/problem8.go index 949eb2d2..1d5c5544 100644 --- a/exercise6/problem8/problem8.go +++ b/exercise6/problem8/problem8.go @@ -1,3 +1,24 @@ package problem8 -func multiplex(chs []<-chan string) []string {} +import "sync" + +func multiplex(chs []<-chan string) []string { + var result []string + var wg sync.WaitGroup + var mux sync.Mutex + + for _, ch := range chs { + wg.Add(1) + go func(ch <-chan string) { + defer wg.Done() + for msg := range ch { + mux.Lock() + result = append(result, msg) + mux.Unlock() + } + }(ch) + } + + wg.Wait() + return result +} diff --git a/exercise7/blogging-platform/.dockerignore b/exercise7/blogging-platform/.dockerignore new file mode 100644 index 00000000..9e03c484 --- /dev/null +++ b/exercise7/blogging-platform/.dockerignore @@ -0,0 +1,32 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/exercise7/blogging-platform/.gitignore b/exercise7/blogging-platform/.gitignore new file mode 100644 index 00000000..901a9fa8 --- /dev/null +++ b/exercise7/blogging-platform/.gitignore @@ -0,0 +1,28 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# postgres local volume +db-data diff --git a/exercise7/blogging-platform/README.md b/exercise7/blogging-platform/README.md index e6ef7017..e0c9a382 100644 --- a/exercise7/blogging-platform/README.md +++ b/exercise7/blogging-platform/README.md @@ -1,3 +1,6 @@ # Blogging Platform -Please check https://roadmap.sh/projects/blogging-platform-api. +Π”ΠΎΠ±Ρ€ΠΎ ΠΏΠΎΠΆΠ°Π»ΠΎΠ²Π°Ρ‚ΡŒ Π² ΠΌΠΎΠΉ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚! + +ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ доступСн ΠΏΠΎ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ ссылкС: [ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ Π½Π° GitHub](https://6bb9-188-0-144-205.ngrok-free.app/) + diff --git a/exercise7/blogging-platform/compose.yaml b/exercise7/blogging-platform/compose.yaml new file mode 100644 index 00000000..efd65f79 --- /dev/null +++ b/exercise7/blogging-platform/compose.yaml @@ -0,0 +1,53 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "server". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + server: + build: + context: . + target: final + environment: + - API_PORT=80 + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=$DB_NAME + - DB_USER=$DB_USER + - DB_PASSWORD=$DB_PASSWORD + ports: + - ${API_PORT}:80 + depends_on: + db: + condition: service_healthy + + db: + image: postgres + restart: always + volumes: + - ./db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=$DB_NAME + - POSTGRES_USER=$DB_USER + - POSTGRES_PASSWORD=$DB_PASSWORD + ports: + - ${DB_PORT}:5432 + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 +volumes: + db-data: + +# The commented out section below is an example of how to define a PostgreSQL +# database that your application can use. `depends_on` tells Docker Compose to +# start the database before your application. The `db-data` volume persists the +# database data between container restarts. The `db-password` secret is used +# to set the database password. You must create `db/password.txt` and add +# a password of your choosing to it before running `docker compose up`. + diff --git a/exercise7/blogging-platform/dockerfile b/exercise7/blogging-platform/dockerfile new file mode 100644 index 00000000..ed4e193c --- /dev/null +++ b/exercise7/blogging-platform/dockerfile @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +################################################################################ +# Create a stage for building the application. +ARG GO_VERSION=1.23 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage bind mounts to go.sum and go.mod to avoid having to copy them into +# the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.sum,target=go.sum \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x + +# This is the architecture you're building for, which is passed in by the builder. +# Placing it here allows the previous steps to be cached across architectures. +ARG TARGETARCH + +# Build the application. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage a bind mount to the current directory to avoid having to copy the +# source code into the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,target=. \ + CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . + +################################################################################ +# Create a new stage for running the application that contains the minimal +# runtime dependencies for the application. This often uses a different base +# image from the build stage where the necessary files are copied from the build +# stage. +# +# The example below uses the alpine image as the foundation for running the app. +# By specifying the "latest" tag, it will also use whatever happens to be the +# most recent version of that image when you build your Dockerfile. If +# reproducability is important, consider using a versioned tag +# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff). +FROM alpine:latest AS final + +# Install any runtime dependencies that are needed to run your application. +# Leverage a cache mount to /var/cache/apk/ to speed up subsequent builds. +RUN --mount=type=cache,target=/var/cache/apk \ + apk --update add \ + ca-certificates \ + tzdata \ + && \ + update-ca-certificates + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser +USER appuser + +# Copy the executable from the "build" stage. +COPY --from=build /bin/server /bin/ + +# Expose the port that the application listens on. +EXPOSE 80 + +# What the container should run when it is started. +ENTRYPOINT [ "/bin/server" ] diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..095d0d8b 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -1,5 +1,8 @@ -module github.com/talgat-ruby/exercises-go/exercise7/blogging-platform +module github.com/UAssylbek/blogging-platform go 1.23.3 -require github.com/lib/pq v1.10.9 +require ( + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 +) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..ecb9035f 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,2 +1,4 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/exercise7/blogging-platform/internal/api/handler/main.go b/exercise7/blogging-platform/internal/api/handler/main.go new file mode 100644 index 00000000..1d3fa9ba --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/main.go @@ -0,0 +1,18 @@ +package handler + +import ( + "log/slog" + + "github.com/UAssylbek/blogging-platform/internal/api/handler/posts" + "github.com/UAssylbek/blogging-platform/internal/db" +) + +type Handler struct { + *posts.Posts +} + +func New(logger *slog.Logger, db *db.DB) *Handler { + return &Handler{ + Posts: posts.New(logger, db), + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/create_post.go b/exercise7/blogging-platform/internal/api/handler/posts/create_post.go new file mode 100644 index 00000000..311265ff --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/create_post.go @@ -0,0 +1,82 @@ +package posts + +import ( + "net/http" + + "github.com/UAssylbek/blogging-platform/internal/db/post" + "github.com/UAssylbek/blogging-platform/pkg/httputils/request" + "github.com/UAssylbek/blogging-platform/pkg/httputils/response" +) + +type CreatePostRequest struct { + Data *post.ModelPost `json:"data"` +} + +type CreatePostResponse struct { + Data *post.ModelPost `json:"data"` +} + +func (h *Posts) CreatePost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "CreatePost") + + // request parse + requestBody := &CreatePostRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // db request + dbResp, err := h.db.CreatePost(ctx, requestBody.Data) + + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + + if dbResp == nil { + log.ErrorContext( + ctx, + "row is empty", + ) + http.Error(w, "row is empty", http.StatusInternalServerError) + return + } + + // response + resp := CreatePostResponse{ + Data: dbResp, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success insert post", + "post id", resp.Data.ID, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/delete_post.go b/exercise7/blogging-platform/internal/api/handler/posts/delete_post.go new file mode 100644 index 00000000..373cec1f --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/delete_post.go @@ -0,0 +1,57 @@ +package posts + +import ( + "net/http" + "strconv" + + "github.com/UAssylbek/blogging-platform/pkg/httputils/response" +) + +func (h *Posts) DeletePost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "DeletePost") + + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + log.ErrorContext( + ctx, + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + + // db request + if err := h.db.DeletePost(ctx, int64(id)); err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := response.JSON( + w, + http.StatusNoContent, + nil, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success delete post", + "id", id, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/find_post.go b/exercise7/blogging-platform/internal/api/handler/posts/find_post.go new file mode 100644 index 00000000..9db677d5 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/find_post.go @@ -0,0 +1,67 @@ +package posts + +import ( + "net/http" + "strconv" + + "github.com/UAssylbek/blogging-platform/internal/db/post" + "github.com/UAssylbek/blogging-platform/pkg/httputils/response" +) + +type FindPostResponse struct { + Data *post.ModelPost `json:"data"` +} + +func (h *Posts) FindPost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "FindPost") + + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + log.ErrorContext( + ctx, + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + + dbResp, err := h.db.FindPost(ctx, int64(id)) + + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp := FindPostResponse{ + Data: dbResp, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success find post", + "post id", resp.Data.ID, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/find_posts.go b/exercise7/blogging-platform/internal/api/handler/posts/find_posts.go new file mode 100644 index 00000000..b319b989 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/find_posts.go @@ -0,0 +1,48 @@ +package posts + +import ( + "net/http" + + "github.com/UAssylbek/blogging-platform/internal/db/post" + "github.com/UAssylbek/blogging-platform/pkg/httputils/response" +) + +type FindPostsResponse struct { + Data []post.ModelPost `json:"data"` +} + +func (h *Posts) FindPosts(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + log := h.logger.With("method", "FindPosts") + + dbResp, err := h.db.FindPosts(ctx) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + resp := FindPostsResponse{ + Data: dbResp, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success find posts", + "number_of_posts", len(resp.Data), + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/main.go b/exercise7/blogging-platform/internal/api/handler/posts/main.go new file mode 100644 index 00000000..72908284 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/main.go @@ -0,0 +1,19 @@ +package posts + +import ( + "log/slog" + + "github.com/UAssylbek/blogging-platform/internal/db" +) + +type Posts struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Posts { + return &Posts{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/update_post.go b/exercise7/blogging-platform/internal/api/handler/posts/update_post.go new file mode 100644 index 00000000..bd01f048 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/update_post.go @@ -0,0 +1,76 @@ +package posts + +import ( + "net/http" + "strconv" + + "github.com/UAssylbek/blogging-platform/internal/db/post" + "github.com/UAssylbek/blogging-platform/pkg/httputils/request" + "github.com/UAssylbek/blogging-platform/pkg/httputils/response" +) + +type UpdatePostRequest struct { + Data *post.ModelPost `json:"data"` +} + +func (h *Posts) UpdatePost(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "UpdatePost") + + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + log.ErrorContext( + ctx, + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + + // request parse + requestBody := &UpdatePostRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // db request + if err := h.db.UpdatePost(ctx, int64(id), requestBody.Data); err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := response.JSON( + w, + http.StatusNoContent, + nil, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success update post", + "id", id, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/main.go b/exercise7/blogging-platform/internal/api/main.go new file mode 100644 index 00000000..b6cde0a7 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/main.go @@ -0,0 +1,55 @@ +package api + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "strconv" + + "github.com/UAssylbek/blogging-platform/internal/api/handler" + "github.com/UAssylbek/blogging-platform/internal/api/router" + "github.com/UAssylbek/blogging-platform/internal/db" +) + +type Api struct { + logger *slog.Logger + router *router.Router +} + +func New(logger *slog.Logger, db *db.DB) *Api { + h := handler.New(logger, db) + r := router.New(h) + + return &Api{ + logger: logger, + router: r, + } +} + +func (a *Api) Start(ctx context.Context) error { + mux := a.router.Start(ctx) + + port, err := strconv.Atoi(os.Getenv("API_PORT")) + if err != nil { + return err + } + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + BaseContext: func(_ net.Listener) context.Context { + return ctx + }, + } + + fmt.Printf("Starting server on :%d\n", port) + if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { + return err + } + + return nil +} diff --git a/exercise7/blogging-platform/internal/api/router/main.go b/exercise7/blogging-platform/internal/api/router/main.go new file mode 100644 index 00000000..0aeb8494 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/main.go @@ -0,0 +1,28 @@ +package router + +import ( + "context" + "net/http" + + "github.com/UAssylbek/blogging-platform/internal/api/handler" +) + +type Router struct { + router *http.ServeMux + handler *handler.Handler +} + +func New(handler *handler.Handler) *Router { + mux := http.NewServeMux() + + return &Router{ + router: mux, + handler: handler, + } +} + +func (r *Router) Start(ctx context.Context) *http.ServeMux { + r.posts(ctx) + + return r.router +} diff --git a/exercise7/blogging-platform/internal/api/router/posts.go b/exercise7/blogging-platform/internal/api/router/posts.go new file mode 100644 index 00000000..664ba65f --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/posts.go @@ -0,0 +1,13 @@ +package router + +import ( + "context" +) + +func (r *Router) posts(ctx context.Context) { + r.router.HandleFunc("GET /posts", r.handler.FindPosts) + r.router.HandleFunc("GET /posts/{id}", r.handler.FindPost) + r.router.HandleFunc("POST /posts", r.handler.CreatePost) + r.router.HandleFunc("PUT /posts/{id}", r.handler.UpdatePost) + r.router.HandleFunc("DELETE /posts/{id}", r.handler.DeletePost) +} diff --git a/exercise7/blogging-platform/internal/db/init.go b/exercise7/blogging-platform/internal/db/init.go new file mode 100644 index 00000000..825b8ad2 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/init.go @@ -0,0 +1,29 @@ +package db + +import ( + "context" +) + +func (db *DB) Init(ctx context.Context) error { + log := db.logger.With("method", "Init") + + stmt := ` +CREATE TABLE IF NOT EXISTS post ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + content TEXT NOT NULL, + category TEXT NOT NULL, + tags TEXT[] NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) +` + + if _, err := db.pg.Exec(stmt); err != nil { + log.ErrorContext(ctx, "fail create table post", "error", err) + return err + } + + log.InfoContext(ctx, "success create table post") + return nil +} diff --git a/exercise7/blogging-platform/internal/db/main.go b/exercise7/blogging-platform/internal/db/main.go new file mode 100644 index 00000000..ba8891a3 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/main.go @@ -0,0 +1,53 @@ +package db + +import ( + "database/sql" + "fmt" + "log/slog" + "os" + "strconv" + + _ "github.com/lib/pq" + "github.com/UAssylbek/blogging-platform/internal/db/post" +) + +type DB struct { + logger *slog.Logger + pg *sql.DB + *post.Post +} + +func New(logger *slog.Logger) (*DB, error) { + pgsql, err := newPgSQL() + if err != nil { + return nil, err + } + + return &DB{ + logger: logger, + pg: pgsql, + Post: post.New(pgsql, logger), + }, nil +} + +func newPgSQL() (*sql.DB, error) { + host := os.Getenv("DB_HOST") + port, err := strconv.Atoi(os.Getenv("DB_PORT")) + if err != nil { + return nil, err + } + user := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + dbname := os.Getenv("DB_NAME") + + psqlInfo := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname, + ) + db, err := sql.Open("postgres", psqlInfo) + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/create_post.go b/exercise7/blogging-platform/internal/db/post/create_post.go new file mode 100644 index 00000000..55120ca3 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/create_post.go @@ -0,0 +1,48 @@ +package post + +import ( + "context" + "database/sql" + "errors" + + "github.com/lib/pq" +) + +func (m *Post) CreatePost(ctx context.Context, insertData *ModelPost) (*ModelPost, error) { + log := m.logger.With("method", "CreatePost") + + stmt := ` +INSERT INTO post (title, content, category, tags) +VALUES ($1, $2, $3, $4) +RETURNING id, title, content, category, tags, created_at, updated_at +` + + row := m.db.QueryRowContext(ctx, stmt, insertData.Title, insertData.Content, insertData.Category, pq.Array(insertData.Tags)) + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to insert to table post", "error", err) + return nil, err + } + + post := ModelPost{} + + if err := row.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.Category, + &post.Tags, + &post.CreatedAt, + &post.UpdatedAt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.ErrorContext(ctx, "no values was found", "error", err) + return nil, nil + } + log.ErrorContext(ctx, "fail to scan post", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success insert to table post") + return &post, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/delete_post.go b/exercise7/blogging-platform/internal/db/post/delete_post.go new file mode 100644 index 00000000..3c4daac4 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/delete_post.go @@ -0,0 +1,35 @@ +package post + +import ( + "context" + "fmt" +) + +func (m *Post) DeletePost(ctx context.Context, id int64) error { + log := m.logger.With("method", "DeletePost", "id", id) + + stmt := ` +DELETE FROM post +WHERE id = $1 +` + + res, err := m.db.ExecContext(ctx, stmt, id) + if err != nil { + log.ErrorContext(ctx, "fail to delete from the table post", "error", err) + return err + } + + num, err := res.RowsAffected() + if err != nil { + log.ErrorContext(ctx, "fail to delete from the table post", "error", err) + return err + } + + if num == 0 { + log.WarnContext(ctx, "post with id was not found", "id", id) + return fmt.Errorf("post with id was not found") + } + + log.InfoContext(ctx, "success delete from the table post") + return nil +} diff --git a/exercise7/blogging-platform/internal/db/post/find_post.go b/exercise7/blogging-platform/internal/db/post/find_post.go new file mode 100644 index 00000000..6c69403d --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/find_post.go @@ -0,0 +1,40 @@ +package post + +import ( + "context" +) + +func (m *Post) FindPost(ctx context.Context, id int64) (*ModelPost, error) { + log := m.logger.With("method", "FindPost") + + stmt := ` +SELECT id, title, content, category, tags, created_at, updated_at +FROM post +WHERE id = $1 +` + + row := m.db.QueryRowContext(ctx, stmt, id) + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table post", "error", err) + return nil, err + } + + post := ModelPost{} + + if err := row.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.Category, + &post.Tags, + &post.CreatedAt, + &post.UpdatedAt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan post", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success query table post") + return &post, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/find_posts.go b/exercise7/blogging-platform/internal/db/post/find_posts.go new file mode 100644 index 00000000..99ab6237 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/find_posts.go @@ -0,0 +1,51 @@ +package post + +import ( + "context" +) + +func (m *Post) FindPosts(ctx context.Context) ([]ModelPost, error) { + log := m.logger.With("method", "FindPosts") + + posts := make([]ModelPost, 0) + + stmt := ` +SELECT id, title, content, category, tags, created_at, updated_at +FROM post +` + + rows, err := m.db.QueryContext(ctx, stmt) + if err != nil { + log.ErrorContext(ctx, "fail to query table post", "error", err) + return nil, err + } + + defer rows.Close() + + for rows.Next() { + post := ModelPost{} + + if err := rows.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.Category, + &post.Tags, + &post.CreatedAt, + &post.UpdatedAt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan post", "error", err) + return nil, err + } + + posts = append(posts, post) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success query table post") + return posts, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/main.go b/exercise7/blogging-platform/internal/db/post/main.go new file mode 100644 index 00000000..5995966b --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/main.go @@ -0,0 +1,18 @@ +package post + +import ( + "database/sql" + "log/slog" +) + +type Post struct { + logger *slog.Logger + db *sql.DB +} + +func New(db *sql.DB, logger *slog.Logger) *Post { + return &Post{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/db/post/model.go b/exercise7/blogging-platform/internal/db/post/model.go new file mode 100644 index 00000000..93250d15 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/model.go @@ -0,0 +1,17 @@ +package post + +import ( + "time" + + "github.com/lib/pq" +) + +type ModelPost struct { + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Category string `json:"category"` + Tags pq.StringArray `json:"tags"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/exercise7/blogging-platform/internal/db/post/update_post.go b/exercise7/blogging-platform/internal/db/post/update_post.go new file mode 100644 index 00000000..1a9168c5 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/update_post.go @@ -0,0 +1,38 @@ +package post + +import ( + "context" + "fmt" + + "github.com/lib/pq" +) + +func (m *Post) UpdatePost(ctx context.Context, id int64, insertData *ModelPost) error { + log := m.logger.With("method", "UpdatePost", "id", id) + + stmt := ` +UPDATE post +SET title = $2, content = $3, category = $4, tags = $5 +WHERE id = $1 +` + + res, err := m.db.ExecContext(ctx, stmt, id, insertData.Title, insertData.Content, insertData.Category, pq.Array(insertData.Tags)) + if err != nil { + log.ErrorContext(ctx, "fail to update the table post", "error", err) + return err + } + + num, err := res.RowsAffected() + if err != nil { + log.ErrorContext(ctx, "fail to update from the table post", "error", err) + return err + } + + if num == 0 { + log.WarnContext(ctx, "post with id was not found", "id", id) + return fmt.Errorf("post with id was not found") + } + + log.InfoContext(ctx, "success update the table post") + return nil +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 1ffa1477..8741bc2a 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -2,48 +2,32 @@ package main import ( "context" + "fmt" "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" + "github.com/joho/godotenv" + "github.com/UAssylbek/blogging-platform/internal/api" + "github.com/UAssylbek/blogging-platform/internal/db" ) func main() { - ctx, cancel := context.WithCancel(context.Background()) + ctx := context.Background() - // db - _, err := db.New() + _ = godotenv.Load() + + fmt.Println(os.Getenv("API_PORT")) + + d, err := db.New(slog.With("service", "db")) if err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "db", - "error", err, - ) + panic(err) + } + if err := d.Init(ctx); err != nil { panic(err) } - // api - a := api.New() + a := api.New(slog.With("service", "api"), d) 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/pkg/httputils/request/body.go b/exercise7/blogging-platform/pkg/httputils/request/body.go index 92d639f4..450e696a 100644 --- a/exercise7/blogging-platform/pkg/httputils/request/body.go +++ b/exercise7/blogging-platform/pkg/httputils/request/body.go @@ -8,7 +8,7 @@ import ( "net/http" "strings" - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/statusError" + "github.com/UAssylbek/blogging-platform/pkg/httputils/statusError" ) func JSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {