Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion coolfacts/cmd/coolfacts_client/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
Expand All @@ -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"`
}

Expand All @@ -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)
Expand Down Expand Up @@ -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"`
}
Expand Down
32 changes: 30 additions & 2 deletions coolfacts/cmd/coolfacts_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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")
}
Expand Down
3 changes: 3 additions & 0 deletions coolfacts/cmd/coolfacts_server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log"
"net/http"
"time"

"github.com/FTBpro/go-workshop/coolfacts/coolfact"
"github.com/FTBpro/go-workshop/coolfacts/inmem"
Expand All @@ -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),
},
}
}
34 changes: 25 additions & 9 deletions coolfacts/cmd/coolfacts_server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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 ...")

Expand Down Expand Up @@ -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
}
}

Expand Down
1 change: 1 addition & 0 deletions coolfacts/coolfact/fact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
7 changes: 7 additions & 0 deletions coolfacts/coolfact/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "fmt"

type Repository interface {
GetFacts() ([]Fact, error)
// TODO: add method createFact
}

type service struct {
Expand All @@ -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
}
67 changes: 67 additions & 0 deletions coolfacts/coolfact/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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)
Expand All @@ -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)
})
}
}
Expand All @@ -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)
}

Expand Down
13 changes: 13 additions & 0 deletions coolfacts/inmem/factsrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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