diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..f5dd938e 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,10 @@ package main -func addUp() {} +func addUp(x int) int { + var c int = 0 + var i int + for i = 1; i <= x; i++ { + c += i + } + return c +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..26409b81 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,19 @@ package main -func sum() {} +import ( + "errors" + "fmt" + "strconv" +) + +func sum(a, b string) (string, error) { + sum1, err := strconv.Atoi(a) + if err != nil { + return "", errors.New("string: a cannot be converted") + } + sum2, err := strconv.Atoi(b) + if err != nil { + return "", errors.New("string: b cannot be converted") + } + return fmt.Sprintf("%v", sum1+sum2), nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..eaad0de7 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,15 @@ package main -func binary() {} +import "fmt" + +func binary(x int) string { + var result string + if x == 0 { + return "0" + } + for x > 0 { + result = fmt.Sprintf("%v", x%2) + result + x = x / 2 + } + return result +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..24dfbe06 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,10 @@ package main -func numberSquares() {} +func numberSquares(x int) int { + var ac int + for x > 0 { + ac += x * x + x -= 1 + } + return ac +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..a18f8ea8 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,13 @@ package main -func detectWord() {} +import "unicode" + +func detectWord(str string) string { + var result string + for _, ch := range str { + if unicode.IsLower(ch) { + result += string(ch) + } + } + return result +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..918e5b22 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,7 @@ package main -func potatoes() {} +import "strings" + +func potatoes(str string) int { + return strings.Count(str, "potato") +} diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..58705650 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,22 @@ package main -func emojify() {} +import "strings" + +func emojify(str string) string { + return strings.Replace( + strings.Replace( + strings.Replace( + strings.Replace(str, "smile", "🙂", -1), + "grin", + "😀", + -1, + ), + "sad", + "😥", + -1, + ), + "mad", + "😠", + -1, + ) +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..cad2a0a4 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,13 @@ package main -func highestDigit() {} +func highestDigit(num int) int { + var max int = 0 + for num > 1 { + cur := num % 10 + num = num / 10 + if cur > max { + max = cur + } + } + return max +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..1d3a1113 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,12 @@ package main -func countVowels() {} +import "strings" + +func countVowels(str string) int { + var c int = 0 + var vowels = []string{"a", "e", "i", "o", "u"} + for _, ch := range vowels { + c += strings.Count(str, ch) + } + return c +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..579f4144 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,26 @@ 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 +} + +// func bitwise(a int) string { +// var result string +// for i := 0; i < 8; i++ { +// if a > 0 { +// result = fmt.Sprintf("%v", a%2) + result +// a = a / 2 +// } else { +// result = "0" + result +// } +// } +// return result +// } diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..3ca40c4b 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,5 @@ package problem1 -func isChangeEnough() { +func isChangeEnough(coins [4]int, cost float32) bool { + return (float32(coins[0])*0.25 + float32(coins[1])*0.10 + float32(coins[2])*0.05 + float32(coins[3])*0.01) >= cost } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..91551962 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,12 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(string) func(int)) { + m := map[string]int{} + f := func(model string) func(int) { + m[model] = 0 + return func(inc int) { + m[model] += inc + } + } + return m, f +} diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..f716f3f4 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,19 @@ package problem11 -func removeDups() {} +func removeDups[T int | bool | string](sl []T) []T { + var result []T + var l bool = false + for i, a := range sl { + l = false + for _, b := range result { + if a == b { + l = true + break + } + } + if l == false { + result = append(result, sl[i]) + } + } + return result +} diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..96911bc7 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,23 @@ package problem11 -func keysAndValues() {} +import ( + "slices" +) + +func keysAndValues[T int | string, P int | bool | string](m map[T]P) ([]T, []P) { + + var keys []T + var values []P + for key, value := range m { + keys = append(keys, key) + values = append(values, value) + } + + slices.SortFunc(keys, func(a, b T) int { + if a > b { + return 1 + } + return -1 + }) + return keys, values +} diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..281423cf 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,19 @@ package problem2 -func capitalize() { +import ( + "strings" + "unicode" +) + +func capitalize(names []string) []string { + for i, name := range names { + names[i] = strings.ToLower(name) + r := []rune(names[i]) + if len(r) == 0 { + continue + } + r[0] = unicode.ToUpper(r[0]) + names[i] = string(r) + } + return names } diff --git a/exercise2/problem3/problem3.go b/exercise2/problem3/problem3.go index f183fafb..64d9a0e9 100644 --- a/exercise2/problem3/problem3.go +++ b/exercise2/problem3/problem3.go @@ -9,5 +9,22 @@ const ( lr dir = "lr" ) -func diagonalize() { +func diagonalize(d int, direction dir) [][]int { + matrix := make([][]int, d) + for i := 0; i < d; i++ { + matrix[i] = make([]int, d) + for j := 0; j < d; j++ { + switch { + case direction == ul: + matrix[i][j] = j + i + case direction == lr: + matrix[i][j] = (d-1)*2 - i - j + case direction == ur: + matrix[i][j] = (d - 1) + i - j + case direction == ll: + matrix[i][j] = (d - 1) + j - i + } + } + } + return matrix } diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..d4025e56 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,13 @@ package problem4 -func mapping() { +import ( + "strings" +) + +func mapping(arr []string) map[string]string { + result := map[string]string{} + for _, value := range arr { + result[value] = strings.ToUpper(value) + } + return result } diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..30ed5b6d 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,21 @@ package problem5 -func products() { +import "sort" + +func products(m map[string]int, price int) []string { + result := []string{} + for key, val := range m { + if val > price { + result = append(result, key) + } + } + + sort.Slice(result, func(i, j int) bool { + if m[result[i]] == m[result[j]] { + return result[i] < result[j] + } + return m[result[i]] > m[result[j]] + }) + + return result } diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..01f2ba24 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,12 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(a []int, b []int, sum int) bool { + for _, val1 := range a { + for _, val2 := range b { + if val1+val2 == sum { + return true + } + } + } + return false } diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..b6cdf173 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,5 @@ package problem7 -func swap() { +func swap(a *int, b *int) { + *a, *b = *b, *a } diff --git a/exercise2/problem8/problem8.go b/exercise2/problem8/problem8.go index 9389d3b0..8c8eb869 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -4,13 +4,13 @@ func simplify(list []string) map[string]int { var indMap map[string]int indMap = make(map[string]int) - load(&indMap, &list) + load(indMap, list) return indMap } -func load(m *map[string]int, students *[]string) { - for i, name := range *students { - (*m)[name] = i +func load(m map[string]int, students []string) { + for i, name := range students { + m[name] = i } } diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..5c45931f 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,12 @@ package problem9 -func factory() {} +func factory(a int) func(vals ...int) []int { + fun := func(vals ...int) []int { + for i, val := range vals { + vals[i] = val * a + } + return vals + } + + return fun +} diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..533e3385 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,31 @@ package problem1 -type Queue struct{} +import "errors" + +type Queue struct { + items []any +} + +func (q *Queue) Enqueue(item any) { + q.items = append(q.items, item) +} +func (q *Queue) Dequeue() (any, error) { + if len(q.items) == 0 { + return nil, errors.New("Empty queue") + } + first := q.items[0] + q.items = q.items[1:] + return first, nil +} +func (q *Queue) Peek() (any, error) { + if len(q.items) == 0 { + return nil, errors.New("Empty queue") + } + return q.items[0], nil +} +func (q *Queue) Size() int { + return len(q.items) +} +func (q *Queue) IsEmpty() bool { + return len(q.items) == 0 +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..d9190c6a 100644 --- a/exercise3/problem2/problem2.go +++ b/exercise3/problem2/problem2.go @@ -1,3 +1,34 @@ package problem2 -type Stack struct{} +import "errors" + +type Stack struct { + items []any +} + +func (s *Stack) Push(item any) { + s.items = append(s.items, item) +} + +func (s *Stack) Pop() (any, error) { + last, err := s.Peek() + if err == nil { + s.items = s.items[:s.Size()-1] + } + return last, err +} + +func (s *Stack) Peek() (any, error) { + if s.IsEmpty() { + return nil, errors.New("Stack empty") + } + return s.items[len(s.items)-1], nil +} + +func (s *Stack) Size() int { + return len(s.items) +} + +func (s *Stack) IsEmpty() bool { + return s.Size() == 0 +} diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..03048ea4 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,113 @@ package problem3 -type Set struct{} +type Set struct { + items []any +} + +func NewSet() *Set { + return &Set{items: make([]any, 0)} +} + +func (s *Set) Add(item any) { + if !s.Has(item) { + s.items = append(s.items, item) + } +} + +func (s *Set) Remove(item any) { + if s.IsEmpty() { + return + } + + var rk int + for k, v := range s.items { + if v == item { + rk = k + } + } + + s.items[rk] = s.items[s.Size()-1] + s.items = s.items[:s.Size()-1] +} + +func (s *Set) IsEmpty() bool { + return s.Size() == 0 +} + +func (s *Set) Size() int { + return len(s.items) +} + +func (s *Set) List() []any { + return s.items +} + +func (s *Set) Has(item any) bool { + for _, v := range s.items { + if v == item { + return true + } + } + + return false +} + +func (s *Set) Copy() *Set { + cp := make([]any, s.Size()) + copy(s.items, cp) + return &Set{items: cp} +} + +func (s1 *Set) Difference(s2 *Set) *Set { + diff := make([]any, 0) + for _, v1 := range s1.items { + if !s2.Has(v1) { + diff = append(diff, v1) + } + } + + return &Set{items: diff} +} + +func (sub *Set) IsSubset(s *Set) bool { + for _, v := range sub.items { + if !s.Has(v) { + return false + } + } + + return true +} + +func Union(sets ...*Set) *Set { + un := NewSet() + for _, v := range sets { + for _, v1 := range v.items { + un.Add(v1) + } + } + + return un +} + +func Intersect(sets ...*Set) *Set { + in := NewSet() + if len(sets) == 0 { + return in + } + + if len(sets) == 1 { + return sets[0] + } + + for _, v := range sets[0].items { + in.Add(v) + for _, v1 := range sets[1:] { + if !v1.Has(v) { + in.Remove(v) + } + } + } + + return in +} diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..e13fee40 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,104 @@ package problem4 -type LinkedList struct{} +import ( + "errors" +) + +type LinkedList[T int | bool | string] struct { + head *Element[T] +} + +type Element[T int | bool | string] struct { + next *Element[T] + value T +} + +func (l *LinkedList[T]) Add(item *Element[T]) { + if l.head == nil { + l.head = item + } else { + cur := l.head + for cur.next != nil { + cur = cur.next + } + + cur.next = item + } +} + +func (l *LinkedList[T]) Insert(item *Element[T], position int) error { + if position > l.Size()+1 { + return errors.New("incorrect position") + } + if position == 0 { + item.next = l.head + l.head = item + } else { + cur := l.head + for i := 0; i < position-1; i++ { + cur = cur.next + } + item.next = cur.next + cur.next = item + } + return nil +} + +func (l *LinkedList[T]) Delete(item *Element[T]) error { + if l.IsEmpty() { + return errors.New("empty list") + } + + if l.head.value == item.value { + l.head = l.head.next + return nil + } + + cur := l.head.next + prev := l.head + for cur.next != nil { + if cur.value == item.value { + prev.next = cur.next + return nil + } + prev = cur + cur = cur.next + } + + if cur.next == nil { + return errors.New("not found") + } + + return nil +} + +func (l *LinkedList[T]) Find(value T) (*Element[T], error) { + cur := l.head + for cur.next != nil { + if cur.value == value { + return cur, nil + } + cur = cur.next + } + + return nil, errors.New("not found") +} + +func (l *LinkedList[T]) List() []T { + sl := make([]T, 0) + cur := l.head + for cur != nil { + sl = append(sl, cur.value) + cur = cur.next + } + return sl +} + +func (l *LinkedList[T]) IsEmpty() bool { + return l.Size() == 0 +} + +func (l *LinkedList[T]) Size() int { + + return len(l.List()) +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..108af3a5 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,17 @@ package problem5 -type Person struct{} +type Person struct { + Name string + Age int +} + +func (p *Person) compareAge(p2 *Person) string { + var compare string = " is older than me." + if p2.Age == p.Age { + compare = " is the same age as me." + } + if p2.Age < p.Age { + compare = " is younger than me." + } + return p2.Name + compare +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..345e7160 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,31 @@ package problem6 -type Animal struct{} +type creature interface { + GetLegs() int +} -type Insect struct{} +func (a *Animal) GetLegs() int { + return a.legsNum +} -func sumOfAllLegsNum() {} +type Animal struct { + name string + legsNum int +} + +func (i *Insect) GetLegs() int { + return i.legsNum +} + +type Insect struct { + name string + legsNum int +} + +func sumOfAllLegsNum(s ...creature) int { + var acc int = 0 + for _, v := range s { + acc += v.GetLegs() + } + return acc +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..7b73584f 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,53 @@ package problem7 +type withdrawable interface { + withdraw(sum int) +} + +type sendeble interface { + sendPackages(name string) +} + type BankAccount struct { + name string + balance int +} + +func (b *BankAccount) withdraw(sum int) { + b.balance -= sum } type FedexAccount struct { + name string + packages []string +} + +func (f *FedexAccount) sendPackages(name string) { + f.packages = append(f.packages, f.name+" send package to "+name) } type KazPostAccount struct { + name string + balance int + packages []string +} + +func (k *KazPostAccount) withdraw(sum int) { + k.balance -= sum +} + +func (k *KazPostAccount) sendPackages(name string) { + k.packages = append(k.packages, k.name+" send package to "+name) +} + +func withdrawMoney(sum int, w ...withdrawable) { + for _, v := range w { + v.withdraw(sum) + } +} + +func sendPackagesTo(name string, s ...sendeble) { + for _, v := range s { + v.sendPackages(name) + } } diff --git a/exercise4/bot/main.go b/exercise4/bot/main.go index 64f9e0a3..a0aea121 100644 --- a/exercise4/bot/main.go +++ b/exercise4/bot/main.go @@ -1,20 +1,46 @@ package main import ( - "context" + "bytes" + "fmt" + "net/http" "os" "os/signal" "syscall" ) func main() { - ctx := context.Background() + //ctx := context.Background() + url := "http://localhost:" + os.Getenv("PORT") + gamehost := "http://localhost:4444" + name := os.Getenv("NAME") ready := startServer() <-ready // TODO after server start + requestURL := fmt.Sprintf(gamehost + "/status") + res, err := http.Get(requestURL) + if err != nil { + fmt.Printf("error making http request: %s\n", err) + os.Exit(1) + } + + fmt.Printf("client: status code: %d\n", res.StatusCode) + jsonBody := []byte(`{"name": "` + name + `", "url": "` + url + `"}`) + bodyReader := bytes.NewReader(jsonBody) + + requestURL = fmt.Sprintf(gamehost + "/join") + + res, err = http.Post(requestURL, "application/json", bodyReader) + if err != nil { + fmt.Printf("error making http request: %s\n", err) + os.Exit(1) + } + + fmt.Printf("client: status code: %d\n", res.StatusCode) + stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop // Wait for SIGINT or SIGTERM diff --git a/exercise4/bot/server.go b/exercise4/bot/server.go index e6760ec5..cd161796 100644 --- a/exercise4/bot/server.go +++ b/exercise4/bot/server.go @@ -1,8 +1,10 @@ package main import ( + "encoding/json" "errors" "fmt" + "log" "net" "net/http" "os" @@ -21,6 +23,143 @@ func (l *readyListener) Accept() (net.Conn, error) { return l.Listener.Accept() } +type ResponseMove struct { + Index int `json:"index"` +} + +func servePing(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "{\"message\":\"OK\"}") +} + +type RequestMove struct { + Board [9]string `json:"board"` + Token string `json:"token"` +} + +func getIndex(b [9]string, me string, rival string) int { + index := 0 + sum := 0 + diagCells := [4]int{0, 2, 6, 8} + midCells := [4]int{1, 3, 5, 7} + winCases := [][]int{ + {0, 1, 2}, + {3, 4, 5}, + {6, 7, 8}, + {0, 3, 6}, + {1, 4, 7}, + {2, 5, 8}, + {0, 4, 8}, + {2, 4, 6}, + } + + if b[4] == " " { + return 4 + } else { + for _, v := range winCases { + sum = 0 + if b[v[0]] == me { + sum++ + } else { + if b[v[0]] == rival { + continue + } + index = v[0] + } + if b[v[1]] == me { + sum++ + } else { + if b[v[1]] == rival { + continue + } + index = v[1] + } + if b[v[2]] == me { + sum++ + } else { + if b[v[2]] == rival { + continue + } + index = v[2] + } + + if sum == 2 { + if b[index] == " " { + return index + } + } + } + + for _, v := range winCases { + sum = 0 + if b[v[0]] == rival { + sum++ + } else { + index = v[0] + } + if b[v[1]] == rival { + sum++ + } else { + index = v[1] + } + if b[v[2]] == rival { + sum++ + } else { + index = v[2] + } + + if sum == 2 { + if b[index] == " " { + return index + } + } + } + + for _, v := range diagCells { + if b[v] == " " { + return v + } + } + + for _, v := range midCells { + if b[v] == " " { + return v + } + } + } + + return 0 +} + +func serveMove(w http.ResponseWriter, r *http.Request) { + reqJson := RequestMove{} + respJson := ResponseMove{} + var me string + var rival string + if reqJson.Token == "x" { + me = "x" + rival = "o" + } else { + me = "o" + rival = "x" + } + + if err := json.NewDecoder(r.Body).Decode(&reqJson); err != nil { + log.Fatal(err) + } + + index := getIndex(reqJson.Board, me, rival) + + respJson = ResponseMove{ + Index: index, + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(respJson) + if err != nil { + log.Fatal(err) + } +} + func startServer() <-chan struct{} { ready := make(chan struct{}) @@ -34,6 +173,9 @@ func startServer() <-chan struct{} { IdleTimeout: 2 * time.Minute, } + http.HandleFunc("GET /ping", servePing) + http.HandleFunc("POST /move", serveMove) + go func() { err := srv.Serve(list) if !errors.Is(err, http.ErrServerClosed) { diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..943db542 100644 --- a/exercise5/problem1/problem1.go +++ b/exercise5/problem1/problem1.go @@ -1,9 +1,14 @@ package problem1 func incrementConcurrently(num int) int { - go func() { + ch := make(chan bool) + + go func(ch chan bool) { num++ - }() + ch <- true + }(ch) + + <-ch return num } diff --git a/exercise5/problem2/problem2.go b/exercise5/problem2/problem2.go index 16d38e1d..15baeead 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 @@ -11,6 +16,21 @@ func add(numbers []int) int64 { func addConcurrently(numbers []int) int64 { var sum int64 + var wg sync.WaitGroup + + pCount := runtime.GOMAXPROCS(runtime.NumCPU()) + wg.Add(pCount) + items := len(numbers) / pCount + + for i := range pCount { + part := numbers[i*items : (i+1)*items] + go func(part []int) { + defer wg.Done() + sum += add(part) + }(part) + } + + wg.Wait() return sum } diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..dc77811c 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -2,10 +2,14 @@ package problem3 func sum(a, b int) int { var c int + ch := make(chan bool) - go func(a, b int) { + go func(a, b int, ch chan bool) { c = a + b - }(a, b) + ch <- true + }(a, b, ch) + + <-ch return c } diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index b5899ddf..ce51a219 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -12,8 +12,9 @@ func sum(nums []int) int { go iter(ch, nums) var sum int - for n := range ch { - sum += n + for i := 0; i < len(nums); i++ { + sum += <-ch } + return sum } diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..1f2be1ed 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 _, word := range words { + ch <- word + } -func consumer() {} + ch <- "done" +} + +func consumer(ch <-chan string) string { + var res string + for n := range ch { + if n == "done" { + break + } + if res != "" { + res += " " + } + res += n + } + + return res +} func send( words []string, diff --git a/exercise5/problem6/problem6.go b/exercise5/problem6/problem6.go index e1beea87..e1b8978a 100644 --- a/exercise5/problem6/problem6.go +++ b/exercise5/problem6/problem6.go @@ -2,8 +2,34 @@ package problem6 type pipe func(in <-chan int) <-chan int -var multiplyBy2 pipe = func() {} +var multiplyBy2 pipe = func(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for v := range in { + out <- v * 2 + } + }() -var add5 pipe = func() {} + return out +} -func piper(in <-chan int, pipes []pipe) <-chan int {} +var add5 pipe = func(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for v := range in { + out <- v + 5 + } + }() + + return out +} + +func piper(in <-chan int, pipes []pipe) <-chan int { + for _, p := range pipes { + in = p(in) + } + + return in +} diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..42885f18 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 + var done1 bool + var done2 bool + for { + select { + case val1, ok := <-ch1: + if !ok { + done1 = true + } else { + result = append(result, val1) + } + case val2, ok := <-ch2: + if !ok { + done2 = true + } else { + result = append(result, val2) + } + } + if done1 && done2 { + break + } + + } + return result +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..98caa0aa 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -1,7 +1,21 @@ package problem8 import ( + "context" "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + ctx, cancel := context.WithTimeout(context.Background(), ttl) + defer cancel() + select { + case <-ctx.Done(): + return "timeout" + case result, ok := <-ch: + if ok { + return result + } else { + return "fail" + } + } +} diff --git a/exercise6/problem1/problem1.go b/exercise6/problem1/problem1.go index ee453b24..b347474a 100644 --- a/exercise6/problem1/problem1.go +++ b/exercise6/problem1/problem1.go @@ -1,9 +1,31 @@ package problem1 +import ( + "sync" +) + type bankAccount struct { blnc int + mtx *sync.Mutex +} + +func (b *bankAccount) withdraw(s int) { + defer b.mtx.Unlock() + b.mtx.Lock() + if s > b.blnc { + return + } + b.blnc -= s + +} + +func (b *bankAccount) deposit(s int) { + defer b.mtx.Unlock() + b.mtx.Lock() + b.blnc += s } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + var m sync.Mutex + return &bankAccount{blnc, &m} } diff --git a/exercise6/problem2/problem2.go b/exercise6/problem2/problem2.go index 97e02368..b558aafd 100644 --- a/exercise6/problem2/problem2.go +++ b/exercise6/problem2/problem2.go @@ -1,6 +1,7 @@ package problem2 import ( + "sync" "time" ) @@ -8,13 +9,33 @@ var readDelay = 10 * time.Millisecond type bankAccount struct { blnc int + mtx *sync.Mutex } -func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} +func (b *bankAccount) withdraw(s int) { + defer b.mtx.Unlock() + b.mtx.Lock() + if s > b.blnc { + return + } + b.blnc -= s + +} + +func (b *bankAccount) deposit(s int) { + defer b.mtx.Unlock() + b.mtx.Lock() + b.blnc += s } func (b *bankAccount) balance() int { time.Sleep(readDelay) - return 0 + defer b.mtx.Unlock() + b.mtx.Lock() + return b.blnc +} + +func newAccount(blnc int) *bankAccount { + var m sync.Mutex + return &bankAccount{blnc, &m} } diff --git a/exercise6/problem3/problem3.go b/exercise6/problem3/problem3.go index b34b90bb..2f734b98 100644 --- a/exercise6/problem3/problem3.go +++ b/exercise6/problem3/problem3.go @@ -1,9 +1,24 @@ package problem3 +import "sync/atomic" + type counter struct { val int64 } +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) +} + func newCounter() *counter { return &counter{ val: 0, diff --git a/exercise6/problem4/problem4.go b/exercise6/problem4/problem4.go index 793449c9..4b855439 100644 --- a/exercise6/problem4/problem4.go +++ b/exercise6/problem4/problem4.go @@ -1,31 +1,40 @@ package problem4 import ( + "sync" "time" ) -func worker(id int, _ *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +func worker(id int, shoppingList *[]string, ch chan<- int, c *sync.Cond) { + defer c.L.Unlock() + c.L.Lock() + for len(*shoppingList) == 0 { + c.Wait() + } ch <- id } -func updateShopList(shoppingList *[]string) { +func updateShopList(shoppingList *[]string, c *sync.Cond) { time.Sleep(10 * time.Millisecond) - + defer c.L.Unlock() + c.L.Lock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + + c.Signal() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) + c := sync.NewCond(&sync.Mutex{}) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, notifier, c) time.Sleep(time.Millisecond) // order matters } - go updateShopList(shoppingList) + go updateShopList(shoppingList, c) return notifier } diff --git a/exercise6/problem5/problem5.go b/exercise6/problem5/problem5.go index 8e4a1703..63e91cf4 100644 --- a/exercise6/problem5/problem5.go +++ b/exercise6/problem5/problem5.go @@ -1,31 +1,41 @@ package problem5 import ( + "sync" "time" ) -func worker(id int, shoppingList *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +func worker(id int, shoppingList *[]string, ch chan<- int, c *sync.Cond) { + defer c.L.Unlock() + c.L.Lock() + for len(*shoppingList) == 0 { + c.Wait() + } ch <- id } -func updateShopList(shoppingList *[]string) { +func updateShopList(shoppingList *[]string, c *sync.Cond) { + defer c.L.Unlock() + c.L.Lock() time.Sleep(10 * time.Millisecond) *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + + c.Broadcast() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) + c := sync.NewCond(&sync.Mutex{}) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, notifier, c) time.Sleep(time.Millisecond) // order matters } - go updateShopList(shoppingList) + go updateShopList(shoppingList, c) return notifier } diff --git a/exercise6/problem6/problem6.go b/exercise6/problem6/problem6.go index 0c1122b9..09057a21 100644 --- a/exercise6/problem6/problem6.go +++ b/exercise6/problem6/problem6.go @@ -5,16 +5,12 @@ import ( ) func runTasks(init func()) { - var wg sync.WaitGroup + var once sync.Once for range 10 { - wg.Add(1) - go func() { - defer wg.Done() - - //TODO: modify so that load function gets called only once. + once.Do(func() { init() - }() + }) } - wg.Wait() + } diff --git a/exercise6/problem7/problem7.go b/exercise6/problem7/problem7.go index ef49497b..1e85dfe9 100644 --- a/exercise6/problem7/problem7.go +++ b/exercise6/problem7/problem7.go @@ -3,17 +3,28 @@ package problem7 import ( "fmt" "math/rand" + "sync" "time" ) func task() { start := time.Now() var t *time.Timer + m := sync.Mutex{} + + resetTimer := func() { + m.Lock() // Lock before accessing the timer + defer m.Unlock() + + // Reset the timer inside the critical section + t.Reset(randomDuration()) + } + t = time.AfterFunc( randomDuration(), func() { fmt.Println(time.Now().Sub(start)) - t.Reset(randomDuration()) + resetTimer() }, ) time.Sleep(5 * time.Second) diff --git a/exercise6/problem8/problem8.go b/exercise6/problem8/problem8.go index 949eb2d2..dd5172c9 100644 --- a/exercise6/problem8/problem8.go +++ b/exercise6/problem8/problem8.go @@ -1,3 +1,30 @@ package problem8 -func multiplex(chs []<-chan string) []string {} +import "sync" + +func multiplex(chs []<-chan string) []string { + var result []string + var wg sync.WaitGroup + cha := make(chan string) + + for _, ch := range chs { + wg.Add(1) + go func(ch <-chan string) { + defer wg.Done() + for value := range ch { + cha <- value + } + }(ch) + } + + go func() { + wg.Wait() + close(cha) + }() + + for val := range cha { + result = append(result, val) + } + + return result +} diff --git a/exercise7/blogging-platform/.env.examle b/exercise7/blogging-platform/.env.examle new file mode 100644 index 00000000..65a0ccfc --- /dev/null +++ b/exercise7/blogging-platform/.env.examle @@ -0,0 +1,6 @@ +API_PORT=8000 +DB_HOST=localhost +DB_PORT=54321 +DB_USER=posts +DB_PASSWORD=posts +DB_NAME=posts \ No newline at end of file diff --git a/exercise7/blogging-platform/.giignore b/exercise7/blogging-platform/.giignore new file mode 100644 index 00000000..666bbd38 --- /dev/null +++ b/exercise7/blogging-platform/.giignore @@ -0,0 +1,28 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# postgres local volume +db-data \ No newline at end of file diff --git a/exercise7/blogging-platform/Dockerfile b/exercise7/blogging-platform/Dockerfile new file mode 100644 index 00000000..92350cec --- /dev/null +++ b/exercise7/blogging-platform/Dockerfile @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +################################################################################ +# Create a stage for building the application. +ARG GO_VERSION=1.23 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage bind mounts to go.sum and go.mod to avoid having to copy them into +# the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.sum,target=go.sum \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x + +# This is the architecture you're building for, which is passed in by the builder. +# Placing it here allows the previous steps to be cached across architectures. +ARG TARGETARCH + +# Build the application. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage a bind mount to the current directory to avoid having to copy the +# source code into the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,target=. \ + CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . + +################################################################################ +# Create a new stage for running the application that contains the minimal +# runtime dependencies for the application. This often uses a different base +# image from the build stage where the necessary files are copied from the build +# stage. +# +# The example below uses the alpine image as the foundation for running the app. +# By specifying the "latest" tag, it will also use whatever happens to be the +# most recent version of that image when you build your Dockerfile. If +# reproducability is important, consider using a versioned tag +# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff). +FROM alpine:latest AS final + +# Install any runtime dependencies that are needed to run your application. +# Leverage a cache mount to /var/cache/apk/ to speed up subsequent builds. +RUN --mount=type=cache,target=/var/cache/apk \ + apk --update add \ + ca-certificates \ + tzdata \ + && \ + update-ca-certificates + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser +USER appuser + +# Copy the executable from the "build" stage. +COPY --from=build /bin/server /bin/ + +# Expose the port that the application listens on. +EXPOSE 80 + +# What the container should run when it is started. +ENTRYPOINT [ "/bin/server" ] \ No newline at end of file diff --git a/exercise7/blogging-platform/docker-compose.yml b/exercise7/blogging-platform/docker-compose.yml new file mode 100644 index 00000000..8897fcd1 --- /dev/null +++ b/exercise7/blogging-platform/docker-compose.yml @@ -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", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 +volumes: + db-data: + +# The commented out section below is an example of how to define a PostgreSQL +# database that your application can use. `depends_on` tells Docker Compose to +# start the database before your application. The `db-data` volume persists the +# database data between container restarts. The `db-password` secret is used +# to set the database password. You must create `db/password.txt` and add +# a password of your choosing to it before running `docker compose up`. \ No newline at end of file diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..a5fcea6f 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 + github.com/lib/pq v1.10.9 +) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..f5e81b88 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-20241126132414-a6474f9c9b52 h1:oGffEWFUfUhOkUU0xq6qqn5HgMZwJSgnb2breVbcisg= +github.com/talgat-ruby/exercises-go/exercise7/blogging-platform v0.0.0-20241126132414-a6474f9c9b52/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..c5012687 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/main.go @@ -0,0 +1,17 @@ +package handler + +import ( + "blogging-platform/internal/api/handler/posts" + "blogging-platform/internal/db" + "log/slog" +) + +type Handler struct { + *posts.Posts +} + +func New(logger *slog.Logger, db *db.DB) *Handler { + return &Handler{ + Posts: posts.New(logger, db), + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/posts/main.go b/exercise7/blogging-platform/internal/api/handler/posts/main.go new file mode 100644 index 00000000..956568fc --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/main.go @@ -0,0 +1,18 @@ +package posts + +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/posts/posts_create.go b/exercise7/blogging-platform/internal/api/handler/posts/posts_create.go new file mode 100644 index 00000000..e9dd7066 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/posts_create.go @@ -0,0 +1,33 @@ +package posts + +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/posts/posts_delete.go b/exercise7/blogging-platform/internal/api/handler/posts/posts_delete.go new file mode 100644 index 00000000..98d60c7e --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/posts_delete.go @@ -0,0 +1,37 @@ +package posts + +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/posts/posts_index.go b/exercise7/blogging-platform/internal/api/handler/posts/posts_index.go new file mode 100644 index 00000000..14b34a1b --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/posts_index.go @@ -0,0 +1,28 @@ +package posts + +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/posts/posts_show.go b/exercise7/blogging-platform/internal/api/handler/posts/posts_show.go new file mode 100644 index 00000000..1ceffd0e --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/posts_show.go @@ -0,0 +1,35 @@ +package posts + +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/posts/posts_update.go b/exercise7/blogging-platform/internal/api/handler/posts/posts_update.go new file mode 100644 index 00000000..861a08c5 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/posts/posts_update.go @@ -0,0 +1,46 @@ +package posts + +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..0ec29c20 --- /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 { + + handler := handler.New(logger.With("type", "handler"), db) + router := router.New(handler) + return &Api{ + logger: logger, + router: router, + } +} + +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 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/posts.go b/exercise7/blogging-platform/internal/api/router/posts.go new file mode 100644 index 00000000..0babaccc --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/posts.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/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..c4c59bb2 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/model.go @@ -0,0 +1,17 @@ +package post + +import ( + "time" + + "github.com/lib/pq" +) + +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/post_create.go b/exercise7/blogging-platform/internal/db/post/post_create.go new file mode 100644 index 00000000..4ff03550 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/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("method", "PostsShow") + + 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 save data to db", "error", err) + return nil, err + } + + return &result, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/post_delete.go b/exercise7/blogging-platform/internal/db/post/post_delete.go new file mode 100644 index 00000000..b7d3e930 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/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("method", "PostsShow") + + 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 data from 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/post_index.go b/exercise7/blogging-platform/internal/db/post/post_index.go new file mode 100644 index 00000000..bfaa5ed2 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/post_index.go @@ -0,0 +1,41 @@ +package post + +import ( + "context" + "database/sql" +) + +func (m *Post) PostsIndex(ctx context.Context, term string) ([]Model, error) { + log := m.logger.With("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 rows.Close() + + 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 retrieve data from db", "error", err) + return nil, err + } + models = append(models, model) + } + + return models, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/post_show.go b/exercise7/blogging-platform/internal/db/post/post_show.go new file mode 100644 index 00000000..a09fbba8 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/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("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 retrieve data from db", "error", err) + return nil, err + } + + return &model, nil +} diff --git a/exercise7/blogging-platform/internal/db/post/post_update.go b/exercise7/blogging-platform/internal/db/post/post_update.go new file mode 100644 index 00000000..f6af7d51 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/post/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("method", "PostsShow") + + 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 data", "error", err) + return nil, err + } + + return &result, nil +} diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 1ffa1477..b55d20c0 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -1,49 +1,24 @@ 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()) + ctx := context.Background() - // db - _, err := db.New() + db, 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, - ) + db.Init() + + api := api.NewApi(slog.With("service", "database"), db) + if err := api.Start(ctx); err != nil { panic(err) } - - go func() { - shutdown := make(chan os.Signal, 1) // Create channel to signify s signal being sent - signal.Notify(shutdown, os.Interrupt) // When an interrupt is sent, notify the channel - - sig := <-shutdown - slog.WarnContext(ctx, "signal received - shutting down...", "signal", sig) - - cancel() - }() } diff --git a/exercise7/blogging-platform/pkg/httputils/request/body.go b/exercise7/blogging-platform/pkg/httputils/request/body.go index 92d639f4..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 { diff --git a/internal/api/main.go b/internal/api/main.go new file mode 100644 index 00000000..778f64ec --- /dev/null +++ b/internal/api/main.go @@ -0,0 +1 @@ +package api diff --git a/internal/db/main.go b/internal/db/main.go new file mode 100644 index 00000000..3a49c63e --- /dev/null +++ b/internal/db/main.go @@ -0,0 +1 @@ +package db