From 54eae4c4c112a140dd052ea7b587b0157ad64919 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Tue, 29 Nov 2022 14:42:37 +0200 Subject: [PATCH 1/5] add code --- coolfacts/cmd/coolfacts_client/client.go | 60 +++++++++++++----------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index 5f6a802..cb20438 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -15,28 +15,19 @@ const ( ) type getFactsResponse struct { - // TODO: add fields - // This struct represent the createFact API response body of the server. - // We will decode the response into a variable of this struct type. - // Since the server response is json, we will use json decode method. - // For this be sure to add json tags on the struct. (https://gobyexample.com/json) - // The response body is: - // { - // "facts": [ - // { - // "image": "...", - // "description": "...", - // "createdAt": "...", - // } - // ... - // ] - // } - // + Facts []struct { + Image string `json:"image"` + Description string `json:"description"` + } `json:"facts"` } -func (r getFactsResponse) ToCoolFacts() []coolfact.Fact { - // TODO: implement - // loop over the response facts and convert them to the entity type []coolfact.Fact +func (r getFactsResponse) toCoolFacts() []coolfact.Fact { + coolfacts := make([]coolfact.Fact, len(r.Facts)) + for i, fact := range r.Facts { + coolfacts[i] = coolfact.Fact(fact) + } + + return coolfacts } type client struct { @@ -67,11 +58,21 @@ func (c *client) GetAllFacts() ([]coolfact.Fact, error) { } }() - // TODO: handle response - // this method returns *http.Response. - // - If response status code isn't 200 (http.StatusOK), you should read the error from the response. - // use method c.readError which is already implemented. - // - If the response is OK, use method readResponseGetFacts (which you will implement) to return the facts + if res.StatusCode != http.StatusOK { + errMessage, err := c.readError(res) + if err != nil { + return nil, fmt.Errorf("client.CreateFact: %s", err) + } + + return nil, fmt.Errorf("client.GetLastCreatedFact got an error from server. status: %d. error: %s", res.StatusCode, errMessage) + } + + getFactsRes, err := c.readResponseGetFacts(res) + if err != nil { + return nil, fmt.Errorf("client.GetLastCreatedFact: %s", err) + } + + return getFactsRes.toCoolFacts(), nil } type errorResponse struct { @@ -88,7 +89,10 @@ func (c *client) readError(res *http.Response) (string, error) { } func (c *client) readResponseGetFacts(res *http.Response) (getFactsResponse, error) { - // TODO: implement - decode the json response into the target - // Use variable of type getFactsResponse. - // Use json.NewDecoder(...).Decode(...) (unlike the decoding in readError method) + var factsRes getFactsResponse + if err := json.NewDecoder(res.Body).Decode(&factsRes); err != nil { + return getFactsResponse{}, fmt.Errorf("readResponseGetFacts failed to read response body: %v. \nbody string is: %s", err) + } + + return factsRes, nil } From 68fdb8c7489e4ec221593b5a3798cc4506f45bc1 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Tue, 29 Nov 2022 15:12:43 +0200 Subject: [PATCH 2/5] add doc --- coolfacts/docs/ex4-initial-client.md | 161 +++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 coolfacts/docs/ex4-initial-client.md diff --git a/coolfacts/docs/ex4-initial-client.md b/coolfacts/docs/ex4-initial-client.md new file mode 100644 index 0000000..49d0112 --- /dev/null +++ b/coolfacts/docs/ex4-initial-client.md @@ -0,0 +1,161 @@ +# Part 4 + +In this exercise, you will implement a client that we will use for calling our server. +The client will sit in another main package in our `cmd` folder, making our coolfacts Go program to have 2 application. One server and one client. + +The starting point +To get started, run this command to clone the necessary exercise materials in a convenient folder: +```commandline +$ git clone --branch v4-initial-client https://github.com/FTBpro/go-workshop.git +``` + +# The Goal +After completing the exercise, you will have a client application which will take one command from the terminal, to call the server and get all the facts. + +# **Getting Started** +Take a look around the program, you notice a new cmd application - `coolfacts_client`, with two files, `main.go` and `client.go` + +## Step 0 - Notice `main.go` +In this file you don't hva any TODO, but let's get over it so you will be familiar with what that's going on + +First, we initialize the client with our server endpoint: +```go +cl := NewClient(serverEndpoint) +``` + +Then we're waiting for an input from the client: +```go + for { + fmt.Print("> ") + input, err := reader.ReadString('\n') + // code omitted + + res, err := processCmd(cl, cmd, args) + // code omitted + } +} + +func processCmd(cl *client, cmd string, args []string) (string, error) { + switch cmd { + case "": + return "", nil + case commandGetAllFacts: + // code omitted + default: + return "", errors.New("unknown command") + } +} +``` + +Currently, there is only one command `commandGetAllFacts` which is a const for "getAllFacts". + +## Step 1 - client.go - implement the client +Take a look in the file and notice we have client struct and initializer. When the user will send an input `getAllFacts`, +the client's method `GetAllFacts` will be called, and the client will call the server getFacts API. You will implement the method `GetAllFacts` + +- Fill struct `getFactsResponse` + - This struct represents the JSON response from the client, and we will use it for deserializing the server's response-body into a convenient struct. Add fields corresponding to the response. Reminder, the response looks like this: + ```json + { + "facts": [ + { + "image": "...", + "description": "..." + } + //... + ] + } + ``` + - Example on JSON tags and some json package functionality - For this be sure to add json tags on the struct. (https://gobyexample.com/json) +- Implement `ToCoolFacts()` method. You will use this method for converting the server response to the entity. +- Implement `GetAllFacts`. + - Notice the start of the method, we are composing the url for get facts which is "127.0.0.1:9002/facts" and calling `http.Get(...)`. The clinet must read and close the response after using it, you can see it in the `defer` block. + - Finish implementing the method, handle the response as specified in the TODO in the code +- Implement `readResponseGetFacts` as specified in the TODO in the code. + +## Building and Running + +If everything is implemented well, this is what the final result should look like when running the program, in one tab we are running the server as we did in the previous exercise, and in another tab we are running the client, and writing the command for getting the facts: + +TODO:(oren) add gif + +## Full Walkthrough + +In the following section you fill find a full walkthrough. Use it in case you are stuck. + +## Step 1 - client.go + +We will add fields to the struct `getFactsResponse`: +```go +type getFactsResponse struct { + Facts []struct { + Image string `json:"image"` + Description string `json:"description"` + } `json:"facts"` +} +``` +As been said, this type represent the server JSON response. The json decoding, when receiving a struct, search for the json tags, to know how to format the fields. +Note that the field are exported (public), this is for the json package to be able to see them. + +We also implement method `toCoolFacts()`. We will use this method to convert the response to the entity the client needs to return. +```go +func (r getFactsResponse) toCoolFacts() []coolfact.Fact { + coolfacts := make([]coolfact.Fact, len(r.Facts)) + for i, fact := range r.Facts { + coolfacts[i] = coolfact.Fact(fact) + } + + return coolfacts +} +``` + +Finish implementing the method `GetAllFacts` - handling the response. In case the `StatusCode` is not `http.StatusOK`, we will read an error from the response and return it. Otherwise, we read the body and convert to our coolfact.Fact slice +```go +func (c *client) GetAllFacts() ([]coolfact.Fact, error) { + + // code emitted + + if res.StatusCode != http.StatusOK { + errMessage, err := c.readError(res) + if err != nil { + return nil, fmt.Errorf("client.CreateFact: %s", err) + } + + return nil, fmt.Errorf("client.GetLastCreatedFact got an error from server. status: %d. error: %s", res.StatusCode, errMessage) + } + + getFactsRes, err := c.readResponseGetFacts(res) + if err != nil { + return nil, fmt.Errorf("client.GetLastCreatedFact: %s", err) + } + + return getFactsRes.toCoolFacts(), nil +} +``` +Notice the method `readError` which is already implemented: +```go +func (c *client) readError(res *http.Response) (string, error) { + var errRes errorResponse + if err := json.NewDecoder(res.Body).Decode(&errRes); err != nil { + return "", fmt.Errorf("readBody failed to read response body: %v. \nbody string is: %s", err) + } + + return errRes.Error, nil +} +``` +We use json package to decode the response. We will do it in the same way in the method `readResponseGetFacts`: +```go +func (c *client) readResponseGetFacts(res *http.Response) (getFactsResponse, error) { + var factsRes getFactsResponse + if err := json.NewDecoder(res.Body).Decode(&factsRes); err != nil { + return getFactsResponse{}, fmt.Errorf("readResponseGetFacts failed to read response body: %v. \nbody string is: %s", err) + } + + return factsRes, nil +} +``` + +# Finish! +Congratulation! You've just implemented a client application, and when running alongside our server, you have request-response system! + +In the following exercise we will add an API for creating a fact! From 76fa4fda618d63f30598bf6a4320a518c7a0f5d4 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 10 Dec 2022 18:15:54 +0200 Subject: [PATCH 3/5] ff --- coolfacts/cmd/coolfacts_client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index aff7342..dc9ab5d 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -91,7 +91,7 @@ func (c *client) readError(res *http.Response) (string, error) { func (c *client) readResponseGetFacts(res *http.Response) (getFactsResponse, error) { var factsRes getFactsResponse if err := json.NewDecoder(res.Body).Decode(&factsRes); err != nil { - return getFactsResponse{}, fmt.Errorf("readResponseGetFacts failed to read response body: %v. \nbody string is: %s", err) + return getFactsResponse{}, fmt.Errorf("readResponseGetFacts failed to read response body: %v", err) } return factsRes, nil From ff93ca6b61912c4786316b72655c92eda50ecec1 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 10 Dec 2022 18:22:51 +0200 Subject: [PATCH 4/5] Add gif --- coolfacts/docs/ex4-initial-client.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/coolfacts/docs/ex4-initial-client.md b/coolfacts/docs/ex4-initial-client.md index 03e1ebc..cb4dc90 100644 --- a/coolfacts/docs/ex4-initial-client.md +++ b/coolfacts/docs/ex4-initial-client.md @@ -72,9 +72,10 @@ the client's method `GetAllFacts` will be called, and the client will call the s ## Building and Running -If everything is implemented well, this is what the final result should look like when running the program, in one tab we are running the server as we did in the previous exercise, and in another tab we are running the client, and writing the command for getting the facts: +If everything is implemented well, this is what the final result should look like when running the applications (in one tab we are running the server, and in the other one we are running the client): + +![v4-client](https://user-images.githubusercontent.com/5252381/206864714-deb7c295-b448-44e8-b867-824e1b5f0d39.gif) -TODO:(oren) add gif ## Full Walkthrough From 51bf87acfbb27e0073cdc6d6d1dffbf2b4c85fc9 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 10 Dec 2022 18:24:09 +0200 Subject: [PATCH 5/5] fix --- coolfacts/cmd/coolfacts_client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index dc9ab5d..e2c9c87 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -82,7 +82,7 @@ type errorResponse struct { func (c *client) readError(res *http.Response) (string, error) { var errRes errorResponse if err := json.NewDecoder(res.Body).Decode(&errRes); err != nil { - return "", fmt.Errorf("readBody failed to read response body: %v. \n", err) + return "", fmt.Errorf("readBody failed to read response body: %v", err) } return errRes.Error, nil