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
51 changes: 41 additions & 10 deletions coolfacts/cmd/coolfacts_client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"io/ioutil"
"net/http"
"time"

"github.com/FTBpro/go-workshop/coolfacts/coolfact"
)
Expand All @@ -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"`
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
32 changes: 16 additions & 16 deletions coolfacts/cmd/coolfacts_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,51 @@ 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"
)

func main() {
fmt.Println("Hello, Client!")

cl := NewClient(serverEndpoint)

reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("> ")
input, err := reader.ReadString('\n')
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) {
Expand All @@ -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")
}
Expand Down
55 changes: 36 additions & 19 deletions coolfacts/cmd/coolfacts_server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
}
}

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

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

type service struct {
Expand All @@ -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
}
20 changes: 14 additions & 6 deletions coolfacts/inmem/factsrepo.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package inmem

import (
"sort"

"github.com/FTBpro/go-workshop/coolfacts/coolfact"
)

Expand All @@ -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)
}