diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index 07c57a1..34da0ff 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "net/http" + "time" "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -18,9 +19,9 @@ const ( type getFactsResponse struct { Facts []struct { - Topic string `json:"topic"` - Description string `json:"description"` - // TODO: add a field for createdAt + Topic string `json:"topic"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` } `json:"facts"` } @@ -46,8 +47,16 @@ func NewClient(endpoint string) *client { } func (c *client) GetLastCreatedFact() (coolfact.Fact, error) { - // TODO: implement this method. - // Use the method GetFacts for getting all the facts + allFacts, err := c.GetFacts() + if err != nil { + return coolfact.Fact{}, fmt.Errorf("GetLastCreatedFact: %w", err) + } + + if len(allFacts) == 0 { + return coolfact.Fact{}, fmt.Errorf("fact not found") + } + + return allFacts[0], nil } func (c *client) GetFacts() ([]coolfact.Fact, error) { @@ -100,11 +109,33 @@ func (c *client) CreateFact(fact coolfact.Fact) error { } responseBody := bytes.NewBuffer(postBody) - // TODO: - // 1. create a new request. Use http.NewRequestWithContext. For argument use the ul and the responseBody. - // 2. Do the request using c.httpClient - // 3. As in GetFacts, in case of a failure (response status code is not 200), return error using readError - // * don't forget to close the body like we did in GetFacts method + req, err := http.NewRequest(http.MethodPost, ul, responseBody) + if err != nil { + return fmt.Errorf("client.CreateFact failed to create request: %v", err) + } + + res, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("client.CreateFact failed to do request: %v", err) + } + + defer func() { + if res != nil && res.Body != nil { + _, _ = io.Copy(ioutil.Discard, res.Body) + _ = res.Body.Close() + } + }() + + if res.StatusCode != http.StatusOK { + errMessage, err := c.readError(res) + if err != nil { + return fmt.Errorf("client.CreateFact: %s", err) + } + + return fmt.Errorf("client.CreateFact got an error from server. status: %d. error: %s", res.StatusCode, errMessage) + } + + return nil } type errorResponse struct { diff --git a/coolfacts/cmd/coolfacts_client/main.go b/coolfacts/cmd/coolfacts_client/main.go index 6fb6c70..a463fa9 100644 --- a/coolfacts/cmd/coolfacts_client/main.go +++ b/coolfacts/cmd/coolfacts_client/main.go @@ -8,13 +8,13 @@ import ( "os" "regexp" "strings" - + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) const ( serverEndpoint = "http://127.0.0.1:9002" - + commandGetFacts = "getFacts" createFactCommand = "createFact" commandGetLastFact = "getLastFact" @@ -22,9 +22,9 @@ const ( func main() { fmt.Println("Hello, Client!") - + cl := NewClient(serverEndpoint) - + reader := bufio.NewReader(os.Stdin) for { fmt.Print("> ") @@ -32,27 +32,27 @@ func main() { if err != nil { log.Fatal(err) } - + input = strings.Trim(input, "\n ") tokens := regexp.MustCompile("[ ]+").Split(input, -1) - + cmd, args := tokens[0], tokens[1:] if cmd == "exit" { fmt.Println("Bye, bye!") return } - + res, err := processCmd(cl, cmd, args) if err != nil { fmt.Println("ERROR:", err) continue } - + if res != "" { fmt.Println(res) } } - + } func processCmd(cl *client, cmd string, args []string) (string, error) { @@ -64,38 +64,38 @@ func processCmd(cl *client, cmd string, args []string) (string, error) { if err != nil { return "", err } - + var msg string for i, fact := range facts { msg += fmt.Sprintf("\n**************\nFact %d:", i) msg += fmt.Sprintf("\tTopic: %s\n\tDescription: %s\n\tCreatedAt: %s", fact.Topic, fact.Description, fact.CreatedAt) } - + return msg, nil case commandGetLastFact: lastFact, err := cl.GetLastCreatedFact() if err != nil { return "", err } - + return fmt.Sprintf("\tTopic: %s\n\tDescription: %s\n\tCreatedAt: %s", lastFact.Topic, lastFact.Description, lastFact.CreatedAt), nil case createFactCommand: if len(args) < 2 { return "", errors.New("invalid arguments") } - + fct := coolfact.Fact{ Topic: args[0], Description: strings.Join(args[1:], " "), } - + err := cl.CreateFact(fct) if err != nil { return "", fmt.Errorf("failed to create fact: %v", err) } - + return fmt.Sprintf(" ---> Fact created successfully"), nil - + default: return "", errors.New("unknown command") } diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index 70358e6..523f254 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -6,23 +6,28 @@ import ( "log" "net/http" "strings" + "time" "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) type FactsService interface { GetFacts() ([]coolfact.Fact, error) - // TODO: add method CreateFact + CreateFact(fact coolfact.Fact) error } -// TODO: add struct factRequest -// This struct should represent the client request for creating a new fact. -// The client sends JSON: -// { -// "topic": "...", -// "description": "..." -// } -// TODO: add method on this struct `ToCoolFact` that convert it into an entity coolfact.Fact +type createFactRequest struct { + Topic string `json:"topic"` + Description string `json:"description"` +} + +func (r createFactRequest) ToCoolFact() coolfact.Fact { + return coolfact.Fact{ + Topic: r.Topic, + Description: r.Description, + CreatedAt: time.Now(), + } +} type server struct { factsService FactsService @@ -37,10 +42,6 @@ func NewServer(factsService FactsService) *server { func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("incoming request", r.Method, r.URL.Path) - // TODO: add case to support the create fact API - // the expected path for creating a fact is "/paths", and the http method is POST (http.MethodPost) - // use server method HandleCreateFact - switch r.Method { case http.MethodGet: switch strings.ToLower(r.URL.Path) { @@ -51,6 +52,13 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.HandleNotFound(w, r) } + case http.MethodPost: + switch strings.ToLower(r.URL.Path) { + case "/facts": + s.HandleCreateFact(w, r) + default: + s.HandleNotFound(w, r) + } default: s.HandleNotFound(w, r) } @@ -92,11 +100,20 @@ func (s *server) HandleGetFacts(w http.ResponseWriter, _ *http.Request) { func (s *server) HandleCreateFact(w http.ResponseWriter, r *http.Request) { log.Println("Handling createFact ...") - // TODO: - // 1. Read the request body into factRequest - // Use json.NewDecoder and Decode - // 2. Call the service for creating a fact - // 3. On success return status OK + var request createFactRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + err = fmt.Errorf("server.HandleCreateFact failed to decode request: %s", err) + s.HandleError(w, err) + return + } + + if err := s.factsService.CreateFact(request.ToCoolFact()); err != nil { + err = fmt.Errorf("server.HandleCreateFact: %s", err) + s.HandleError(w, err) + return + } + + w.WriteHeader(http.StatusOK) } func (s *server) HandleNotFound(w http.ResponseWriter, r *http.Request) { @@ -134,7 +151,7 @@ func (s *server) formatGetFactsResponse(facts []coolfact.Fact) map[string]interf formattedFacts[i] = map[string]interface{}{ "topic": coolFact.Topic, "description": coolFact.Description, - // TODO: add created at to the response + "createdAt": coolFact.CreatedAt, } } diff --git a/coolfacts/coolfact/fact.go b/coolfacts/coolfact/fact.go index 1974d41..a764521 100644 --- a/coolfacts/coolfact/fact.go +++ b/coolfacts/coolfact/fact.go @@ -1,7 +1,9 @@ package coolfact +import "time" + type Fact struct { Topic string Description string - // TODO: add field CreatedAt from type time.Time. (time.Time represents an instant in time and have designated convenience methods) + CreatedAt time.Time } diff --git a/coolfacts/coolfact/service.go b/coolfacts/coolfact/service.go index 80b826e..9037ea0 100644 --- a/coolfacts/coolfact/service.go +++ b/coolfacts/coolfact/service.go @@ -4,7 +4,7 @@ import "fmt" type Repository interface { GetFacts() ([]Fact, error) - // TODO: add method createFact + CreateFact(fct Fact) error } type service struct { @@ -27,7 +27,9 @@ func (s *service) GetFacts() ([]Fact, error) { } func (s *service) CreateFact(fact Fact) error { - // TODO: implement CreateFact + if err := s.factsRepo.CreateFact(fact); err != nil { + return fmt.Errorf("factsService.CreateFact: %w", err) + } return nil } diff --git a/coolfacts/inmem/factsrepo.go b/coolfacts/inmem/factsrepo.go index 9e6cbae..56681fb 100644 --- a/coolfacts/inmem/factsrepo.go +++ b/coolfacts/inmem/factsrepo.go @@ -1,6 +1,8 @@ package inmem import ( + "sort" + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -15,18 +17,24 @@ func NewFactsRepository(facts ...coolfact.Fact) *factsRepo { } func (r *factsRepo) GetFacts() ([]coolfact.Fact, error) { - //TODO: before returning the facts, sort the facts according to the createdAt - // Use sort.Sort method with the slice byCreatedAt. The most recent facts will be return first. - // For explain on sort.Sort, see example: https://gobyexample.com/sorting-by-functions - // Check methods of time.Time to decide how can you check if one time is after another one. (https://pkg.go.dev/time#Time.Before) + sort.Sort(byCreatedAt(r.facts)) return r.facts, nil } func (r *factsRepo) CreateFact(fact coolfact.Fact) error { - // TODO: implement + r.facts = append(r.facts, fact) + return nil } type byCreatedAt []coolfact.Fact -// TODO: make type byCreatedAt implement sort.Interface. Example: https://gobyexample.com/sorting-by-functions +func (s byCreatedAt) Len() int { + return len(s) +} +func (s byCreatedAt) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s byCreatedAt) Less(i, j int) bool { + return s[i].CreatedAt.After(s[j].CreatedAt) +}