diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..57b77d40 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,12 @@ package main -func addUp() {} +func addUp(num int) int { + + sum := 0 + + for i := 1; i <= num; i++ { + sum += i + } + + return sum +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..0046044d 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,23 @@ package main -func sum() {} +import ( + "fmt" + "strconv" +) + +func sum(a string, b string) (string, error) { + + aNum, err := strconv.Atoi(a) + if err != nil { + + return "", fmt.Errorf("string: %s cannot be converted", a) + } + + bNum, err := strconv.Atoi(b) + if err != nil { + + return "", fmt.Errorf("string: %s cannot be converted", b) + } + + return strconv.Itoa(aNum + bNum), nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..cebcc789 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,20 @@ package main -func binary() {} +import "strconv" + +func binary(num int) string { + output := "" + + for { + digit := strconv.Itoa(num % 2) + num /= 2 + + output = digit + output + + if num == 0 { + break + } + } + + return output +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..3d893203 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,12 @@ package main -func numberSquares() {} +func numberSquares(num int) int { + + squares := 0 + + for i := 1; i <= num; i++ { + squares += (i * i) + } + + return squares +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..956229f0 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,17 @@ package main -func detectWord() {} +func detectWord(word string) string { + wanted := "" + + for _, char := range word { + // if unicode.IsLower(char) { + // wanted += string(char) + // } + code := int(char) + if code >= 97 && code <= 122 { + wanted += string(char) + } + } + + return wanted +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..eac47350 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,23 @@ package main -func potatoes() {} +import "strings" + +func potatoes(word string) int { + + // return strings.Count(word, "potato") + + substr := "potato" + count := 0 + + for { + index := strings.Index(word, substr) + if index == -1 { + break + } + + count += 1 + word = word[index+len(substr):] + } + + return count +} diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..0a4c1ed8 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,19 @@ package main -func emojify() {} +import "strings" + +func emojify(word string) string { + + matches := map[string]string{ + "smile": "🙂", + "grin": "😀", + "sad": "😥", + "mad": "😠", + } + + for k, v := range matches { + word = strings.Replace(word, k, v, -1) + } + + return word +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..b2b3b191 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,20 @@ package main -func highestDigit() {} +func highestDigit(num int) int { + + highest := 0 + + for { + digit := num % 10 + if digit > highest { + highest = digit + } + + num /= 10 + if num == 0 { + break + } + } + + return highest +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..d528df41 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,27 @@ package main -func countVowels() {} +import "unicode" + +func countVowels(word string) int { + + cnt := 0 + + vowels := map[string]bool{ + "a": true, + "e": true, + "i": true, + "o": true, + "u": true, + } + + for _, ch := range word { + ch = unicode.ToLower(ch) + + _, ok := vowels[string(ch)] + if ok { + cnt += 1 + } + } + + return cnt +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..d6870336 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,13 @@ package main -func bitwiseAND() {} +func bitwiseAND(a int, b int) int { + return a & b +} -func bitwiseOR() {} +func bitwiseOR(a int, b int) int { + return a | b +} -func bitwiseXOR() {} +func bitwiseXOR(a int, b int) int { + return a ^ b +} diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..5ee3c30c 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,12 @@ package problem1 -func isChangeEnough() { +func isChangeEnough(changes [4]int, total float32) bool { + current := float32(0) + + current += float32(changes[0]) * 0.25 + current += float32(changes[1]) * 0.10 + current += float32(changes[2]) * 0.05 + current += float32(changes[3]) * 0.01 + + return current >= total } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..dd51fe6e 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,15 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(string) func(int)) { + + brands := make(map[string]int) + + makeBrand := func(name string) func(int) { + brands[name] = 0 + return func(increment int) { + brands[name] += increment + } + } + + return brands, makeBrand +} diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..6c2d1145 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,15 @@ package problem11 -func removeDups() {} +func removeDups[T comparable](list []T) []T { + seen := map[T]struct{}{} + output := make([]T, 0, len(list)) + for _, v := range list { + if _, ok := seen[v]; ok { + continue + } + output = append(output, v) + seen[v] = struct{}{} + } + + return output +} diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..805a4dc3 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,23 @@ package problem11 -func keysAndValues() {} +import "sort" + +func keysAndValues[K string | int, V comparable](mp map[K]V) ([]K, []V) { + + keys := make([]K, 0, len(mp)) + values := make([]V, 0, len(mp)) + + for k := range mp { + keys = append(keys, k) + } + + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + values = append(values, mp[k]) + } + + return keys, values +} diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..ec656b06 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,23 @@ package problem2 -func capitalize() { +import ( + "unicode" +) + +func capitalize(names []string) []string { + for i, v := range names { + // names[i] = strings.Title(strings.ToLower(v)) + + capitalized := []rune(v) + for pos, char := range capitalized { + if pos == 0 { + capitalized[pos] = unicode.ToUpper(char) + continue + } + capitalized[pos] = unicode.ToLower(char) + } + names[i] = string(capitalized) + } + + return names } diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..6561f710 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,13 @@ package problem4 -func mapping() { +import "strings" + +func mapping(letters []string) map[string]string { + mp := make(map[string]string) + + for _, l := range letters { + mp[strings.ToLower(l)] = strings.ToUpper(l) + } + + return mp } diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..9f2211e2 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,38 @@ package problem5 -func products() { +import ( + "sort" +) + +func products(products map[string]int, price int) []string { + + pricesDict := make(map[int][]string) + prices := make([]int, 0, len(products)) + + for k, v := range products { + if v < price { + delete(products, k) + continue + } + if _, ok := pricesDict[v]; !ok { + prices = append(prices, v) + } + pricesDict[v] = append(pricesDict[v], k) + } + sort.Slice(prices, func(i, j int) bool { + return prices[i] > prices[j] + }) + + out := make([]string, 0, len(products)) + + for _, price := range prices { + keys := pricesDict[price] + + if len(keys) > 1 { + sort.Strings(keys) + } + out = append(out, keys...) + } + + return out } 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..e40a8bb1 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -3,14 +3,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, len(list)) + load(indMap, list) return indMap } -func load(m *map[string]int, students *[]string) { - for i, name := range *students { - (*m)[name] = i +func load(m map[string]int, students []string) { + for i, name := range students { + m[name] = i } } diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..91afe287 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,11 @@ package problem9 -func factory() {} +func factory(factor int) func(nums ...int) []int { + return func(nums ...int) []int { + output := make([]int, len(nums)) + for i, num := range nums { + output[i] = factor * num + } + return output + } +} diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..5ef95b55 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,35 @@ package problem1 -type Queue struct{} +import "errors" + +type Queue struct { + values []any +} + +func (q *Queue) Enqueue(value any) { + q.values = append(q.values, value) +} + +func (q *Queue) Dequeue() (any, error) { + if q.IsEmpty() { + return nil, errors.New("Queue is empty") + } + valueToRemove := q.values[0] + q.values = q.values[1:] + return valueToRemove, nil +} + +func (q *Queue) Peek() (any, error) { + if q.IsEmpty() { + return nil, errors.New("Queue is empty") + } + return q.values[0], nil +} + +func (q *Queue) Size() int { + return len(q.values) +} + +func (q *Queue) IsEmpty() bool { + return q.Size() == 0 +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..20f8b440 100644 --- a/exercise3/problem2/problem2.go +++ b/exercise3/problem2/problem2.go @@ -1,3 +1,35 @@ package problem2 -type Stack struct{} +import "errors" + +type Stack struct { + values []any +} + +func (s *Stack) Push(value any) { + s.values = append(s.values, value) +} + +func (s *Stack) Pop() (any, error) { + if s.IsEmpty() { + return nil, errors.New("Stack is empty") + } + valueToRemove := s.values[s.Size()-1] + s.values = s.values[:s.Size()-1] + return valueToRemove, nil +} + +func (s *Stack) Peek() (any, error) { + if s.IsEmpty() { + return nil, errors.New("Stack is empty") + } + return s.values[s.Size()-1], nil +} + +func (s *Stack) Size() int { + return len(s.values) +} + +func (s *Stack) IsEmpty() bool { + return s.Size() == 0 +} diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..411ab0bb 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,105 @@ package problem3 -type Set struct{} +import "fmt" + +type Set struct { + values map[any]bool +} + +func NewSet() *Set { + return &Set{ + values: make(map[any]bool), + } +} + +func (s *Set) Add(key any) { + s.values[key] = true +} +func (s *Set) Remove(key any) { + delete(s.values, key) +} + +func (s *Set) IsEmpty() bool { + return s.Size() == 0 +} + +func (s *Set) Size() int { + return len(s.values) +} + +func (s *Set) List() []any { + keys := make([]any, 0, s.Size()) + for k := range s.values { + keys = append(keys, k) + } + return keys +} + +func (s *Set) Has(key any) bool { + _, ok := s.values[key] + return ok +} +func (s *Set) Copy() *Set { + copies := make(map[any]bool) + for k, v := range s.values { + copies[k] = v + } + return &Set{ + values: copies, + } +} + +func (s *Set) Difference(b *Set) *Set { + differences := make(map[any]bool) + for k := range s.values { + ok := b.Has(k) + if !ok { + differences[k] = true + } + } + return &Set{ + values: differences, + } +} + +func (a *Set) IsSubset(b *Set) bool { + for k := range a.values { + if !b.Has(k) { + fmt.Println(k, a.Has(k), a.List(), b.List()) + return false + } + } + return true +} + +func Union(sets ...*Set) *Set { + unions := make(map[any]bool) + for _, s := range sets { + for k := range s.values { + unions[k] = true + } + } + return &Set{ + values: unions, + } +} + +func Intersect(sets ...*Set) *Set { + elements := make(map[any]int) + intersections := make(map[any]bool) + for _, s := range sets { + for k := range s.values { + elements[k] += 1 + } + } + + for k := range elements { + if elements[k] == len(sets) { + intersections[k] = true + } + } + + return &Set{ + values: intersections, + } +} diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..0d7d9cc0 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,100 @@ package problem4 -type LinkedList struct{} +import "errors" + +type Element[T comparable] struct { + value T + next *Element[T] +} + +func (e *Element[T]) Equal(b *Element[T]) bool { + return e.value == b.value +} + +type LinkedList[T comparable] struct { + head *Element[T] +} + +func (l *LinkedList[T]) Add(value *Element[T]) { + if l.head == nil { + l.head = value + return + } + + head := l.head + for head.next != nil { + head = head.next + } + head.next = value +} + +func (l *LinkedList[T]) Insert(value *Element[T], index int) error { + if l.Size() <= index { + return errors.New("index is greater than size of linked list") + } + head := l.head + if index == 0 { + temp := head + value.next = temp + head = value + return nil + } + for range index - 1 { + head = head.next + } + + value.next = head.next + head.next = value + return nil +} + +func (l *LinkedList[T]) Delete(e *Element[T]) error { + if l.head.value == e.value { + l.head = l.head.next + return nil + } + head := l.head + for head != nil { + if head.next.value == e.value { + head.next = head.next.next + return nil + } + head = head.next + } + + return errors.New("don't find an element") + +} + +func (l *LinkedList[T]) Find(value T) (*Element[T], error) { + head := l.head + for head != nil { + if head.value == value { + return head, nil + } + head = head.next + } + return nil, errors.New("don't find such a value") +} +func (l *LinkedList[T]) List() []T { + values := make([]T, 0, l.Size()) + temp := l.head + for temp != nil { + values = append(values, temp.value) + temp = temp.next + } + return values +} + +func (l *LinkedList[T]) Size() int { + cnt := 0 + temp := l.head + for temp != nil { + temp = temp.next + cnt += 1 + } + return cnt +} +func (l *LinkedList[T]) IsEmpty() bool { + return l.Size() == 0 +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..570e3b3f 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,17 @@ package problem5 -type Person struct{} +import "fmt" + +type Person struct { + name string + age int +} + +func (p *Person) compareAge(p2 *Person) string { + if p.age > p2.age { + return fmt.Sprintf("%s is younger than me.", p2.name) + } else if p.age < p2.age { + return fmt.Sprintf("%s is older than me.", p2.name) + } + return fmt.Sprintf("%s is the same age as me.", p2.name) +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..14693ffa 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,31 @@ package problem6 -type Animal struct{} +type WithLegs interface { + getLegsCount() int +} -type Insect struct{} +type Animal struct { + name string + legsNum int +} -func sumOfAllLegsNum() {} +func (a *Animal) getLegsCount() int { + return a.legsNum +} + +type Insect struct { + name string + legsNum int +} + +func (i *Insect) getLegsCount() int { + return i.legsNum +} + +func sumOfAllLegsNum(withLegs ...WithLegs) int { + sum := 0 + for _, v := range withLegs { + sum += v.getLegsCount() + } + return sum +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..f219dbd0 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,57 @@ package problem7 +import "fmt" + +type Withdrawable interface { + withdraw(money int) +} + +type PackageSendable interface { + sendPackage(pkg string) +} + type BankAccount struct { + name string + balance int +} + +func (b *BankAccount) withdraw(money int) { + b.balance -= money } type FedexAccount struct { + name string + packages []string +} + +func (f *FedexAccount) sendPackage(receiver string) { + msg := fmt.Sprintf("%s send package to %s", f.name, receiver) + f.packages = append(f.packages, msg) } type KazPostAccount struct { + name string + balance int + packages []string +} + +func (k *KazPostAccount) withdraw(money int) { + k.balance -= money +} + +func (k *KazPostAccount) sendPackage(receiver string) { + msg := fmt.Sprintf("%s send package to %s", k.name, receiver) + k.packages = append(k.packages, msg) +} + +func withdrawMoney(money int, accounts ...Withdrawable) { + for _, account := range accounts { + account.withdraw(money) + } +} + +func sendPackagesTo(receiver string, accounts ...PackageSendable) { + for _, account := range accounts { + account.sendPackage(receiver) + } } diff --git a/exercise4/bot/internal/api/handler/main.go b/exercise4/bot/internal/api/handler/main.go new file mode 100644 index 00000000..bd7a9fb3 --- /dev/null +++ b/exercise4/bot/internal/api/handler/main.go @@ -0,0 +1,7 @@ +package handler + +type Handler struct{} + +func New() *Handler { + return &Handler{} +} diff --git a/exercise4/bot/internal/api/handler/move.go b/exercise4/bot/internal/api/handler/move.go new file mode 100644 index 00000000..b7f22837 --- /dev/null +++ b/exercise4/bot/internal/api/handler/move.go @@ -0,0 +1,88 @@ +package handler + +import ( + "encoding/json" + "fmt" + "math/rand" + "net/http" + "time" +) + +type Token string + +const ( + TokenEmpty Token = " " + TokenX Token = "x" + TokenO Token = "o" +) + +const ( + Cols = 3 + Rows = 3 +) + +type Board [Cols * Rows]Token + +type RequestMove struct { + Board Board `json:"board"` + Token Token `json:"token"` +} + +type ResponseMove struct { + Index int `json:"index"` +} + +func (h *Handler) Move(w http.ResponseWriter, r *http.Request) { + var reqBody RequestMove + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil { + http.Error(w, "Invalid input", http.StatusInternalServerError) + return + } + + fmt.Printf("Received move request for token %s on board:\n%s\n", reqBody.Token, reqBody.Board.String()) + + rand.Seed(time.Now().UnixNano()) + availableMoves := reqBody.Board.AvailableMoves() // Получаем все доступные ходы + if len(availableMoves) == 0 { + http.Error(w, "No available moves", http.StatusBadRequest) + return + } + + randomMove := availableMoves[rand.Intn(len(availableMoves))] // Выбираем случайный ход + + resp := ResponseMove{Index: randomMove} + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(resp) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } + +} + +func (b *Board) AvailableMoves() []int { + var moves []int + + for i := 0; i < len(b); i++ { + if b[i] == TokenEmpty { + moves = append(moves, i) + } + } + + return moves +} + +func (b *Board) String() string { + var result string + for row := 0; row < Rows; row++ { + result += "| " + for col := 0; col < Cols; col++ { + index := row*Cols + col + result += string(b[index]) + " | " + } + result += "\n" + } + return result +} diff --git a/exercise4/bot/internal/api/handler/ping.go b/exercise4/bot/internal/api/handler/ping.go new file mode 100644 index 00000000..1ccbc045 --- /dev/null +++ b/exercise4/bot/internal/api/handler/ping.go @@ -0,0 +1,11 @@ +package handler + +import ( + "fmt" + "net/http" +) + +func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "ping pong") +} diff --git a/exercise4/bot/internal/api/router/main.go b/exercise4/bot/internal/api/router/main.go new file mode 100644 index 00000000..61adbb23 --- /dev/null +++ b/exercise4/bot/internal/api/router/main.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/player/main.go b/exercise4/bot/internal/player/main.go new file mode 100644 index 00000000..b5879aa4 --- /dev/null +++ b/exercise4/bot/internal/player/main.go @@ -0,0 +1,59 @@ +package player + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "time" +) + +type RequestJoin struct { + Name string `json:"name"` + URL string `json:"url"` +} + +var ( + host = "http://127.0.0.1" + name = os.Getenv("NAME") + port = os.Getenv("PORT") + URL = fmt.Sprintf("%s:%s", host, port) +) + +func JoinToGame(ctx context.Context) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + join := RequestJoin{ + Name: name, + URL: URL, + } + jsonData, err := json.Marshal(join) + if err != nil { + return fmt.Errorf("failed to marshal move request: %w", err) + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + fmt.Sprintf("%s:4444/join", host), + bytes.NewBuffer(jsonData), + ) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to make 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)) + } + + return nil +} diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..42984b4d 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -5,6 +5,8 @@ import ( "os" "os/signal" "syscall" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/player" ) func main() { @@ -13,7 +15,7 @@ func main() { ready := startServer() <-ready - // TODO after server start + player.JoinToGame(ctx) stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go index e6760ec5..079ee576 100644 --- a/exercise4/bot/server.go +++ b/exercise4/bot/server.go @@ -8,6 +8,8 @@ import ( "os" "sync" "time" + + "github.com/talgat-ruby/exercises-go/exercise4/bot/internal/api/router" ) type readyListener struct { @@ -21,6 +23,11 @@ func (l *readyListener) Accept() (net.Conn, error) { return l.Listener.Accept() } +func pingHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "pong") +} + func startServer() <-chan struct{} { ready := make(chan struct{}) @@ -29,8 +36,15 @@ func startServer() <-chan struct{} { panic(err) } + print(listener) + + // os.Getenv("NAME") + + r := router.New() + list := &readyListener{Listener: listener, ready: ready} srv := &http.Server{ + Handler: r, IdleTimeout: 2 * time.Minute, } diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..7ea33b13 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/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..12c42aa6 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -1,11 +1,15 @@ package problem3 func sum(a, b int) int { + ch := make(chan struct{}) var c int go func(a, b int) { c = a + b + ch <- struct{}{} }(a, b) + <-ch + return c } diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index b5899ddf..96900161 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -4,16 +4,27 @@ func iter(ch chan<- int, nums []int) { for _, n := range nums { ch <- n } + close(ch) } func sum(nums []int) int { - ch := make(chan int) + ch := make(chan int, len(nums)) + done := make(chan bool) go iter(ch, nums) var sum int - for n := range ch { - sum += n - } + go func() { + for { + n, more := <-ch + if more { + sum += n + } else { + done <- true + break + } + } + }() + <-done return sum } diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..ef712a06 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,8 +1,27 @@ package problem5 -func producer() {} +func producer(words []string, ch chan<- string) { + for i, w := range words { + if i != len(words)-1 { + ch <- w + " " + continue + } + ch <- w + } + close(ch) +} -func consumer() {} +func consumer(ch <-chan string) string { + msg := "" + for { + w, more := <-ch + if more { + msg += w + } else { + return msg + } + } +} func send( words []string, diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..fc1b53cb 100644 --- a/exercise5/problem7/problem7.go +++ b/exercise5/problem7/problem7.go @@ -1,3 +1,25 @@ package problem7 -func multiplex(ch1 <-chan string, ch2 <-chan string) []string {} +func multiplex(ch1 <-chan string, ch2 <-chan string) []string { + output := make([]string, 0) + + for { + v, more := <-ch1 + if more { + output = append(output, v) + } else { + break + } + } + + for { + v, more := <-ch2 + if more { + output = append(output, v) + } else { + break + } + } + + return output +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..cde2d620 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -4,4 +4,11 @@ import ( "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + select { + case msg := <-ch: + return msg + case <-time.After(ttl): + return "timeout" + } +} diff --git a/exercise7/blogging-platform/docker-compose.yml b/exercise7/blogging-platform/docker-compose.yml new file mode 100644 index 00000000..56b763a4 --- /dev/null +++ b/exercise7/blogging-platform/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.9' + +services: + postgres: + container_name: blogging_techorda_container + image: postgres:14.5 + ports: + - 5432:5432 + environment: + POSTGRES_DB: "bloggingdb" + POSTGRES_PASSWORD: "postgres" + volumes: + - ./pgdata:/var/lib/postgresql/data + +volumes: + pgdata: \ No newline at end of file diff --git a/exercise7/blogging-platform/errors.go b/exercise7/blogging-platform/errors.go new file mode 100644 index 00000000..0ccb29f8 --- /dev/null +++ b/exercise7/blogging-platform/errors.go @@ -0,0 +1,33 @@ +package main + +import "net/http" + +func (app *application) logError(r *http.Request, err error) { + app.logger.Println(err) +} + +func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) { + env := envelope{"error": message} + + err := app.writeJSON(w, status, env, nil) + if err != nil { + app.logError(r, err) + w.WriteHeader(http.StatusInternalServerError) + } +} + +func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) { + app.logError(r, err) + + message := "the server encountered a problem and could not process your request" + app.errorResponse(w, r, http.StatusInternalServerError, message) +} + +func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) { + message := "the requested resource could not be found" + app.errorResponse(w, r, http.StatusNotFound, message) +} + +func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { + app.errorResponse(w, r, http.StatusBadRequest, err.Error()) +} diff --git a/exercise7/blogging-platform/healthcheck.go b/exercise7/blogging-platform/healthcheck.go new file mode 100644 index 00000000..54c2997d --- /dev/null +++ b/exercise7/blogging-platform/healthcheck.go @@ -0,0 +1,18 @@ +package main + +import "net/http" + +func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) { + data := envelope{ + "status": "available", + "sytem_info": map[string]string{ + "environment": app.config.env, + "version": version, + }, + } + + err := app.writeJSON(w, http.StatusOK, data, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/exercise7/blogging-platform/helpers.go b/exercise7/blogging-platform/helpers.go new file mode 100644 index 00000000..f5d5bcf9 --- /dev/null +++ b/exercise7/blogging-platform/helpers.go @@ -0,0 +1,91 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/statusError" + "io" + "net/http" + "strings" +) + +type envelope map[string]interface{} + +func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error { + js, err := json.MarshalIndent(data, "", "\t") + if err != nil { + return err + } + + js = append(js, '\n') + + for key, value := range headers { + w.Header()[key] = value + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(js) + + return nil +} + +func (app *application) readJSON(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) + } + } + + maxBytes := 1_048_576 + r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes)) + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(dst) + if err != nil { + var syntaxError *json.SyntaxError + var unmarshalTypeError *json.UnmarshalTypeError + var invalidUnmarshalError *json.InvalidUnmarshalError + + switch { + case errors.As(err, &syntaxError): + return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset) + case errors.Is(err, io.ErrUnexpectedEOF): + return errors.New("body contains badly-formed JSON") + + case errors.As(err, &unmarshalTypeError): + if unmarshalTypeError.Field != "" { + return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field) + } + return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset) + + case errors.Is(err, io.EOF): + return errors.New("body must not be empty") + + case strings.HasPrefix(err.Error(), "json: unknown field "): + fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") + return fmt.Errorf("body contains unknown key %s", fieldName) + + case err.Error() == "http: request body too large": + return fmt.Errorf("body must not be larger than %d bytes", maxBytes) + + case errors.As(err, &invalidUnmarshalError): + panic(err) + + default: + return err + } + } + + err = dec.Decode(&struct{}{}) + if err != io.EOF { + return errors.New("body must only contain a single JSON value") + } + return nil +} diff --git a/exercise7/blogging-platform/internal/data/models.go b/exercise7/blogging-platform/internal/data/models.go new file mode 100644 index 00000000..efd02402 --- /dev/null +++ b/exercise7/blogging-platform/internal/data/models.go @@ -0,0 +1,20 @@ +package data + +import ( + "database/sql" + "errors" +) + +var ( + ErrRecordNotFound = errors.New("record not found") +) + +type Models struct { + Posts PostModel +} + +func NewModels(db *sql.DB) Models { + return Models{ + Posts: PostModel{DB: db}, + } +} diff --git a/exercise7/blogging-platform/internal/data/posts.go b/exercise7/blogging-platform/internal/data/posts.go new file mode 100644 index 00000000..87d0a6d3 --- /dev/null +++ b/exercise7/blogging-platform/internal/data/posts.go @@ -0,0 +1,163 @@ +package data + +import ( + "database/sql" + "errors" + "github.com/lib/pq" + "time" +) + +type Post struct { + ID int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Category string `json:"category"` + Tags []string `json:"tags"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PostModel struct { + DB *sql.DB +} + +func (p PostModel) GetAll() ([]*Post, error) { + posts := make([]*Post, 0) + + query := ` + SELECT id, title, content, category, tags, created_at, updated_at + FROM posts` + + rows, err := p.DB.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var post Post + + if err := rows.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.Category, + pq.Array(&post.Tags), + &post.CreatedAt, + &post.UpdatedAt, + ); err != nil { + return nil, err + } + + posts = append(posts, &post) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return posts, nil +} + +func (p PostModel) Insert(post *Post) error { + query := ` + INSERT INTO posts (title, content, category, tags) + VALUES ($1, $2, $3, $4) + RETURNING id, title, content, category, tags, created_at, updated_at` + + args := []interface{}{post.Title, post.Content, post.Category, pq.Array(post.Tags)} + + row := p.DB.QueryRow(query, args...) + if err := row.Err(); err != nil { + return err + } + if err := row.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.Category, + pq.Array(&post.Tags), + &post.CreatedAt, + &post.UpdatedAt, + ); err != nil { + return err + } + return nil +} + +func (p PostModel) Get(id int64) (*Post, error) { + + query := ` + SELECT id, title, content, category, tags, created_at, updated_at + FROM posts + WHERE id = $1` + + row := p.DB.QueryRow(query, id) + if err := row.Err(); err != nil { + + return nil, err + } + var post Post + + if err := row.Scan( + &post.ID, + &post.Title, + &post.Content, + &post.Category, + pq.Array(&post.Tags), + &post.CreatedAt, + &post.UpdatedAt, + ); err != nil { + switch { + case errors.Is(err, sql.ErrNoRows): + return nil, ErrRecordNotFound + default: + return nil, err + } + } + + return &post, nil +} + +func (p PostModel) Update(post *Post) error { + query := ` + UPDATE posts + SET title = $1, content = $2, category = $3, tags = $4, updated_at = now() + WHERE id = $5 + RETURNING updated_at` + + args := []interface{}{ + post.Title, + post.Content, + post.Category, + pq.Array(post.Tags), + post.ID, + } + + err := p.DB.QueryRow(query, args...).Scan(&post.UpdatedAt) + if err != nil { + return err + } + + return nil +} + +func (p PostModel) Delete(id int64) error { + query := `DELETE FROM posts WHERE id = $1` + + result, err := p.DB.Exec(query, id) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return ErrRecordNotFound + } + + return nil +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 1ffa1477..e4355d7b 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -2,48 +2,138 @@ package main import ( "context" - "log/slog" + "database/sql" + "flag" + "fmt" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/data" + "log" + "net/http" "os" - "os/signal" + "time" - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api" - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" + _ "github.com/lib/pq" ) +const version = "1.0.0" + +type config struct { + port int + env string + db struct { + dsn string + maxOpenConns int + maxIdleConns int + maxIdleTime string + } +} + +type application struct { + config config + logger *log.Logger + models data.Models +} + func main() { - ctx, cancel := context.WithCancel(context.Background()) + var cfg config + + flag.IntVar(&cfg.port, "port", 4000, "API server port") + flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") + flag.StringVar(&cfg.db.dsn, "db-dsn", "postgres://postgres:postgres@localhost/bloggingdb?sslmode=disable", "PostgreSQL DSN") + flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") + flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") + flag.StringVar(&cfg.db.maxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time") + + flag.Parse() + + logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) + + db, err := openDB(cfg) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + logger.Printf("database connection is established") + + app := &application{ + config: cfg, + logger: logger, + models: data.NewModels(db), + } - // db - _, err := db.New() + fmt.Println("app", app.routes()) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", cfg.port), + Handler: app.routes(), + IdleTimeout: time.Minute, + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + } + + logger.Printf("starting %s server on %s", cfg.env, srv.Addr) + err = srv.ListenAndServe() + logger.Fatal(err) + //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() + // }() +} + +func openDB(cfg config) (*sql.DB, error) { + db, err := sql.Open("postgres", cfg.db.dsn) if err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "db", - "error", err, - ) - panic(err) + return nil, err } - // api - a := api.New() - if err := a.Start(ctx); err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "api", - "error", err, - ) - panic(err) + db.SetMaxOpenConns(cfg.db.maxOpenConns) + db.SetMaxIdleConns(cfg.db.maxIdleConns) + + duration, err := time.ParseDuration(cfg.db.maxIdleTime) + if err != nil { + return nil, 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 + db.SetConnMaxIdleTime(duration) - sig := <-shutdown - slog.WarnContext(ctx, "signal received - shutting down...", "signal", sig) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err = db.PingContext(ctx) + if err != nil { + return nil, err + } - cancel() - }() + return db, nil } diff --git a/exercise7/blogging-platform/migrations/000001_create_posts_table.down.sql b/exercise7/blogging-platform/migrations/000001_create_posts_table.down.sql new file mode 100644 index 00000000..52ac968d --- /dev/null +++ b/exercise7/blogging-platform/migrations/000001_create_posts_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS posts; \ No newline at end of file diff --git a/exercise7/blogging-platform/migrations/000001_create_posts_table.up.sql b/exercise7/blogging-platform/migrations/000001_create_posts_table.up.sql new file mode 100644 index 00000000..52ba33db --- /dev/null +++ b/exercise7/blogging-platform/migrations/000001_create_posts_table.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS posts ( + id bigserial PRIMARY KEY, + created_at timestamp(0) with time zone NOT NULL DEFAULT now(), + updated_at timestamp(0) with time zone NOT NULL DEFAULT now(), + title text NOT NULL, + content text NOT NULL, + category text NOT NULL, + tags text[] NOT NULL +) \ No newline at end of file diff --git a/exercise7/blogging-platform/posts.go b/exercise7/blogging-platform/posts.go new file mode 100644 index 00000000..05d769d6 --- /dev/null +++ b/exercise7/blogging-platform/posts.go @@ -0,0 +1,161 @@ +package main + +import ( + "errors" + "fmt" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/data" + "net/http" + "strconv" +) + +func (app *application) showPostsHandler(w http.ResponseWriter, r *http.Request) { + posts, err := app.models.Posts.GetAll() + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + err = app.writeJSON(w, http.StatusOK, envelope{"posts": posts}, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } +} + +func (app *application) createPostHandler(w http.ResponseWriter, r *http.Request) { + var input struct { + Title string `json:"title"` + Content string `json:"content"` + Category string `json:"category"` + Tags []string `json:"tags"` + } + err := app.readJSON(w, r, &input) + if err != nil { + app.badRequestResponse(w, r, err) + return + } + post := &data.Post{ + Title: input.Title, + Content: input.Content, + Category: input.Category, + Tags: input.Tags, + } + err = app.models.Posts.Insert(post) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + headers := make(http.Header) + headers.Set("Location", fmt.Sprintf("/posts/%d", post.ID)) + + err = app.writeJSON(w, http.StatusOK, envelope{"data": post}, headers) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} + +func (app *application) showPostHandler(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "failed to convert to int", http.StatusBadRequest) + return + } + + post, err := app.models.Posts.Get(int64(id)) + if err != nil { + switch { + case errors.Is(err, data.ErrRecordNotFound): + app.notFoundResponse(w, r) + default: + + app.serverErrorResponse(w, r, err) + } + return + } + + err = app.writeJSON(w, http.StatusOK, envelope{"data": post}, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + +} +func (app *application) updatePostHandler(w http.ResponseWriter, r *http.Request) { + + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "failed to convert to int", http.StatusBadRequest) + return + } + + post, err := app.models.Posts.Get(int64(id)) + if err != nil { + switch { + case errors.Is(err, data.ErrRecordNotFound): + app.notFoundResponse(w, r) + default: + app.serverErrorResponse(w, r, err) + } + return + } + + var input struct { + Title string `json:"title"` + Content string `json:"content"` + Category string `json:"category"` + Tags []string `json:"tags"` + } + + err = app.readJSON(w, r, &input) + if err != nil { + app.badRequestResponse(w, r, err) + return + } + + post.Title = input.Title + post.Content = input.Content + post.Category = input.Category + post.Tags = input.Tags + + err = app.models.Posts.Update(post) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + err = app.writeJSON(w, http.StatusOK, envelope{"data": post}, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } +} + +func (app *application) deletePostHandler(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "failed to convert to int", http.StatusBadRequest) + return + } + + err = app.models.Posts.Delete(int64(id)) + if err != nil { + switch { + case errors.Is(err, data.ErrRecordNotFound): + app.notFoundResponse(w, r) + default: + app.serverErrorResponse(w, r, err) + } + return + } + + err = app.writeJSON(w, http.StatusNoContent, envelope{"message": "post successfully deleted"}, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/exercise7/blogging-platform/routes.go b/exercise7/blogging-platform/routes.go new file mode 100644 index 00000000..65287e93 --- /dev/null +++ b/exercise7/blogging-platform/routes.go @@ -0,0 +1,18 @@ +package main + +import ( + "net/http" +) + +func (app *application) routes() *http.ServeMux { + router := http.NewServeMux() + + router.HandleFunc("GET /healthcheck", app.healthcheckHandler) + + router.HandleFunc("GET /posts", app.showPostsHandler) + router.HandleFunc("POST /posts", app.createPostHandler) + router.HandleFunc("GET /posts/{id}", app.showPostHandler) + router.HandleFunc("PUT /posts/{id}", app.updatePostHandler) + router.HandleFunc("DELETE /posts/{id}", app.deletePostHandler) + return router +}