diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..015e40d5 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,9 @@ package main -func addUp() {} +func addUp(n int) int { + summa := 0 + for i := 1; i <= n; i++ { + summa += i + } + return summa +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..6c6d4d81 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,26 @@ package main -func sum() {} +import ( + "fmt" + "strconv" +) + +func sum(a, b string) (string, error) { + // Convert the first string to an integer + intA, err := strconv.Atoi(a) + if err != nil { + return "", fmt.Errorf("string: %s cannot be converted", a) + } + + // Convert the second string to an integer + intB, err := strconv.Atoi(b) + if err != nil { + return "", fmt.Errorf("string: %s cannot be converted", b) + } + + // Sum the integers + result := intA + intB + + // Convert the result back to a string and return it + return strconv.Itoa(result), nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..1463cf9e 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,7 @@ package main -func binary() {} +import "strconv" + +func binary(n int) string { + return strconv.FormatInt(int64(n), 2) +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..d65bff50 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,9 @@ package main -func numberSquares() {} +func numberSquares(n int) int { + quantity := 0 + for i := 1; i <= n; i++ { + quantity += (n - i + 1) * (n - i + 1) + } + return quantity +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..2cc17644 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,17 @@ package main -func detectWord() {} +import ( + "unicode" +) + +func detectWord(input string) string { + var result string + + for _, char := range input { + if unicode.IsLower(char) { + result += string(char) + } + } + + return result +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..e508ee39 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,11 @@ package main -func potatoes() {} +import "strings" + +func potatoes(input string) int { + + searchPotatoes := "potato" + count := strings.Count(input, searchPotatoes) + + return count +} diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..dabddc28 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,25 @@ package main -func emojify() {} +import ( + "strings" +) + +func emojify(sentence string) string { + + emojiMap := map[string]string{ + "smile": "🙂", + "grin": "😀", + "sad": "😥", + "mad": "😠", + } + words := strings.Fields(sentence) + + for i, word := range words { + cleanedWord := strings.Trim(word, ".,!?") + if emoji, found := emojiMap[cleanedWord]; found { + words[i] = strings.Replace(word, cleanedWord, emoji, -1) + } + } + + return strings.Join(words, " ") +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..fa2a744e 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,25 @@ package main -func highestDigit() {} +import ( + "fmt" + "strconv" +) + +func highestDigit(num int) int { + + numStr := strconv.Itoa(num) + maxDigit := 0 + + for _, char := range numStr { + digit, err := strconv.Atoi(string(char)) + if err != nil { + fmt.Println("Error converting character to digit:", err) + return -1 + } + if digit > maxDigit { + maxDigit = digit + } + } + + return maxDigit +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..7f64399d 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,17 @@ package main -func countVowels() {} +import "strings" + +func countVowels(s string) int { + + vowels := "aeiouAEIOU" + count := 0 + + for _, char := range s { + if strings.ContainsRune(vowels, char) { + count++ + } + } + + return count +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..d6870336 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,13 @@ package main -func bitwiseAND() {} +func bitwiseAND(a int, b int) int { + return a & b +} -func bitwiseOR() {} +func bitwiseOR(a int, b int) int { + return a | b +} -func bitwiseXOR() {} +func bitwiseXOR(a int, b int) int { + return a ^ b +} diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..e6ea3060 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,9 @@ package problem1 -func isChangeEnough() { +func isChangeEnough(cash [4]int, amount float32) bool { + + allCash := cash[0]*25 + cash[1]*10 + cash[2]*5 + cash[3] + amountToCent := int(amount * 100) + + return allCash >= amountToCent } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..6d3be93e 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,19 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(string) func(int)) { + + brands := make(map[string]int) + + makeBrand := func(brandName string) func(int) { + + if _, exists := brands[brandName]; !exists { + brands[brandName] = 0 + } + + return func(amount int) { + brands[brandName] += amount + } + } + + return brands, makeBrand +} diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..3a5ab3e9 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,16 @@ package problem11 -func removeDups() {} +func removeDups[T comparable](items []T) []T { + + seen := make(map[T]struct{}) + result := []T{} + + for _, item := range items { + if _, exists := seen[item]; !exists { + seen[item] = struct{}{} + result = append(result, item) + } + } + + return result +} diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..b839f0fe 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,28 @@ package problem11 -func keysAndValues() {} +import ( + "fmt" + "sort" +) + +func keysAndValues[K comparable, V any](m map[K]V) ([]K, []V) { + + keys := make([]K, 0, len(m)) + values := make([]V, 0, len(m)) + + for k, v := range m { + keys = append(keys, k) + values = append(values, v) + } + + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprintf("%v", keys[i]) < fmt.Sprintf("%v", keys[j]) + }) + + sortedValues := make([]V, len(keys)) + for i, key := range keys { + sortedValues[i] = m[key] + } + + return keys, sortedValues +} diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..c12e272d 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,13 @@ package problem2 -func capitalize() { +import "strings" + +func capitalize(names []string) []string { + + capitalizedNames := []string{} + for _, name := range names{ + capitalizedNames = append(capitalizedNames, strings.Title(strings.ToLower(name))) + } + + return capitalizedNames } diff --git a/exercise2/problem3/problem3.go b/exercise2/problem3/problem3.go index f183fafb..9f6f8b73 100644 --- a/exercise2/problem3/problem3.go +++ b/exercise2/problem3/problem3.go @@ -9,5 +9,49 @@ const ( lr dir = "lr" ) -func diagonalize() { +func diagonalize(size int, direction dir) [][]int { + + array := make([][]int, size) + + for i := range array { + array[i] = make([]int, size) + } + + switch direction { + case ul: + for i := range array { + for j := range array[i] { + array[i][j] = i + j + } + } + return array + case ur: + for i := range array { + for j := range array[i] { + array[i][j] = (size - 1 - j) + i + } + } + return array + case ll: + for i := range array { + for j := range array[i] { + array[i][j] = (size - 1 - i) + j + } + } + return array + case lr: + for i := range array { + for j := range array[i] { + array[i][j] = (size - 1 - i) + (size - 1 - j) + } + } + return array + default: + for i := range array { + for j := range array[i] { + array[i][j] = 0 + } + } + return array + } } diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..f96ab9d2 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,12 @@ package problem4 -func mapping() { +import "strings" + +func mapping(lowerCase []string) map[string]string { + result := make(map[string]string) + + for _, value := range lowerCase { + result[value] = strings.ToUpper(value) + } + return result } diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..606c5ae7 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,22 @@ package problem5 -func products() { +import "sort" + +func products(catalog map[string]int, minimalPrice int) []string { + var result []string + + for product, price := range catalog { + if price >= minimalPrice { + result = append(result, product) + } + } + + sort.Slice(result, func(i, j int) bool { + if catalog[result[i]] == catalog[result[j]] { + return result[i] < result[j] + } + return catalog[result[i]] > catalog[result[j]] + }) + + return result } diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..5b6aac25 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,17 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(a []int, b []int, v int) bool { + sumSet := make(map[int]struct{}) + + for _, numA := range a { + sumSet[v-numA] = struct{}{} + } + + for _, numB := range b { + if _, exists := sumSet[numB]; exists { + return true + } + } + + return false } diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..68732252 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,5 @@ package problem7 -func swap() { +func swap(x, y *int) { + *x, *y = *y, *x } 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..006e7f64 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,14 @@ package problem9 -func factory() {} +func factory(multiplier int) func(...int) []int { + return func(nums ...int) []int { + + result := make([]int, len(nums)) + + for i, num := range nums { + result[i] = num * multiplier + } + + return result + } +} diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..80eb5c86 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 { + slc []interface{} +} + +func (q *Queue) Enqueue(value interface{}) { + q.slc = append(q.slc, value) +} + +func (q *Queue) Dequeue() (interface{}, error) { + if q.IsEmpty() { + return nil, errors.New("Пумпурум, пусто") + } + firstValue := q.slc[0] + q.slc = q.slc[1:] + return firstValue, nil +} + +func (q *Queue) Peek() (interface{}, error) { + if q.IsEmpty() { + return nil, errors.New("Пумпурум, пусто") + } + return q.slc[0], nil +} + +func (q *Queue) Size() int { + return len(q.slc) +} + +func (q *Queue) IsEmpty() bool { + return len(q.slc) == 0 +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..4c1469e3 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 { + slc []interface{} +} + +func (s *Stack) Push(value interface{}) { + s.slc = append(s.slc, value) +} + +func (s *Stack) Pop() (interface{}, error) { + if s.IsEmpty() { + return nil, errors.New("Пумпурум, пусто") + } + lastValue := s.slc[len(s.slc)-1] + s.slc = s.slc[:len(s.slc)-1] + return lastValue, nil +} + +func (s *Stack) Peek() (interface{}, error) { + if s.IsEmpty() { + return nil, errors.New("Пумпурум, пусто") + } + return s.slc[len(s.slc)-1], nil +} + +func (s *Stack) Size() int { + return len(s.slc) +} + +func (s *Stack) IsEmpty() bool { + return len(s.slc) == 0 +} diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..e110a17c 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,124 @@ package problem3 -type Set struct{} +type Set struct { + slc []interface{} +} + +func NewSet() *Set { + return &Set{} +} + +func (s *Set) Add(value interface{}) { + flag := false + for _, v := range s.slc { + if v == value { + flag = true + } + } + + if !flag { + s.slc = append(s.slc, value) + } +} + +func (s *Set) Remove(value interface{}) { + var newSlc []interface{} + for _, v := range s.slc { + if v != value { + newSlc = append(newSlc, v) + } + } + s.slc = newSlc +} + +func (s *Set) IsEmpty() bool { + return len(s.slc) == 0 +} + +func (s *Set) Size() int { + return len(s.slc) +} + +func (s *Set) List() []interface{} { + return s.slc +} + +func (s *Set) Has(value interface{}) bool { + for _, v := range s.slc { + if v == value { + return true + } + } + return false +} + +func (s *Set) Copy() *Set { + newSet := NewSet() + for _, value := range s.slc { + newSet.Add(value) + } + return newSet +} + +func (s *Set) Difference(ss *Set) *Set { + newSet := NewSet() + for _, value := range s.slc { + flag := false + for _, value2 := range ss.slc { + if value == value2 { + flag = true + break + } + } + if !flag { + newSet.Add(value) + } + } + return newSet +} + +func (s *Set) IsSubset(ss *Set) bool { + for _, value := range s.slc { + flag := false + for _, value2 := range ss.slc { + if value == value2 { + flag = true + break + } else { + flag = false + } + } + if !flag { + return false + } + } + return true +} + +func Union(sets ...*Set) *Set { + newSet := NewSet() + for _, set := range sets { + for _, value := range set.slc { + newSet.Add(value) + } + } + return newSet +} + +func Intersect(sets ...*Set) *Set { + newSet := NewSet() + for _, value := range sets[0].slc { + newSet.Add(value) + } + + for _, set := range sets[1:] { + temp := NewSet() + for _, value := range newSet.slc { + if set.Has(value) { + temp.Add(value) + } + } + newSet = temp + } + return newSet +} diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..2efa256e 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,109 @@ package problem4 -type LinkedList struct{} +import ( + "errors" + "fmt" +) + +type LinkedList[T comparable] struct { + head *Element[T] +} + +type Element[T comparable] struct { + value T + next *Element[T] +} + +func (list *LinkedList[T]) Add(element *Element[T]) { + newElement := element + if list.head == nil { + list.head = newElement + } else { + currentList := list.head + for currentList.next != nil { + currentList = currentList.next + } + currentList.next = newElement + } +} + +func (list *LinkedList[T]) Insert(element *Element[T], index int) error { + if index < 0 || index > list.Size() { + return errors.New("индекс превышен") + } + + i := 1 + if index == i { + temporary := list.head + list.head = element + list.head.next = temporary + return nil + } + + currentList := list.head + for currentList.next != nil { + i++ + fmt.Println(currentList.value) + if i == index { + temp := currentList.next + currentList.next = element + currentList.next.next = temp + return nil + } + currentList = currentList.next + } + return nil +} + +func (list *LinkedList[T]) Delete(element *Element[T]) error { + if list.head.value == element.value { + list.head = list.head.next + } + currentList := list.head + if currentList == nil { + return errors.New("пумпумпум пусто") + } + for currentList.next != nil { + if currentList.next.value == element.value { + currentList.next = currentList.next.next + } + currentList = currentList.next + } + return errors.New("не найден") +} + +func (list *LinkedList[T]) Find(value any) (Element[T], error) { + if list.head.value == value { + return *list.head, nil + } + currentList := list.head + for currentList.next != nil { + if currentList.next.value == value { + return *currentList.next, nil + } + currentList = currentList.next + } + return Element[T]{}, errors.New("не найден") +} + +func (list *LinkedList[T]) List() []T { + var result []T + if list.head == nil { + return result + } + currentList := list.head + result = append(result, currentList.value) + for currentList.next != nil { + currentList = currentList.next + result = append(result, currentList.value) + } + return result +} + +func (list *LinkedList[T]) Size() int { + return len(list.List()) +} + +func (list *LinkedList[T]) IsEmpty() bool { + return list.Size() == 0 +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..eac89ad6 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,16 @@ package problem5 -type Person struct{} +type Person struct { + name string + age int +} + +func (person Person) compareAge(comparePerson *Person) string { + if comparePerson.age > person.age { + return comparePerson.name + " is older than me." + } else if comparePerson.age < person.age { + return comparePerson.name + " is younger than me." + } else { + return comparePerson.name + " is the same age as me." + } +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..638f5231 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,31 @@ package problem6 -type Animal struct{} +type Animal struct { + name string + legsNum int +} -type Insect struct{} +type Insect struct { + name string + legsNum int +} -func sumOfAllLegsNum() {} +type HaveLeggs interface { + getLeggs() int +} + +func (animal *Animal) getLeggs() int { + return animal.legsNum +} + +func (insect *Insect) getLeggs() int { + return insect.legsNum +} + +func sumOfAllLegsNum(haveLeggs ...HaveLeggs) int { + quantity := 0 + for _, value := range haveLeggs { + quantity += value.getLeggs() + } + return quantity +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..8e61ea8a 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,63 @@ package problem7 +type Withdrawal interface { + Withdraw(amount int) bool +} + +type PackageSender interface { + SendPackage(to string) +} + type BankAccount struct { + name string + balance int } type FedexAccount struct { + name string + packages []string } type KazPostAccount struct { + name string + balance int + packages []string +} + +func (b *BankAccount) Withdraw(amount int) bool { + if amount > b.balance { + return false + } + b.balance -= amount + return true +} + +func (k *KazPostAccount) Withdraw(amount int) bool { + if amount > k.balance { + return false + } + k.balance -= amount + return true +} + +func (account *FedexAccount) SendPackage(to string) { + message := account.name + " send package to " + to + account.packages = append(account.packages, message) +} + +func (account *KazPostAccount) SendPackage(to string) { + message := account.name + " send package to " + to + account.packages = append(account.packages, message) +} + +func withdrawMoney(amount int, accounts ...Withdrawal) { + for _, account := range accounts { + account.Withdraw(amount) + } +} + +func sendPackagesTo(to string, accounts ...PackageSender) { + for _, account := range accounts { + account.SendPackage(to) + } } diff --git a/exercise4/README.md b/exercise4/README.md index 20dc94b8..99263dd8 100644 --- a/exercise4/README.md +++ b/exercise4/README.md @@ -8,7 +8,7 @@ You have two projects `judge` and `bot`. can run it: ```shell -$ PORT=4444 go run . +//$ PORT=4444 go run . ``` * `bot` is a boilerplate app for you bot. Don't delete anything, but rather add. diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..cd5e3494 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -1,21 +1,100 @@ package main import ( + "bytes" "context" + "encoding/json" + "fmt" + "log" + "net/http" "os" "os/signal" "syscall" + "time" ) +type MoveRequest struct { + Board []string `json:"board"` + Token string `json:"token"` +} + +type MoveResponse struct { + Index int `json:"index"` +} + func main() { - ctx := context.Background() + + _, cancel := context.WithCancel(context.Background()) + defer cancel() ready := startServer() <-ready - // TODO after server start + http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("pong")) + }) + + http.HandleFunc("/move", func(w http.ResponseWriter, r *http.Request) { + var req MoveRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + move := findBestMove(req.Board) + + resp := MoveResponse{Index: move} + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(resp); err != nil { + http.Error(w, "Error encoding response", http.StatusInternalServerError) + } + }) + + err := registerBot(os.Getenv("NAME"), "http://localhost:"+os.Getenv("PORT")) + if err != nil { + log.Fatalf("Ошибка регистрации бота: %v", err) + } stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - <-stop // Wait for SIGINT or SIGTERM + <-stop + log.Println("Shutting down gracefully...") + cancel() + time.Sleep(1 * time.Second) +} + +func registerBot(name, url string) error { + registerURL := "http://localhost:4444/join" + botInfo := map[string]string{ + "name": name, + "url": url, + } + + jsonData, err := json.Marshal(botInfo) + if err != nil { + return fmt.Errorf("ошибка сериализации данных бота: %w", err) + } + + resp, err := http.Post(registerURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("ошибка отправки запроса: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("не удалось зарегистрировать бота, статус: %d", resp.StatusCode) + } + + log.Println("Бот успешно зарегистрирован") + return nil +} + +func findBestMove(board []string) int { + for i, cell := range board { + if cell == " " { + return i + } + } + return -1 } diff --git a/exercise4/judge/main.go b/exercise4/judge/main.go index 1df66255..0cea3ff9 100644 --- a/exercise4/judge/main.go +++ b/exercise4/judge/main.go @@ -10,6 +10,7 @@ import ( ) func main() { + ctx, cancel := context.WithCancel(context.Background()) port := os.Getenv("PORT") diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..4e4f56d9 100644 --- a/exercise5/problem1/problem1.go +++ b/exercise5/problem1/problem1.go @@ -1,9 +1,12 @@ package problem1 func incrementConcurrently(num int) int { + + result := make(chan int) + go func() { - num++ + result <- num + 1 }() - return num + return <-result } diff --git a/exercise5/problem2/problem2.go b/exercise5/problem2/problem2.go index 16d38e1d..0905aad9 100644 --- a/exercise5/problem2/problem2.go +++ b/exercise5/problem2/problem2.go @@ -1,5 +1,10 @@ package problem2 +import ( + "runtime" + "sync" +) + // add - sequential code to add numbers, don't update it, just to illustrate concept func add(numbers []int) int64 { var sum int64 @@ -10,7 +15,35 @@ func add(numbers []int) int64 { } func addConcurrently(numbers []int) int64 { + quantityOfCpu := runtime.NumCPU() + runtime.GOMAXPROCS(quantityOfCpu) + + sizeOfBlock := (len(numbers) + quantityOfCpu - 1) / quantityOfCpu + var wg sync.WaitGroup + result := make([]int64, quantityOfCpu) + + for i := 0; i < quantityOfCpu; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + start := i * sizeOfBlock + end := start + sizeOfBlock + if end > len(numbers) { + end = len(numbers) + } + for _, n := range numbers[start:end] { + result[i] += int64(n) + } + }(i) + } + + wg.Wait() + var sum int64 + for _, result := range result { + sum += result + } + return sum } diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..94bc1025 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -1,11 +1,11 @@ package problem3 func sum(a, b int) int { - var c int + result := make(chan int) go func(a, b int) { - c = a + b + result <- a + b }(a, b) - return c + return <-result } 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..d273bf22 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,8 +1,21 @@ package problem5 -func producer() {} +import "strings" -func consumer() {} +func producer(words []string, ch chan<- string) { + for _, word := range words { + ch <- word + } + close(ch) +} + +func consumer(ch <-chan string) string { + var result []string + for word := range ch { + result = append(result, word) + } + return strings.Join(result, " ") +} func send( words []string, diff --git a/exercise5/problem6/problem6.go b/exercise5/problem6/problem6.go index e1beea87..e6487d33 100644 --- a/exercise5/problem6/problem6.go +++ b/exercise5/problem6/problem6.go @@ -2,8 +2,32 @@ package problem6 type pipe func(in <-chan int) <-chan int -var multiplyBy2 pipe = func() {} +var multiplyBy2 pipe = func(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for n := range in { + out <- n * 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 n := range in { + out <- n + 5 + } + }() + return out +} -func piper(in <-chan int, pipes []pipe) <-chan int {} +func piper(in <-chan int, pipes []pipe) <-chan int { + output := in + for _, p := range pipes { + output = p(output) + } + return output +} diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..09f01ebc 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 { + + var result []string + + for { + select { + case value1, ok1 := <-ch1: + if ok1 { + result = append(result, value1) + } else { + ch1 = nil + } + case value2, ok2 := <-ch2: + if ok2 { + result = append(result, value2) + } else { + ch2 = nil + } + } + + if ch1 == nil && ch2 == nil { + break + } + } + return result +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..c9e5d102 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 message := <-ch: + return message + case <-time.After(ttl): + return "timeout" + } +} diff --git a/exercise6/problem1/problem1.go b/exercise6/problem1/problem1.go index ee453b24..9d6423af 100644 --- a/exercise6/problem1/problem1.go +++ b/exercise6/problem1/problem1.go @@ -1,9 +1,31 @@ package problem1 +import ( + "fmt" + "sync" +) + type bankAccount struct { + mu sync.Mutex blnc int } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + return &bankAccount{blnc: blnc} +} + +func (a *bankAccount) deposit(amount int) { + a.mu.Lock() + defer a.mu.Unlock() + a.blnc += amount +} + +func (a *bankAccount) withdraw(amount int) error { + a.mu.Lock() + defer a.mu.Unlock() + if a.blnc > amount { + a.blnc -= amount + return nil + } + return fmt.Errorf("Pupupu, too little money") } diff --git a/exercise6/problem2/problem2.go b/exercise6/problem2/problem2.go index 97e02368..671a3a9b 100644 --- a/exercise6/problem2/problem2.go +++ b/exercise6/problem2/problem2.go @@ -1,6 +1,8 @@ package problem2 import ( + "fmt" + "sync" "time" ) @@ -8,13 +10,32 @@ var readDelay = 10 * time.Millisecond type bankAccount struct { blnc int + mu sync.Mutex } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + return &bankAccount{blnc: blnc} } func (b *bankAccount) balance() int { time.Sleep(readDelay) - return 0 + b.mu.Lock() + defer b.mu.Unlock() + return b.blnc +} + +func (a *bankAccount) deposit(amount int) { + a.mu.Lock() + defer a.mu.Unlock() + a.blnc += amount +} + +func (a *bankAccount) withdraw(amount int) error { + a.mu.Lock() + defer a.mu.Unlock() + if a.blnc > amount { + a.blnc -= amount + return nil + } + return fmt.Errorf("Pupupu, too little money") } diff --git a/exercise6/problem3/problem3.go b/exercise6/problem3/problem3.go index b34b90bb..acb946e2 100644 --- a/exercise6/problem3/problem3.go +++ b/exercise6/problem3/problem3.go @@ -1,5 +1,7 @@ package problem3 +import "sync/atomic" + type counter struct { val int64 } @@ -9,3 +11,15 @@ func newCounter() *counter { val: 0, } } + +func (c *counter) inc() { + atomic.AddInt64(&c.val, 1) +} + +func (c *counter) dec() { + atomic.AddInt64(&c.val, -1) +} + +func (c *counter) value() int64 { + return atomic.LoadInt64(&c.val) +} diff --git a/exercise6/problem4/problem4.go b/exercise6/problem4/problem4.go index 793449c9..177d0b0d 100644 --- a/exercise6/problem4/problem4.go +++ b/exercise6/problem4/problem4.go @@ -1,11 +1,19 @@ package problem4 import ( + "sync" "time" ) -func worker(id int, _ *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +var shopListCond = sync.NewCond(&sync.Mutex{}) +var listFull bool + +func worker(id int, shoppingList *[]string, ch chan<- int) { + shopListCond.L.Lock() + defer shopListCond.L.Unlock() + for !listFull { + shopListCond.Wait() + } ch <- id } @@ -15,6 +23,10 @@ func updateShopList(shoppingList *[]string) { *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + shopListCond.L.Lock() + listFull = true + shopListCond.Signal() + shopListCond.L.Unlock() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { diff --git a/exercise6/problem5/problem5.go b/exercise6/problem5/problem5.go index 8e4a1703..8a0f0fcb 100644 --- a/exercise6/problem5/problem5.go +++ b/exercise6/problem5/problem5.go @@ -1,11 +1,19 @@ package problem5 import ( + "sync" "time" ) +var shopListCond = sync.NewCond(&sync.Mutex{}) +var listFull bool + func worker(id int, shoppingList *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed + shopListCond.L.Lock() + defer shopListCond.L.Unlock() + for !listFull { + shopListCond.Wait() + } ch <- id } @@ -15,6 +23,10 @@ func updateShopList(shoppingList *[]string) { *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + shopListCond.L.Lock() + listFull = true + shopListCond.Broadcast() + shopListCond.L.Unlock() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { diff --git a/exercise6/problem6/problem6.go b/exercise6/problem6/problem6.go index 0c1122b9..27e00196 100644 --- a/exercise6/problem6/problem6.go +++ b/exercise6/problem6/problem6.go @@ -4,6 +4,8 @@ import ( "sync" ) +var once sync.Once + func runTasks(init func()) { var wg sync.WaitGroup @@ -12,8 +14,7 @@ func runTasks(init func()) { 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..fbab9284 100644 --- a/exercise6/problem7/problem7.go +++ b/exercise6/problem7/problem7.go @@ -6,12 +6,14 @@ import ( "time" ) +//TODO: identify the data race +// fix the issue. + func task() { start := time.Now() var t *time.Timer t = time.AfterFunc( - randomDuration(), - func() { + randomDuration(), func() { fmt.Println(time.Now().Sub(start)) t.Reset(randomDuration()) }, diff --git a/exercise6/problem8/problem8.go b/exercise6/problem8/problem8.go index 949eb2d2..b5dfce92 100644 --- a/exercise6/problem8/problem8.go +++ b/exercise6/problem8/problem8.go @@ -1,3 +1,29 @@ package problem8 -func multiplex(chs []<-chan string) []string {} +func multiplex(chs []<-chan string) []string { + resultChan := make(chan string) + results := []string{} + ok := make(chan struct{}) + + for _, ch := range chs { + go func(ch <-chan string) { + for val := range ch { + resultChan <- val + } + ok <- struct{}{} + }(ch) + } + + go func() { + for range chs { + <-ok + } + close(resultChan) + }() + + for val := range resultChan { + results = append(results, val) + } + + return results +} 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/Dockerfile b/exercise7/blogging-platform/Dockerfile new file mode 100644 index 00000000..70b756e4 --- /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.3 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage bind mounts to go.sum and go.mod to avoid having to copy them into +# the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.sum,target=go.sum \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x + +# This is the architecture you're building for, which is passed in by the builder. +# Placing it here allows the previous steps to be cached across architectures. +ARG TARGETARCH + +# Build the application. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage a bind mount to the current directory to avoid having to copy the +# source code into the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,target=. \ + CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . + +################################################################################ +# Create a new stage for running the application that contains the minimal +# runtime dependencies for the application. This often uses a different base +# image from the build stage where the necessary files are copied from the build +# stage. +# +# The example below uses the alpine image as the foundation for running the app. +# By specifying the "latest" tag, it will also use whatever happens to be the +# most recent version of that image when you build your Dockerfile. If +# reproducability is important, consider using a versioned tag +# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff). +FROM alpine:latest AS final + +# 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/README.Docker.md b/exercise7/blogging-platform/README.Docker.md new file mode 100644 index 00000000..3b643020 --- /dev/null +++ b/exercise7/blogging-platform/README.Docker.md @@ -0,0 +1,22 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +Your application will be available at http://localhost:80. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. + +### References +* [Docker's Go guide](https://docs.docker.com/language/golang/) \ No newline at end of file diff --git a/exercise7/blogging-platform/README.md b/exercise7/blogging-platform/README.md index e6ef7017..6942ef1c 100644 --- a/exercise7/blogging-platform/README.md +++ b/exercise7/blogging-platform/README.md @@ -1,3 +1,3 @@ # Blogging Platform -Please check https://roadmap.sh/projects/blogging-platform-api. +Please check . diff --git a/exercise7/blogging-platform/compose.yaml b/exercise7/blogging-platform/compose.yaml new file mode 100644 index 00000000..4e0dc927 --- /dev/null +++ b/exercise7/blogging-platform/compose.yaml @@ -0,0 +1,52 @@ +# 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-SHELL", "pg_isready -U $DB_USER -d $DB_NAME"] + 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`. \ No newline at end of file diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..efa787f1 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 blogging-platform go 1.23.3 -require github.com/lib/pq v1.10.9 +require ( + github.com/joho/godotenv v1.5.1 // indirect + github.com/lib/pq v1.10.9 // indirect +) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..bc72e2da 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,2 +1,6 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/talgat-ruby/exercises-go/exercise7/blogging-platform v0.0.0-20250127131848-cb75c492b4ea h1:d7sZ3zGM4pdaYgy6E/L3tvLLoq4DhWptdNKeU6qMtbU= +github.com/talgat-ruby/exercises-go/exercise7/blogging-platform v0.0.0-20250127131848-cb75c492b4ea/go.mod h1:mTnB2JDUkCeXG4L/NQfQG35ZsbpmvIuIv6/p+hw2Xfk= 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..bbfdbac6 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/main.go @@ -0,0 +1,17 @@ +package handler + +import ( + "blogging-platform/internal/api/handler/postes" + "blogging-platform/internal/db" + "log/slog" +) + +type Handler struct { + *postes.Posts +} + +func New(logger *slog.Logger, db *db.DB) *Handler { + return &Handler{ + Posts: postes.New(logger, db), + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/postes/create.go b/exercise7/blogging-platform/internal/api/handler/postes/create.go new file mode 100644 index 00000000..bf2393b9 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/postes/create.go @@ -0,0 +1,33 @@ +package postes + +import ( + "blogging-platform/internal/db/post" + "blogging-platform/pkg/httputils/request" + "blogging-platform/pkg/httputils/response" + "net/http" +) + +func (h *Posts) PostsCreate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + h.logger.With("method", "PostsCreate") + + data := &post.Model{} + if err := request.JSON(w, r, data); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + res, err := h.db.PostsCreate(ctx, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if err := response.JSON( + w, + http.StatusOK, + res, + ); err != nil { + h.logger.ErrorContext(ctx, "Error json response", "error", err) + } + +} diff --git a/exercise7/blogging-platform/internal/api/handler/postes/delete.go b/exercise7/blogging-platform/internal/api/handler/postes/delete.go new file mode 100644 index 00000000..972a7d11 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/postes/delete.go @@ -0,0 +1,37 @@ +package postes + +import ( + "blogging-platform/pkg/httputils/response" + "database/sql" + "errors" + "net/http" + "strconv" +) + +func (h *Posts) PostsDelete(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + h.logger.With("method", "PostsDelete") + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + h.logger.ErrorContext(ctx, "id to int conv error", "error", err) + } + + err = h.db.PostsDelete(ctx, int64(id)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + http.Error(w, "Not found", http.StatusNotFound) + } + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if err := response.JSON( + w, + http.StatusNoContent, + nil, + ); err != nil { + h.logger.ErrorContext(ctx, "Error json response", "error", err) + } + +} diff --git a/exercise7/blogging-platform/internal/api/handler/postes/index.go b/exercise7/blogging-platform/internal/api/handler/postes/index.go new file mode 100644 index 00000000..9dd33934 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/postes/index.go @@ -0,0 +1,28 @@ +package postes + +import ( + "blogging-platform/pkg/httputils/response" + "net/http" +) + +func (h *Posts) PostsIndex(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + h.logger.With("method", "PostsIndex") + + term := r.URL.Query().Get("term") + h.logger.ErrorContext(ctx, "s string", "term", term) + + res, err := h.db.PostsIndex(ctx, term) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if err := response.JSON( + w, + http.StatusOK, + res, + ); err != nil { + h.logger.ErrorContext(ctx, "Error json response", "error", err) + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/postes/main.go b/exercise7/blogging-platform/internal/api/handler/postes/main.go new file mode 100644 index 00000000..a9a2e707 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/postes/main.go @@ -0,0 +1,18 @@ +package postes + +import ( + "blogging-platform/internal/db" + "log/slog" +) + +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/postes/show.go b/exercise7/blogging-platform/internal/api/handler/postes/show.go new file mode 100644 index 00000000..98a139b0 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/postes/show.go @@ -0,0 +1,35 @@ +package postes + +import ( + "blogging-platform/pkg/httputils/response" + "database/sql" + "errors" + "net/http" + "strconv" +) + +func (h *Posts) PostsShow(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + h.logger.With("method", "PostsShow") + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + h.logger.ErrorContext(ctx, "id to int conv error", "error", err) + } + res, err := h.db.PostsShow(ctx, int64(id)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + http.Error(w, "Not found", http.StatusNotFound) + } + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if err := response.JSON( + w, + http.StatusOK, + res, + ); err != nil { + h.logger.ErrorContext(ctx, "Error json response", "error", err) + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/postes/update.go b/exercise7/blogging-platform/internal/api/handler/postes/update.go new file mode 100644 index 00000000..98295c0a --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/postes/update.go @@ -0,0 +1,46 @@ +package postes + +import ( + "blogging-platform/internal/db/post" + "blogging-platform/pkg/httputils/request" + "blogging-platform/pkg/httputils/response" + "database/sql" + "errors" + "net/http" + "strconv" +) + +func (h *Posts) PostsUpdate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + h.logger.With("method", "PostsUpdate") + + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + h.logger.ErrorContext(ctx, "id to int conv error", "error", err) + } + + data := &post.Model{} + if err := request.JSON(w, r, data); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + res, err := h.db.PostsUpdate(ctx, int64(id), data) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + http.Error(w, "Not found", http.StatusNotFound) + } + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if err := response.JSON( + w, + http.StatusOK, + res, + ); err != nil { + h.logger.ErrorContext(ctx, "Error json response", "error", err) + } + +} diff --git a/exercise7/blogging-platform/internal/api/main.go b/exercise7/blogging-platform/internal/api/main.go new file mode 100644 index 00000000..a661a3d0 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/main.go @@ -0,0 +1,58 @@ +package api + +import ( + "blogging-platform/internal/api/handler" + "blogging-platform/internal/api/router" + "blogging-platform/internal/db" + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +type Api struct { + logger *slog.Logger + router *router.Router +} + +func NewApi(logger *slog.Logger, db *db.DB) *Api { + + h := handler.New(logger.With("type", "h"), db) + r := router.New(h) + return &Api{ + logger: logger, + router: r, + } +} + +func (api *Api) Start(ctx context.Context) error { + + mux := api.router.Start(ctx) + _ = godotenv.Load() + 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(l net.Listener) context.Context { + return ctx + }, + } + + fmt.Println("Server successfully started") + 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..53c47b06 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/main.go @@ -0,0 +1,26 @@ +package router + +import ( + "blogging-platform/internal/api/handler" + "context" + "net/http" +) + +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.PostRouter(ctx) + + return r.router +} diff --git a/exercise7/blogging-platform/internal/api/router/postes.go b/exercise7/blogging-platform/internal/api/router/postes.go new file mode 100644 index 00000000..0babaccc --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/postes.go @@ -0,0 +1,11 @@ +package router + +import "context" + +func (r *Router) PostRouter(ctx context.Context) { + r.router.HandleFunc("GET /posts", r.handler.PostsIndex) + r.router.HandleFunc("GET /posts/{id}", r.handler.PostsShow) + r.router.HandleFunc("POST /posts", r.handler.PostsCreate) + r.router.HandleFunc("PUT /posts/{id}", r.handler.PostsUpdate) + r.router.HandleFunc("DELETE /posts/{id}", r.handler.PostsDelete) +} diff --git a/exercise7/blogging-platform/internal/db/init.go b/exercise7/blogging-platform/internal/db/init.go new file mode 100644 index 00000000..5c9a6f30 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/init.go @@ -0,0 +1,19 @@ +package db + +func (db *DB) Init() error { + stmt := `CREATE TABLE IF NOT EXISTS posts ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + content TEXT DEFAULT NULL, + category TEXT DEFAULT NULL, + tags TEXT[], + created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NOW() + )` + + if _, err := db.pg.Exec(stmt); err != nil { + return err + } + + return nil +} diff --git a/exercise7/blogging-platform/internal/db/main.go b/exercise7/blogging-platform/internal/db/main.go new file mode 100644 index 00000000..54602173 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/main.go @@ -0,0 +1,47 @@ +package db + +import ( + "blogging-platform/internal/db/post" + "database/sql" + "fmt" + "log/slog" + "os" + "strconv" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +type DB struct { + logger *slog.Logger + pg *sql.DB + *post.Post +} + +func NewDB(logger *slog.Logger) (*DB, error) { + conn, err := newPgSQL() + if err != nil { + return nil, err + } + + return &DB{ + logger: logger, + pg: conn, + Post: post.New(conn, logger), + }, nil +} + +func newPgSQL() (*sql.DB, error) { + _ = godotenv.Load() + port, err := strconv.Atoi(os.Getenv("DB_PORT")) + if err != nil { + return nil, err + } + psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", os.Getenv("DB_HOST"), port, os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME")) + 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.go b/exercise7/blogging-platform/internal/db/post/create.go new file mode 100644 index 00000000..b29bc733 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/create.go @@ -0,0 +1,32 @@ +package post + +import ( + "context" + "database/sql" + "errors" + + "github.com/lib/pq" +) + +func (m *Post) PostsCreate(ctx context.Context, model *Model) (*Model, error) { + log := m.logger.With("post_method", "PostsCreate") + + stmt := `INSERT INTO posts (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, model.Title, model.Content, model.Category, pq.Array(model.Tags)) + if row.Err() != nil { + return nil, row.Err() + } + + result := Model{} + + if err := row.Scan(&result.ID, &result.Title, &result.Content, &result.Category, &result.Tags, &result.CreatedAt, &result.UpdatedAt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + log.ErrorContext(ctx, "failed create to db", "error", err) + return nil, err + } + + return &result, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/delete.go b/exercise7/blogging-platform/internal/db/post/delete.go new file mode 100644 index 00000000..df01b2b2 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/delete.go @@ -0,0 +1,25 @@ +package post + +import ( + "context" +) + +func (m *Post) PostsDelete(ctx context.Context, id int64) error { + log := m.logger.With("post_method", "PostsDelete") + + stmt := `DELETE FROM posts WHERE id = $1 RETURNING id` + + row := m.db.QueryRowContext(ctx, stmt, id) + if row.Err() != nil { + log.ErrorContext(ctx, "failed delete db", "error", row.Err()) + return row.Err() + } + + err := row.Scan(&id) + + if err != nil { + return err + } + + return nil +} diff --git a/exercise7/blogging-platform/internal/db/post/index.go b/exercise7/blogging-platform/internal/db/post/index.go new file mode 100644 index 00000000..47cbc420 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/index.go @@ -0,0 +1,46 @@ +package post + +import ( + "context" + "database/sql" +) + +func (m *Post) PostsIndex(ctx context.Context, term string) ([]Model, error) { + log := m.logger.With("post_method", "PostsIndex") + + var rows *sql.Rows + var err error + + models := make([]Model, 0) + + stmt := `SELECT id, title, content, category, tags, created_at, updated_at FROM posts` + + if term != "" { + stmt += ` WHERE title ILIKE $1 OR content ILIKE $1 OR category ILIKE $1` + rows, err = m.db.QueryContext(ctx, stmt, "%"+term+"%") + } else { + rows, err = m.db.QueryContext(ctx, stmt) + } + + if err != nil { + return nil, err + } + + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + + } + }(rows) + + for rows.Next() { + model := Model{} + if err := rows.Scan(&model.ID, &model.Title, &model.Content, &model.Category, &model.Tags, &model.CreatedAt, &model.UpdatedAt); err != nil { + log.ErrorContext(ctx, "failed index from db", "error", err) + return nil, err + } + models = append(models, model) + } + + return models, 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..f68900ae --- /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 { + db *sql.DB + logger *slog.Logger +} + +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..a830a84e --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/model.go @@ -0,0 +1,16 @@ +package post + +import ( + "github.com/lib/pq" + "time" +) + +type Model 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/show.go b/exercise7/blogging-platform/internal/db/post/show.go new file mode 100644 index 00000000..8c7d3f95 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/show.go @@ -0,0 +1,23 @@ +package post + +import "context" + +func (m *Post) PostsShow(ctx context.Context, id int64) (*Model, error) { + log := m.logger.With("post_method", "PostsShow") + + stmt := `SELECT id, title, content, category, tags, created_at, updated_at FROM posts WHERE id = $1` + + row := m.db.QueryRowContext(ctx, stmt, id) + if row.Err() != nil { + return nil, row.Err() + } + + model := Model{} + + if err := row.Scan(&model.ID, &model.Title, &model.Content, &model.Category, &model.Tags, &model.CreatedAt, &model.UpdatedAt); err != nil { + log.ErrorContext(ctx, "failed show of db", "error", err) + return nil, err + } + + return &model, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/update.go b/exercise7/blogging-platform/internal/db/post/update.go new file mode 100644 index 00000000..6f88dde0 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/update.go @@ -0,0 +1,27 @@ +package post + +import ( + "context" + + "github.com/lib/pq" +) + +func (m *Post) PostsUpdate(ctx context.Context, id int64, model *Model) (*Model, error) { + log := m.logger.With("post_method", "PostsUpdate") + + stmt := `UPDATE posts SET title = $2, content = $3, category = $4, tags = $5 WHERE id = $1 RETURNING id, title, content, category, tags, created_at, updated_at` + + row := m.db.QueryRowContext(ctx, stmt, id, model.Title, model.Content, model.Category, pq.Array(model.Tags)) + if row.Err() != nil { + return nil, row.Err() + } + + result := Model{} + + if err := row.Scan(&result.ID, &result.Title, &result.Content, &result.Category, &result.Tags, &result.CreatedAt, &result.UpdatedAt); err != nil { + log.ErrorContext(ctx, "failed update", "error", err) + return nil, err + } + + return &result, nil +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 1ffa1477..040103bc 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -1,49 +1,28 @@ package main import ( + "blogging-platform/internal/api" + "blogging-platform/internal/db" "context" "log/slog" - "os" - "os/signal" - - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api" - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" ) func main() { - ctx, cancel := context.WithCancel(context.Background()) - // db - _, err := db.New() + ctx := context.Background() + + newDB, err := db.NewDB(slog.With("service", "database")) 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) + err = newDB.Init() + if err != nil { + return } - 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() - }() + newApi := api.NewApi(slog.With("service", "database"), newDB) + if err := newApi.Start(ctx); err != nil { + panic(err) + } } diff --git a/exercise7/blogging-platform/pkg/httputils/request/body.go b/exercise7/blogging-platform/pkg/httputils/request/body.go index 92d639f4..3d8bad28 100644 --- a/exercise7/blogging-platform/pkg/httputils/request/body.go +++ b/exercise7/blogging-platform/pkg/httputils/request/body.go @@ -1,14 +1,13 @@ package request import ( + "blogging-platform/pkg/httputils/statusError" "encoding/json" "errors" "fmt" "io" "net/http" "strings" - - "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/statusError" ) func JSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {