diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index e2c9c87..07c57a1 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "io" @@ -11,13 +12,15 @@ import ( ) const ( - pathGetFacts = "/facts" + pathGetFacts = "/facts" + pathCreateFact = "/facts" ) type getFactsResponse struct { Facts []struct { Topic string `json:"topic"` Description string `json:"description"` + // TODO: add a field for createdAt } `json:"facts"` } @@ -42,6 +45,11 @@ 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 +} + func (c *client) GetFacts() ([]coolfact.Fact, error) { ul := c.endpoint + pathGetFacts res, err := c.httpClient.Get(ul) @@ -75,6 +83,30 @@ func (c *client) GetFacts() ([]coolfact.Fact, error) { return getFactsRes.toCoolFacts(), nil } +func (c *client) CreateFact(fact coolfact.Fact) error { + ul := c.endpoint + pathCreateFact + + // First we are preparing the payload + payload := map[string]interface{}{ + "topic": fact.Topic, + "description": fact.Description, + } + + // we need io.Reader to create a new http request. + // we will create bytes.Buffer which implement this interface + postBody, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("client.CreateFact failed to marshal payload: %v", err) + } + 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 +} + type errorResponse struct { Error string `json:"error"` } diff --git a/coolfacts/cmd/coolfacts_client/main.go b/coolfacts/cmd/coolfacts_client/main.go index fa7fbcd..6fb6c70 100644 --- a/coolfacts/cmd/coolfacts_client/main.go +++ b/coolfacts/cmd/coolfacts_client/main.go @@ -8,12 +8,16 @@ import ( "os" "regexp" "strings" + + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) const ( serverEndpoint = "http://127.0.0.1:9002" - commandGetFacts = "getFacts" + commandGetFacts = "getFacts" + createFactCommand = "createFact" + commandGetLastFact = "getLastFact" ) func main() { @@ -64,10 +68,34 @@ func processCmd(cl *client, cmd string, args []string) (string, error) { var msg string for i, fact := range facts { msg += fmt.Sprintf("\n**************\nFact %d:", i) - msg += fmt.Sprintf("\tTopic: %s\n\tDescription: %s\n", fact.Topic, fact.Description) + 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/main.go b/coolfacts/cmd/coolfacts_server/main.go index 13d8ca4..a5be929 100644 --- a/coolfacts/cmd/coolfacts_server/main.go +++ b/coolfacts/cmd/coolfacts_server/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "time" "github.com/FTBpro/go-workshop/coolfacts/coolfact" "github.com/FTBpro/go-workshop/coolfacts/inmem" @@ -28,10 +29,12 @@ func seedFacts() []coolfact.Fact { { Topic: "Games", Description: "Did you know sonic is a hedgehog?!", + CreatedAt: time.Now(), }, { Topic: "TV", Description: "You won't believe what happened to Arya!", + CreatedAt: time.Now().Add(-time.Duration(1) * time.Hour), }, } } diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index 6a2c439..70358e6 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -12,8 +12,18 @@ import ( type FactsService interface { GetFacts() ([]coolfact.Fact, error) + // TODO: add method CreateFact } +// 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 server struct { factsService FactsService } @@ -27,6 +37,10 @@ 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) { @@ -62,15 +76,6 @@ func (s *server) HandleGetFacts(w http.ResponseWriter, _ *http.Request) { return } - // we first format the facts to map[string]interface. - formattedFacts := make([]map[string]interface{}, len(facts)) - for i, coolFact := range facts { - formattedFacts[i] = map[string]interface{}{ - "topic": coolFact.Topic, - "description": coolFact.Description, - } - } - response := s.formatGetFactsResponse(facts) // write status and content-type @@ -84,6 +89,16 @@ 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 +} + func (s *server) HandleNotFound(w http.ResponseWriter, r *http.Request) { log.Println("Handling notFound ...") @@ -119,6 +134,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 } } diff --git a/coolfacts/coolfact/fact.go b/coolfacts/coolfact/fact.go index d83e895..1974d41 100644 --- a/coolfacts/coolfact/fact.go +++ b/coolfacts/coolfact/fact.go @@ -3,4 +3,5 @@ package coolfact 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) } diff --git a/coolfacts/coolfact/service.go b/coolfacts/coolfact/service.go index 15711ff..80b826e 100644 --- a/coolfacts/coolfact/service.go +++ b/coolfacts/coolfact/service.go @@ -4,6 +4,7 @@ import "fmt" type Repository interface { GetFacts() ([]Fact, error) + // TODO: add method createFact } type service struct { @@ -24,3 +25,9 @@ func (s *service) GetFacts() ([]Fact, error) { return facts, nil } + +func (s *service) CreateFact(fact Fact) error { + // TODO: implement CreateFact + + return nil +} diff --git a/coolfacts/coolfact/service_test.go b/coolfacts/coolfact/service_test.go index b1bd197..6ce2390 100644 --- a/coolfacts/coolfact/service_test.go +++ b/coolfacts/coolfact/service_test.go @@ -27,6 +27,12 @@ func Test_service_GetFacts(t *testing.T) { want: facts, wantErr: false, }, + { + name: "add unsorted facts", + repoField: inmem.NewFactsRepository(facts[5], facts[4], facts[2]), + want: []coolfact.Fact{facts[2], facts[4], facts[5]}, + wantErr: false, + }, { name: "no facts - should get nil", repoField: inmem.NewFactsRepository(), @@ -40,6 +46,7 @@ func Test_service_GetFacts(t *testing.T) { wantErr: true, }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := coolfact.NewService(tc.repoField) @@ -51,6 +58,65 @@ func Test_service_GetFacts(t *testing.T) { require.False(t, tc.wantErr, "expected an error but didn't receive one.") expectEqualFacts(t, tc.want, got) + + }) + } +} + +func Test_service_CreateFact(t *testing.T) { + facts := generateRandomFactsDesc(10) + + tests := []struct { + name string + repoField coolfact.Repository + factsToCreate []coolfact.Fact + want []coolfact.Fact + wantErr bool + }{ + { + name: "base case - adding sorted", + repoField: inmem.NewFactsRepository(facts[5]), + factsToCreate: []coolfact.Fact{facts[2]}, + want: []coolfact.Fact{facts[2], facts[5]}, + }, + { + name: "add fact from the past", + repoField: inmem.NewFactsRepository(facts[3]), + factsToCreate: []coolfact.Fact{facts[5]}, + want: []coolfact.Fact{facts[3], facts[5]}, + }, + { + name: "add many mixed facts", + repoField: inmem.NewFactsRepository(facts[3], facts[5], facts[1], facts[9]), + factsToCreate: []coolfact.Fact{facts[4], facts[2], facts[0]}, + want: []coolfact.Fact{facts[0], facts[1], facts[2], facts[3], facts[4], facts[5], facts[9]}, + }, + { + name: "repo returns error", + repoField: mockRepoError{}, + factsToCreate: []coolfact.Fact{facts[4], facts[2], facts[0]}, + want: nil, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + s := coolfact.NewService(tc.repoField) + for _, fact := range tc.factsToCreate { + err := s.CreateFact(fact) + if err != nil { + require.True(t, tc.wantErr, "got an unexpected error from service") + return + } + + require.False(t, tc.wantErr, "expected an error but didn't receive one.") + return + } + + gotFacts, err := s.GetFacts() + require.NoError(t, err) + require.Equal(t, gotFacts, tc.want) }) } } @@ -60,6 +126,7 @@ func generateRandomFactsDesc(n int) []coolfact.Fact { var facts []coolfact.Fact for i := 0; i < n; i++ { fact := randomFact() + fact.CreatedAt = time.Now().Add(-(time.Duration(i) * time.Hour)) facts = append(facts, fact) } diff --git a/coolfacts/inmem/factsrepo.go b/coolfacts/inmem/factsrepo.go index f879c95..9e6cbae 100644 --- a/coolfacts/inmem/factsrepo.go +++ b/coolfacts/inmem/factsrepo.go @@ -15,5 +15,18 @@ 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) + return r.facts, nil } + +func (r *factsRepo) CreateFact(fact coolfact.Fact) error { + // TODO: implement +} + +type byCreatedAt []coolfact.Fact + +// TODO: make type byCreatedAt implement sort.Interface. Example: https://gobyexample.com/sorting-by-functions