├── .github
└── workflows
│ └── golint_test.yml
├── .gitignore
├── README.md
├── chap1
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── go.mod
│ ├── main.go
│ ├── main_test.go
│ ├── parse_args_test.go
│ ├── run_cmd_test.go
│ └── validate_args_test.go
└── exercise2
│ ├── README.md
│ ├── go.mod
│ ├── main.go
│ ├── output.html
│ ├── parse_args_test.go
│ ├── run_cmd_test.go
│ └── validate_args_test.go
├── chap10
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── client
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── main.go
│ ├── server-tls
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── health_test.go
│ │ ├── server.go
│ │ ├── server_test.go
│ │ └── test_utils.go
│ ├── server
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── health_test.go
│ │ ├── server.go
│ │ ├── server_test.go
│ │ └── test_utils.go
│ ├── service
│ │ ├── go.mod
│ │ ├── users.pb.go
│ │ ├── users.proto
│ │ └── users_grpc.pb.go
│ └── tls
│ │ ├── generate_certs.bash
│ │ ├── server.crt
│ │ └── server.key
└── exercise2
│ ├── README.md
│ ├── server
│ ├── go.mod
│ ├── go.sum
│ ├── health_test.go
│ ├── server.go
│ ├── server_test.go
│ └── test_utils.go
│ └── service
│ ├── go.mod
│ ├── users.pb.go
│ ├── users.proto
│ └── users_grpc.pb.go
├── chap11
├── .emptyfile
├── exercise1
│ ├── LotsOfFiles.tgz
│ ├── README.md
│ ├── go.mod
│ ├── go.sum
│ ├── handlers.go
│ ├── localsetup_minio.sh
│ ├── package_get_handler_test.go
│ ├── package_reg_handler_test.go
│ ├── server.go
│ ├── test_utils.go
│ ├── types.go
│ └── upload_package.go
└── exercise2
│ ├── README.md
│ ├── db_store.go
│ ├── go.mod
│ ├── go.sum
│ ├── handlers.go
│ ├── image.tgz
│ ├── image1.png
│ ├── localsetup_minio.sh
│ ├── localsetup_mysql.sh
│ ├── main_test.go
│ ├── mysql-init
│ ├── 01-create-table.sql
│ └── 02-insert-data.sql
│ ├── package_download_handler_test.go
│ ├── package_query_handler_test.go
│ ├── package_reg_handler_test.go
│ ├── query_db_test.go
│ ├── server.go
│ ├── test_utils.go
│ ├── types.go
│ └── upload_package.go
├── chap2
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ └── main_test.go
├── exercise2
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ └── main_test.go
└── exercise3
│ ├── README.md
│ ├── go.mod
│ ├── input_timeout_test.go
│ └── main.go
├── chap3
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ └── main_test.go
├── exercise2
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ └── main_test.go
├── exercise3
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ └── test.json
└── exercise4
│ ├── README.md
│ ├── cmd
│ ├── errors.go
│ ├── grpcCmd.go
│ ├── handle_grpc_test.go
│ ├── handle_http_test.go
│ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ └── testdata
│ ├── expectedGolden.0
│ ├── expectedGolden.1
│ ├── expectedGolden.2
│ └── expectedGolden.cmd.httpCmdUsage
├── chap4
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ └── test.json
├── exercise2
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ └── test.json
├── exercise3
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── handle_grpc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── godev.output
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ ├── middleware
│ │ └── httpLatency.go
│ └── test.json
└── exercise4
│ ├── README.md
│ ├── cmd
│ ├── errors.go
│ ├── grpcCmd.go
│ ├── handle_grpc_test.go
│ ├── handle_http_test.go
│ └── httpCmd.go
│ ├── go.mod
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ ├── middleware
│ └── httpLatency.go
│ └── test.json
├── chap5
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── go.mod
│ ├── server.go
│ └── server_test.go
├── exercise2
│ ├── README.md
│ ├── go.mod
│ ├── server.go
│ └── server_test.go
└── exercise3
│ ├── README.md
│ ├── go.mod
│ └── server.go
├── chap6
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── go.mod
│ └── server.go
├── exercise2
│ ├── README.md
│ ├── go.mod
│ ├── go.sum
│ └── server.go
└── exercise3
│ ├── README.md
│ ├── config
│ └── config.go
│ ├── go.mod
│ ├── handlers
│ ├── handlers.go
│ ├── handlers_test.go
│ └── register.go
│ ├── middleware
│ ├── middleware.go
│ ├── middleware_test.go
│ └── register.go
│ ├── server.go
│ └── server_test.go
├── chap7
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── go.mod
│ └── server.go
└── exercise2
│ ├── README.md
│ ├── go.mod
│ ├── server.go
│ └── server_test.go
├── chap8
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── cmd
│ │ ├── errors.go
│ │ ├── grpcCmd.go
│ │ ├── grpc_flag_parsing_test.go
│ │ ├── handle_grpc_cmd_test.go
│ │ ├── handle_grpc_users_svc_test.go
│ │ ├── handle_http_test.go
│ │ └── httpCmd.go
│ ├── go.mod
│ ├── go.sum
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ ├── middleware
│ │ └── httpLatency.go
│ └── service
│ │ ├── go.mod
│ │ ├── users.pb.go
│ │ ├── users.proto
│ │ └── users_grpc.pb.go
├── exercise2
│ ├── README.md
│ ├── client-v2
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── main.go
│ │ └── user_client_test.go
│ ├── client
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── main.go
│ │ └── user_client_test.go
│ ├── server
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── server.go
│ │ └── server_test.go
│ ├── service-v1
│ │ ├── go.mod
│ │ ├── users.pb.go
│ │ ├── users.proto
│ │ └── users_grpc.pb.go
│ └── service-v2
│ │ ├── go.mod
│ │ ├── users.pb.go
│ │ ├── users.proto
│ │ └── users_grpc.pb.go
└── exercise3
│ ├── README.md
│ ├── cmd
│ ├── errors.go
│ ├── grpcCmd.go
│ ├── grpc_flag_parsing_test.go
│ ├── handle_grpc_cmd_test.go
│ ├── handle_grpc_repos_svc_test.go
│ ├── handle_grpc_users_svc_test.go
│ ├── handle_http_test.go
│ ├── httpCmd.go
│ └── test_utils.go
│ ├── go.mod
│ ├── go.sum
│ ├── handle_command_test.go
│ ├── main.go
│ ├── main_test.go
│ ├── middleware
│ └── httpLatency.go
│ └── service
│ ├── go.mod
│ ├── repositories.pb.go
│ ├── repositories.proto
│ ├── repositories_grpc.pb.go
│ ├── users.pb.go
│ ├── users.proto
│ └── users_grpc.pb.go
├── chap9
├── .emptyfile
├── exercise1
│ ├── README.md
│ ├── server
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── server.go
│ │ └── server_test.go
│ └── service
│ │ ├── go.mod
│ │ ├── repositories.pb.go
│ │ ├── repositories.proto
│ │ ├── repositories_grpc.pb.go
│ │ ├── users.pb.go
│ │ ├── users.proto
│ │ └── users_grpc.pb.go
├── exercise2
│ ├── README.md
│ ├── client
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── main.go
│ ├── server
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── server.go
│ │ └── server_test.go
│ └── service
│ │ ├── go.mod
│ │ ├── repositories.pb.go
│ │ ├── repositories.proto
│ │ └── repositories_grpc.pb.go
└── exercise3
│ ├── README.md
│ ├── client
│ ├── go.mod
│ ├── go.sum
│ ├── interceptors.go
│ ├── main.go
│ └── user_client_test.go
│ ├── server
│ ├── go.mod
│ ├── go.sum
│ ├── interceptors.go
│ ├── server.go
│ └── server_test.go
│ └── service
│ ├── go.mod
│ ├── users.pb.go
│ ├── users.proto
│ └── users_grpc.pb.go
├── errors.md
└── housekeeping
├── build.sh
├── check_binaries.sh
├── check_consistency.sh
├── copy_code.sh
├── git_commit_push.sh
├── golint.py
├── gotest.py
├── govet.py
└── show_coverage.sh
/.github/workflows/golint_test.yml:
--------------------------------------------------------------------------------
1 | name: Static Analysis and Test
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | lint:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 | steps:
15 |
16 | - name: Set up Go 1.16
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: ^1.16
20 | id: go
21 |
22 | - name: Check out code into the Go module directory
23 | uses: actions/checkout@v2
24 |
25 | - name: Run golint
26 | run: |
27 | go get -u golang.org/x/lint/golint
28 | python housekeeping/golint.py .
29 | vet:
30 | name: Vet
31 | runs-on: ubuntu-latest
32 | steps:
33 |
34 | - name: Set up Go 1.16
35 | uses: actions/setup-go@v2
36 | with:
37 | go-version: ^1.16
38 | id: go
39 |
40 | - name: Check out code into the Go module directory
41 | uses: actions/checkout@v2
42 |
43 | - name: Run govet
44 | run: |
45 | python housekeeping/govet.py .
46 |
47 | test:
48 | name: Test
49 | runs-on: ubuntu-latest
50 | steps:
51 |
52 | - name: Set up Go 1.16
53 | uses: actions/setup-go@v2
54 | with:
55 | go-version: ^1.16
56 | id: go
57 |
58 | - name: Check out code into the Go module directory
59 | uses: actions/checkout@v2
60 |
61 | - name: Run tests
62 | run: |
63 | python housekeeping/gotest.py .
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *exe
2 | .empty
3 | gotOutput*
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for exercises
2 |
3 | This repository contains solutions for the exercises in the book [Practical Go - Building Scalable Network and Non-Network Applications](https://practicalgobook.net/).
4 |
--------------------------------------------------------------------------------
/chap1/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap1/.emptyfile
--------------------------------------------------------------------------------
/chap1/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 1.1
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Copy all the code from chap1/manual-parse
6 | 2. Create a new file, `main_test.go`
7 | 3. Create a `TestMain()` function where I build the application, run the test functions and remove the built application once done
8 | 4. In the test function, i then setup test configurations which will invoke the built application with the specified
9 | command line arguments and/or input and verify the exit code as well as the expected output.
10 |
11 | Note that when verifying the expected output, I verify the lines of output expected instead of the entire
12 | output.
--------------------------------------------------------------------------------
/chap1/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap1/exercise1
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap1/exercise1/parse_args_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | )
7 |
8 | func TestParseArgs(t *testing.T) {
9 | type testConfig struct {
10 | args []string
11 | err error
12 | config
13 | }
14 | tests := []testConfig{
15 | {
16 | args: []string{"-h"},
17 | err: nil,
18 | config: config{printUsage: true, numTimes: 0},
19 | },
20 | {
21 | args: []string{"10"},
22 | err: nil,
23 | config: config{printUsage: false, numTimes: 10},
24 | },
25 | {
26 | args: []string{"abc"},
27 | err: errors.New("strconv.Atoi: parsing \"abc\": invalid syntax"),
28 | config: config{printUsage: false, numTimes: 0},
29 | },
30 | {
31 | args: []string{"1", "foo"},
32 | err: errors.New("Invalid number of arguments"),
33 | config: config{printUsage: false, numTimes: 0},
34 | },
35 | }
36 |
37 | for _, tc := range tests {
38 | c, err := parseArgs(tc.args)
39 | if tc.err != nil && err.Error() != tc.err.Error() {
40 | t.Fatalf("Expected error to be: %v, got: %v\n", tc.err, err)
41 | }
42 | if tc.err == nil && err != nil {
43 | t.Fatalf("Expected nil error, got: %v\n", err)
44 | }
45 | if c.printUsage != tc.printUsage {
46 | t.Errorf("Expected printUsage to be: %v, got: %v\n", tc.printUsage, c.printUsage)
47 | }
48 | if c.numTimes != tc.numTimes {
49 | t.Errorf("Expected numTimes to be: %v, got: %v\n", tc.numTimes, c.numTimes)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/chap1/exercise1/run_cmd_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func TestRunCmd(t *testing.T) {
11 |
12 | tests := []struct {
13 | c config
14 | input string
15 | output string
16 | err error
17 | }{
18 | {
19 | c: config{printUsage: true},
20 | output: usageString,
21 | },
22 | {
23 | c: config{numTimes: 5},
24 | input: "",
25 | output: strings.Repeat("Your name please? Press the Enter key when done.\n", 1),
26 | err: errors.New("You didn't enter your name"),
27 | },
28 | {
29 | c: config{numTimes: 5},
30 | input: "Bill Bryson",
31 | output: "Your name please? Press the Enter key when done.\n" + strings.Repeat("Nice to meet you Bill Bryson\n", 5),
32 | },
33 | }
34 | byteBuf := new(bytes.Buffer)
35 | for _, tc := range tests {
36 | r := strings.NewReader(tc.input)
37 | err := runCmd(r, byteBuf, tc.c)
38 | if err != nil && tc.err == nil {
39 | t.Fatalf("Expected nil error, got: %v\n", err)
40 | }
41 | if tc.err != nil {
42 | if err.Error() != tc.err.Error() {
43 | t.Fatalf("Expected error: %v, Got error: %v\n", tc.err.Error(), err.Error())
44 | }
45 | }
46 | gotMsg := byteBuf.String()
47 | if gotMsg != tc.output {
48 | t.Errorf("Expected stdout message to be: %v, Got: %v\n", tc.output, gotMsg)
49 | }
50 |
51 | byteBuf.Reset()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/chap1/exercise1/validate_args_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | )
7 |
8 | func TestValidateArgs(t *testing.T) {
9 | tests := []struct {
10 | c config
11 | err error
12 | }{
13 | {
14 | c: config{},
15 | err: errors.New("Must specify a number greater than 0"),
16 | },
17 | {
18 | c: config{numTimes: -1},
19 | err: errors.New("Must specify a number greater than 0"),
20 | },
21 | {
22 | c: config{numTimes: 10},
23 | err: nil,
24 | },
25 | }
26 |
27 | for _, tc := range tests {
28 | err := validateArgs(tc.c)
29 | if tc.err != nil && err.Error() != tc.err.Error() {
30 | t.Errorf("Expected error to be: %v, got: %v\n", tc.err, err)
31 | }
32 | if tc.err == nil && err != nil {
33 | t.Errorf("Expected nil error, got: %v\n", err)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/chap1/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 1.2
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Copy all the code from chap1/flag-parse
6 | 2. Add a new option, `-o` to the `parseArgs()` function
7 | 3. Update validation code to accept num times as 0 when an output path is specified
8 | 4. Update tests for `parseArgs()`, `validateArgs()` and `runCmd()` functions
9 |
--------------------------------------------------------------------------------
/chap1/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap1/exercise2
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap1/exercise2/output.html:
--------------------------------------------------------------------------------
1 |
Hello jane
--------------------------------------------------------------------------------
/chap1/exercise2/parse_args_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestParseArgs(t *testing.T) {
10 | tests := []struct {
11 | args []string
12 | err error
13 | numTimes int
14 | outputHtmlPath string
15 | }{
16 | {
17 | args: []string{"-h"},
18 | err: errors.New("flag: help requested"),
19 | numTimes: 0,
20 | },
21 | {
22 | args: []string{"-n", "10"},
23 | err: nil,
24 | numTimes: 10,
25 | },
26 | {
27 | args: []string{"-n", "10", "-o", "output.html"},
28 | err: nil,
29 | numTimes: 10,
30 | outputHtmlPath: "output.html",
31 | },
32 | {
33 | args: []string{"-n", "abc"},
34 | err: errors.New("invalid value \"abc\" for flag -n: parse error"),
35 | numTimes: 0,
36 | },
37 | {
38 | args: []string{"-n", "10", "-o"},
39 | err: errors.New("flag needs an argument: -o"),
40 | numTimes: 10,
41 | },
42 | {
43 | args: []string{"-n", "1", "foo"},
44 | err: errors.New("Positional arguments specified"),
45 | numTimes: 1,
46 | },
47 | }
48 |
49 | byteBuf := new(bytes.Buffer)
50 | for _, tc := range tests {
51 | c, err := parseArgs(byteBuf, tc.args)
52 | if tc.err == nil && err != nil {
53 | t.Errorf("Expected nil error, got: %v\n", err)
54 | }
55 | if tc.err != nil && err.Error() != tc.err.Error() {
56 | t.Errorf("Expected error to be: %v, got: %v\n", tc.err, err)
57 | }
58 |
59 | if c.numTimes != tc.numTimes {
60 | t.Errorf("Expected numTimes to be: %v, got: %v\n", tc.numTimes, c.numTimes)
61 | }
62 |
63 | if len(tc.outputHtmlPath) != 0 && c.outputHtmlPath != tc.outputHtmlPath {
64 | t.Errorf("Expected outputHtmlPath to be: %v, got: %v\n", tc.outputHtmlPath, c.outputHtmlPath)
65 | }
66 | byteBuf.Reset()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/chap1/exercise2/validate_args_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | )
7 |
8 | func TestValidateArgs(t *testing.T) {
9 | tests := []struct {
10 | c config
11 | err error
12 | }{
13 | {
14 | c: config{},
15 | err: errors.New("Must specify a number greater than 0"),
16 | },
17 | {
18 | c: config{outputHtmlPath: "output.html"},
19 | err: nil,
20 | },
21 | {
22 | c: config{numTimes: -1},
23 | err: errors.New("Must specify a number greater than 0"),
24 | },
25 | {
26 | c: config{numTimes: 10},
27 | err: nil,
28 | },
29 | }
30 |
31 | for _, tc := range tests {
32 | err := validateArgs(tc.c)
33 | if tc.err != nil && err.Error() != tc.err.Error() {
34 | t.Errorf("Expected error to be: %v, got: %v\n", tc.err, err)
35 | }
36 | if tc.err == nil && err != nil {
37 | t.Errorf("Expected nil error, got: %v\n", err)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/chap10/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap10/.emptyfile
--------------------------------------------------------------------------------
/chap10/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Workflow for solution to Exercise 10.1
2 |
3 | - Copy the `server` and `service` directory from the code listing of Chapter 10, `chap10/server-healthcheck`
4 | - Create a copy of the `server` directory, `server-tls` to initialize a TLS enabled server
5 | - Copy the `tls` directory from the code listing of Chapter 10, `chap10/user-service-tls`
6 |
7 | - Create a new directory, `client` and initialize a new module inside it
8 | - Create the command line client, borrowing/copying code from the test for the server healthcheck
9 | - Update the `go.mod` of the client to contain:
10 | `replace github.com/practicalgo/code/chap10/server-healthcheck/service => ../service`
11 |
12 | - Specify the TLS certificate to the client using the `TLS_CERT_FILE_PATH` environment variable.
13 | If that is specified, the client will attempt to create a TLS encrypted connection
14 |
15 | ## Behavior of the client
16 |
17 | - If the healthcheck is successful for the `Check` method, it will print the status, else it will print the error
18 | - For the `Watch` method, the client will continue running till it gets a non-successful status or an error
--------------------------------------------------------------------------------
/chap10/exercise1/client/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap10/exercise1
2 |
3 | go 1.16
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | replace github.com/practicalgo/code/chap10/server-healthcheck/service => ../service
8 |
--------------------------------------------------------------------------------
/chap10/exercise1/server-tls/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap10/server-healthcheck/server
2 |
3 | go 1.16
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require github.com/practicalgo/code/chap10/server-healthcheck/service v0.0.0
8 |
9 | replace github.com/practicalgo/code/chap10/server-healthcheck/service => ../service
10 |
--------------------------------------------------------------------------------
/chap10/exercise1/server-tls/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net"
6 | "testing"
7 |
8 | users "github.com/practicalgo/code/chap10/server-healthcheck/service"
9 | "google.golang.org/grpc"
10 | )
11 |
12 | func TestUserService(t *testing.T) {
13 |
14 | l := startTestGrpcServer()
15 |
16 | bufconnDialer := func(
17 | ctx context.Context, addr string,
18 | ) (net.Conn, error) {
19 | return l.Dial()
20 | }
21 |
22 | client, err := grpc.DialContext(
23 | context.Background(),
24 | "", grpc.WithInsecure(),
25 | grpc.WithContextDialer(bufconnDialer),
26 | )
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 | usersClient := users.NewUsersClient(client)
31 | resp, err := usersClient.GetUser(
32 | context.Background(),
33 | &users.UserGetRequest{
34 | Email: "jane@doe.com",
35 | Id: "foo-bar",
36 | },
37 | )
38 |
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | if resp.User.FirstName != "jane" {
43 | t.Errorf(
44 | "Expected FirstName to be: jane, Got: %s",
45 | resp.User.FirstName,
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/chap10/exercise1/server-tls/test_utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | svc "github.com/practicalgo/code/chap10/server-healthcheck/service"
7 | "google.golang.org/grpc"
8 | healthz "google.golang.org/grpc/health"
9 | healthsvc "google.golang.org/grpc/health/grpc_health_v1"
10 | "google.golang.org/grpc/test/bufconn"
11 | )
12 |
13 | var h *healthz.Server
14 |
15 | func startTestGrpcServer() *bufconn.Listener {
16 | h = healthz.NewServer()
17 | l := bufconn.Listen(10)
18 | s := grpc.NewServer()
19 | registerServices(s, h)
20 | updateServiceHealth(
21 | h,
22 | svc.Users_ServiceDesc.ServiceName,
23 | healthsvc.HealthCheckResponse_SERVING,
24 | )
25 | go func() {
26 | log.Fatal(startServer(s, l))
27 | }()
28 | return l
29 | }
30 |
--------------------------------------------------------------------------------
/chap10/exercise1/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap10/server-healthcheck/server
2 |
3 | go 1.16
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require github.com/practicalgo/code/chap10/server-healthcheck/service v0.0.0
8 |
9 | replace github.com/practicalgo/code/chap10/server-healthcheck/service => ../service
10 |
--------------------------------------------------------------------------------
/chap10/exercise1/server/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net"
6 | "testing"
7 |
8 | users "github.com/practicalgo/code/chap10/server-healthcheck/service"
9 | "google.golang.org/grpc"
10 | )
11 |
12 | func TestUserService(t *testing.T) {
13 |
14 | l := startTestGrpcServer()
15 |
16 | bufconnDialer := func(
17 | ctx context.Context, addr string,
18 | ) (net.Conn, error) {
19 | return l.Dial()
20 | }
21 |
22 | client, err := grpc.DialContext(
23 | context.Background(),
24 | "", grpc.WithInsecure(),
25 | grpc.WithContextDialer(bufconnDialer),
26 | )
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 | usersClient := users.NewUsersClient(client)
31 | resp, err := usersClient.GetUser(
32 | context.Background(),
33 | &users.UserGetRequest{
34 | Email: "jane@doe.com",
35 | Id: "foo-bar",
36 | },
37 | )
38 |
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | if resp.User.FirstName != "jane" {
43 | t.Errorf(
44 | "Expected FirstName to be: jane, Got: %s",
45 | resp.User.FirstName,
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/chap10/exercise1/server/test_utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | svc "github.com/practicalgo/code/chap10/server-healthcheck/service"
7 | "google.golang.org/grpc"
8 | healthz "google.golang.org/grpc/health"
9 | healthsvc "google.golang.org/grpc/health/grpc_health_v1"
10 | "google.golang.org/grpc/test/bufconn"
11 | )
12 |
13 | var h *healthz.Server
14 |
15 | func startTestGrpcServer() *bufconn.Listener {
16 | h = healthz.NewServer()
17 | l := bufconn.Listen(10)
18 | s := grpc.NewServer()
19 | registerServices(s, h)
20 | updateServiceHealth(
21 | h,
22 | svc.Users_ServiceDesc.ServiceName,
23 | healthsvc.HealthCheckResponse_SERVING,
24 | )
25 | go func() {
26 | log.Fatal(startServer(s, l))
27 | }()
28 | return l
29 | }
30 |
--------------------------------------------------------------------------------
/chap10/exercise1/service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap10/server-healthcheck/service
2 |
3 | go 1.16
4 |
--------------------------------------------------------------------------------
/chap10/exercise1/service/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap10/server-healthcheck/service/users";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | rpc GetHelp (stream UserHelpRequest) returns (stream UserHelpReply) {}
8 | }
9 |
10 | message UserGetRequest {
11 | string email = 1;
12 | string id = 2;
13 | }
14 |
15 | message User {
16 | string id = 1;
17 | string first_name = 2;
18 | string last_name = 3;
19 | int32 age = 4;
20 | }
21 |
22 | message UserGetReply {
23 | User user = 1;
24 | }
25 |
26 | message UserHelpRequest {
27 | User user = 1;
28 | string request = 2;
29 | }
30 |
31 | message UserHelpReply {
32 | string response = 1;
33 | }
34 |
--------------------------------------------------------------------------------
/chap10/exercise1/tls/generate_certs.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
3 | -days 365 \
4 | -subj "/C=AU/ST=NSW/L=Sydney/O=Echorand/OU=Org/CN=localhost" \
5 | -extensions san \
6 | -config <(echo '[req]'; echo 'distinguished_name=req';
7 | echo '[san]'; echo 'subjectAltName=DNS:localhost') \
8 | -nodes
9 |
--------------------------------------------------------------------------------
/chap10/exercise1/tls/server.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFXTCCA0WgAwIBAgIJAOJvzafXVvr7MA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV
3 | BAYTAkFVMQwwCgYDVQQIDANOU1cxDzANBgNVBAcMBlN5ZG5leTERMA8GA1UECgwI
4 | RWNob3JhbmQxDDAKBgNVBAsMA09yZzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIx
5 | MDcwMjIzMTkwOVoXDTIyMDcwMjIzMTkwOVowYTELMAkGA1UEBhMCQVUxDDAKBgNV
6 | BAgMA05TVzEPMA0GA1UEBwwGU3lkbmV5MREwDwYDVQQKDAhFY2hvcmFuZDEMMAoG
7 | A1UECwwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUA
8 | A4ICDwAwggIKAoICAQCtkkkIyozIs7FSDwYRuVBgvckLBJ+1jsSt1Afq2f+H9NaT
9 | Ofjz3rqC1qryVvZAouhm1WgQLzvQoy424XOp8VxXpOl+hOqCQ05ZHpX9kf16kymV
10 | pG2uLqxLSDPsoG7j+0aSMNEYP75HVYru1hnFEw2n70o9gUFD49+ViCnMEzG5R1ay
11 | iTAmKS8hO2Kphh0YjYB0yNK5it8G06B6FunuY004dKOEcLFjFoQ8QUafClRmpUCy
12 | 3EeCvtO/5gaEePLW1dC9Hn0jzBRxzfyT2LhnY7Q1YLokhjIZkklaCin6ItCmaRW4
13 | 8G2q41+z4JvArs+T5TdVOLFDKA3lWC/U/zy7lwRHkXG3S+lpoSqtZ6gJ7BZrJbr7
14 | kyKkWT6lPsfrI1+rgJ1odA6NTqxsW6/9WDj4X0mP7TyPknl4W8Hc+Y9l6wnA9AUK
15 | 0yT7iWD7/z0dO+dKrfe6HqvFNDbiyKq9iYYYLXSHYUQKCOb+aJ7tLQD39jikrL2B
16 | PPXxHxg32Qus4tGIs71FbWExlqiUwY//cW/hoVrGPorb9wEY4Tea43NAdTMHo2ot
17 | 2kmCtGHu72vlKIZNEXph452TdICRrF8aY1DqKJ5F8uDnMWBxXdJBK7Wm+6Klw+pD
18 | liDKkz9vWPj9AnGbYhPD41zZx5OpQuXdMPtuHeoY0XoJtmYLHiXjwkdIMOCLYwID
19 | AQABoxgwFjAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggIB
20 | AJDGtvhoDBduSaVzJ6EvW7Fb2s5yzxgYpxNGpc1fMVl7ppDOGd9ePBEj9c4CCQJc
21 | UDjndj58GRouHHkZ4UlcMsb5DNVqDPg3BFo+R9uBRWOgnEycbG1Dy3Og8t4gMW8l
22 | Zq5dQWJNkC3QQWV/KLd9V5koz/cjSy/Mu5Sp6iFgoVl5UJ2Ygzqq8/SKX8SwNZhg
23 | b+CkdEfNNLx0KrUfKB2wrB6iAoCw3/V9Y7fg4cOLaWGrr0lmv+0cgd6y0sQT5EbN
24 | raQ8R56JkAVHkxI4ko1RwQ6WEhcgl2KuiPsWuthMYtFbtR3HEOwKYIRXq/oxvBNL
25 | LbJWEhrqU/nUQCswm6/QYTrlEtgtIf6PJe7oIOADzr1vuPLMKNmHae86z8B08xbU
26 | ZzeifLWlS5eyKM/15TnjTxkKG3dh5kldgoTgddfQf1FTh//HR9HO+BFnHeI6++tA
27 | w88mioAavCkgbaSRj0AKpsT0i2dKDPs9mEbK05iKlbowV9Cse+7FOnoW6SFEvWJF
28 | 04VrHgvpZA+05lup1+jD0F04ZmwGw9dBTIdu2vTdZbsf0OPpAtUXkI0rDdl5Bq6X
29 | aQAgQ2yHoeR4tVgsI2AUW1yqC8//V9uYYBoJq7SZANoFpFIYHOoqNDlt3kAiO2af
30 | DQq0upVet6DsqQ8UUuhGWnpl4Hm0nE/kE5bQxXLYybst
31 | -----END CERTIFICATE-----
32 |
--------------------------------------------------------------------------------
/chap10/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Workflow for solution to Exercise 10.2
2 |
3 | - Copy the `server` and `service` directory from the solution of Exercise 10.1
4 | - Change the module name of `server` to `module github.com/practicalgo/book-exercise-solutions/chap10/exercise2`
5 | - `waitForShutDown()` is where the logic is implemented (we use [signal.NotifyContext](https://pkg.go.dev/os/signal?utm_source=gopls#NotifyContext)
6 | to setup signal handling)
--------------------------------------------------------------------------------
/chap10/exercise2/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap10/exercise2
2 |
3 | go 1.16
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require github.com/practicalgo/code/chap10/server-healthcheck/service v0.0.0
8 |
9 | replace github.com/practicalgo/code/chap10/server-healthcheck/service => ../service
10 |
--------------------------------------------------------------------------------
/chap10/exercise2/server/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net"
6 | "testing"
7 |
8 | users "github.com/practicalgo/code/chap10/server-healthcheck/service"
9 | "google.golang.org/grpc"
10 | )
11 |
12 | func TestUserService(t *testing.T) {
13 |
14 | l := startTestGrpcServer()
15 |
16 | bufconnDialer := func(
17 | ctx context.Context, addr string,
18 | ) (net.Conn, error) {
19 | return l.Dial()
20 | }
21 |
22 | client, err := grpc.DialContext(
23 | context.Background(),
24 | "", grpc.WithInsecure(),
25 | grpc.WithContextDialer(bufconnDialer),
26 | )
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 | usersClient := users.NewUsersClient(client)
31 | resp, err := usersClient.GetUser(
32 | context.Background(),
33 | &users.UserGetRequest{
34 | Email: "jane@doe.com",
35 | Id: "foo-bar",
36 | },
37 | )
38 |
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | if resp.User.FirstName != "jane" {
43 | t.Errorf(
44 | "Expected FirstName to be: jane, Got: %s",
45 | resp.User.FirstName,
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/chap10/exercise2/server/test_utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | svc "github.com/practicalgo/code/chap10/server-healthcheck/service"
7 | "google.golang.org/grpc"
8 | healthz "google.golang.org/grpc/health"
9 | healthsvc "google.golang.org/grpc/health/grpc_health_v1"
10 | "google.golang.org/grpc/test/bufconn"
11 | )
12 |
13 | var h *healthz.Server
14 |
15 | func startTestGrpcServer() *bufconn.Listener {
16 | h = healthz.NewServer()
17 | l := bufconn.Listen(10)
18 | s := grpc.NewServer()
19 | registerServices(s, h)
20 | updateServiceHealth(
21 | h,
22 | svc.Users_ServiceDesc.ServiceName,
23 | healthsvc.HealthCheckResponse_SERVING,
24 | )
25 | go func() {
26 | log.Fatal(startServer(s, l))
27 | }()
28 | return l
29 | }
30 |
--------------------------------------------------------------------------------
/chap10/exercise2/service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap10/server-healthcheck/service
2 |
3 | go 1.16
4 |
--------------------------------------------------------------------------------
/chap10/exercise2/service/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap10/server-healthcheck/service/users";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | rpc GetHelp (stream UserHelpRequest) returns (stream UserHelpReply) {}
8 | }
9 |
10 | message UserGetRequest {
11 | string email = 1;
12 | string id = 2;
13 | }
14 |
15 | message User {
16 | string id = 1;
17 | string first_name = 2;
18 | string last_name = 3;
19 | int32 age = 4;
20 | }
21 |
22 | message UserGetReply {
23 | User user = 1;
24 | }
25 |
26 | message UserHelpRequest {
27 | User user = 1;
28 | string request = 2;
29 | }
30 |
31 | message UserHelpReply {
32 | string response = 1;
33 | }
34 |
--------------------------------------------------------------------------------
/chap11/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap11/.emptyfile
--------------------------------------------------------------------------------
/chap11/exercise1/LotsOfFiles.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap11/exercise1/LotsOfFiles.tgz
--------------------------------------------------------------------------------
/chap11/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Workflow for creating Solution 11.1
2 |
3 | - Copy all the files from the code listing, `chap11/pkg-server-1`
4 | - Add a new test function to package_get_handler_test.go to pass a `?download=true` query parameter
5 | - Update the package get handler function to look for `download` query parameter, and then send the data directly if so
--------------------------------------------------------------------------------
/chap11/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap11/exercise1
2 |
3 | go 1.16
4 |
5 | require gocloud.dev v0.23.0
6 |
--------------------------------------------------------------------------------
/chap11/exercise1/localsetup_minio.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eu
3 |
4 | docker run \
5 | -p 9000:9000 \
6 | -p 9001:9001 \
7 | -e MINIO_ROOT_USER=admin \
8 | -e MINIO_ROOT_PASSWORD=admin123 \
9 | -ti minio/minio:RELEASE.2021-07-08T01-15-01Z \
10 | server "/data" \
11 | --console-address ":9001"
12 |
--------------------------------------------------------------------------------
/chap11/exercise1/test_utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "os"
7 |
8 | "gocloud.dev/blob"
9 | "gocloud.dev/blob/fileblob"
10 | )
11 |
12 | func getTestBucket(tmpDir string) (*blob.Bucket, error) {
13 | myDir, err := os.MkdirTemp(tmpDir, "test-bucket")
14 | if err != nil {
15 | return nil, err
16 | }
17 | u, err := url.Parse(fmt.Sprintf("file:///%s", myDir))
18 | if err != nil {
19 | return nil, err
20 | }
21 | opts := fileblob.Options{
22 | URLSigner: fileblob.NewURLSignerHMAC(
23 | u,
24 | []byte("super secret"),
25 | ),
26 | }
27 | return fileblob.OpenBucket(myDir, &opts)
28 | }
29 |
--------------------------------------------------------------------------------
/chap11/exercise1/types.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "io"
4 |
5 | type pkgData struct {
6 | Name string
7 | Version string
8 | Filename string
9 | Bytes io.Reader
10 | }
11 |
12 | type pkgRegisterResponse struct {
13 | ID string `json:"id"`
14 | }
15 |
--------------------------------------------------------------------------------
/chap11/exercise1/upload_package.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "io"
6 | "mime/multipart"
7 | )
8 |
9 | func uploadData(
10 | config appConfig, objectId string, f *multipart.FileHeader,
11 | ) (int64, error) {
12 | ctx := context.Background()
13 |
14 | fData, err := f.Open()
15 | if err != nil {
16 | return 0, err
17 | }
18 | defer fData.Close()
19 |
20 | w, err := config.packageBucket.NewWriter(ctx, objectId, nil)
21 | if err != nil {
22 | return 0, err
23 | }
24 |
25 | nBytes, err := io.Copy(w, fData)
26 | if err != nil {
27 | return 0, err
28 | }
29 | err = w.Close()
30 | if err != nil {
31 | return nBytes, err
32 | }
33 | return nBytes, nil
34 | }
35 |
--------------------------------------------------------------------------------
/chap11/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Workflow for creating Solution 11.2
2 |
3 | - Copy all the files from the code listing, `chap11/pkg-server-2`
4 | - Rename the `TestPackageGetHandler` test function to `TestPackageDownloadHandler`
5 | - Change the URL path the request is being sent to as `/packages/download?owner_id=1&name=pkg&version=0.1`
6 | - Implement a new handler function for handling requets to `/packages/download`
7 | - Once the test passes, continue to implement the querying functionality
8 | - Rename the package get handler function to package query handler function
9 | - Update packageQueryResponse type as follows:
10 |
11 | ```
12 |
13 | type pkgQueryResponse struct {
14 | Packages []pkgRow `json:"packages"`
15 | }
16 | ```
17 |
18 | - The package query handler function will now return a marshalled version of the above type
19 | as a response
20 | - Add/update tests in package_query_handler_test.go
21 |
22 |
--------------------------------------------------------------------------------
/chap11/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap11/pkg-server
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/docker/go-connections v0.4.0
7 | github.com/go-sql-driver/mysql v1.6.0
8 | github.com/testcontainers/testcontainers-go v0.11.1
9 | gocloud.dev v0.23.0
10 | )
11 |
12 | // Remove replace and upgrade library once
13 | // https://github.com/testcontainers/testcontainers-go/pull/342 is merged
14 | // The tag used here is on my personal fork containing the change in PR:
15 | // https://github.com/amitsaha/testcontainers-go/releases/tag/v0.11.1-pr-342
16 | replace github.com/testcontainers/testcontainers-go => github.com/amitsaha/testcontainers-go v0.11.1-pr-342
17 |
--------------------------------------------------------------------------------
/chap11/exercise2/image.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap11/exercise2/image.tgz
--------------------------------------------------------------------------------
/chap11/exercise2/image1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap11/exercise2/image1.png
--------------------------------------------------------------------------------
/chap11/exercise2/localsetup_minio.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eu
3 |
4 | docker run \
5 | -p 9000:9000 \
6 | -p 9001:9001 \
7 | -e MINIO_ROOT_USER=admin \
8 | -e MINIO_ROOT_PASSWORD=admin123 \
9 | -ti minio/minio:RELEASE.2021-07-08T01-15-01Z \
10 | server "/data" \
11 | --console-address ":9001"
12 |
--------------------------------------------------------------------------------
/chap11/exercise2/localsetup_mysql.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eu
3 |
4 | BOOTSTRAP_SQL_PATH="$(pwd)/mysql-init"
5 | if [ ! -d "$BOOTSTRAP_SQL_PATH" ]
6 | then
7 | echo "$BOOTSTRAP_SQL_PATH doesn't exist"
8 | exit 1
9 | fi
10 |
11 |
12 | # mysql
13 | docker run \
14 | --platform linux/x86_64 \
15 | -p 3306:3306 \
16 | -e MYSQL_ROOT_PASSWORD=rootpassword \
17 | -e MYSQL_DATABASE=package_server \
18 | -e MYSQL_USER=packages_rw \
19 | -e MYSQL_PASSWORD=password \
20 | -v "$BOOTSTRAP_SQL_PATH":/docker-entrypoint-initdb.d \
21 | -ti mysql:8.0.26 \
22 | --default-authentication-plugin=mysql_native_password
23 |
--------------------------------------------------------------------------------
/chap11/exercise2/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "testing"
7 |
8 | "github.com/testcontainers/testcontainers-go"
9 | )
10 |
11 | func TestMain(m *testing.M) {
12 | var testC testcontainers.Container
13 | var err error
14 | testC, testDb, err = getTestDb()
15 | if err != nil {
16 | log.Fatal(err)
17 | }
18 | m.Run()
19 | testC.Terminate(context.Background())
20 | }
21 |
--------------------------------------------------------------------------------
/chap11/exercise2/mysql-init/01-create-table.sql:
--------------------------------------------------------------------------------
1 | use package_server;
2 |
3 | CREATE TABLE users (
4 | id INT PRIMARY KEY AUTO_INCREMENT,
5 | username VARCHAR(30) NOT NULL
6 | );
7 |
8 | CREATE TABLE packages(
9 | owner_id INT NOT NULL,
10 | name VARCHAR(100) NOT NULL,
11 | version VARCHAR(50) NOT NULL,
12 | object_store_id VARCHAR(300) NOT NULL,
13 | created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
14 | PRIMARY KEY (owner_id, name, version),
15 | FOREIGN KEY (owner_id)
16 | REFERENCES users(id)
17 | ON DELETE CASCADE
18 | );
--------------------------------------------------------------------------------
/chap11/exercise2/mysql-init/02-insert-data.sql:
--------------------------------------------------------------------------------
1 | use package_server;
2 |
3 | INSERT INTO users (username) VALUES ("joe_cool"), ("jane_doe"), ("go_fer"), ("gopher"), ("bill_bryson")
--------------------------------------------------------------------------------
/chap11/exercise2/package_download_handler_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "log"
7 | "net/http"
8 | "net/http/httptest"
9 | "os"
10 | "strings"
11 | "testing"
12 |
13 | _ "gocloud.dev/blob/fileblob"
14 | )
15 |
16 | func TestPackageDownloadHandler(t *testing.T) {
17 | packageBucket, err := getTestBucket(t.TempDir())
18 | if err != nil {
19 | t.Fatal(err)
20 | }
21 | defer packageBucket.Close()
22 |
23 | testObjectId := "pkg-0.1-pkg-0.1.tar.gz"
24 |
25 | // create a test object
26 | err = packageBucket.WriteAll(
27 | context.Background(),
28 | testObjectId, []byte("test-data"),
29 | nil,
30 | )
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 |
35 | config := appConfig{
36 | logger: log.New(
37 | os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile,
38 | ),
39 | packageBucket: packageBucket,
40 | db: testDb,
41 | }
42 |
43 | // udpate package metadata for the test object
44 | err = updateDb(
45 | config,
46 | pkgRow{
47 | OwnerId: 1,
48 | Name: "pkg",
49 | Version: "0.1",
50 | ObjectStoreId: testObjectId,
51 | },
52 | )
53 | if err != nil {
54 | t.Fatal(err)
55 | }
56 |
57 | mux := http.NewServeMux()
58 | setupHandlers(mux, config)
59 |
60 | ts := httptest.NewServer(mux)
61 | defer ts.Close()
62 |
63 | var redirectUrl string
64 | client := http.Client{
65 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
66 | redirectUrl = req.URL.String()
67 | return errors.New("no redirect")
68 | },
69 | }
70 |
71 | _, err = client.Get(ts.URL + "/packages/download?owner_id=1&name=pkg&version=0.1")
72 | if err == nil {
73 | t.Fatal("Expected error: no redirect, Got nil")
74 | }
75 | if !strings.HasPrefix(redirectUrl, "file:///") {
76 | t.Fatalf("Expected redirect url to start with file:///, got: %v", redirectUrl)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/chap11/exercise2/query_db_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "log"
6 | "os"
7 | "testing"
8 | "time"
9 | )
10 |
11 | var testDb *sql.DB
12 |
13 | func TestQueryDb(t *testing.T) {
14 |
15 | config := appConfig{
16 | logger: log.New(
17 | os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile,
18 | ),
19 | db: testDb,
20 | }
21 |
22 | // update package metadata for the test object
23 | err := updateDb(
24 | config,
25 | pkgRow{
26 | OwnerId: 1,
27 | Name: "pkg",
28 | Version: "0.2",
29 | ObjectStoreId: "pkg-0.2-pkg-0.2.tar.gz",
30 | },
31 | )
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 |
36 | // update package metadata for the test object
37 | err = updateDb(
38 | config,
39 | pkgRow{
40 | OwnerId: 2,
41 | Name: "pkg",
42 | Version: "0.3",
43 | ObjectStoreId: "pkg-0.3-pkg-0.3.tar.gz",
44 | },
45 | )
46 | if err != nil {
47 | t.Fatal(err)
48 | }
49 |
50 | results, err := queryDb(
51 | config,
52 | pkgQueryParams{
53 | ownerId: 2,
54 | version: "0.3",
55 | },
56 | )
57 | if err != nil {
58 | t.Fatal(err)
59 | }
60 |
61 | if len(results) != 1 {
62 | t.Fatalf(
63 | "Expected: 1 row, Got: %d", len(results),
64 | )
65 | }
66 |
67 | layout := "2006-01-02 15:04:05"
68 | created := results[0].Created
69 | parsedTime, err := time.Parse(layout, created)
70 | if err != nil {
71 | t.Fatal(err)
72 | }
73 | t.Logf("%#v", parsedTime.Local().String())
74 | }
75 |
--------------------------------------------------------------------------------
/chap11/exercise2/types.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type pkgRegisterResponse struct {
4 | ID string `json:"id"`
5 | }
6 |
7 | type pkgQueryParams struct {
8 | name string
9 | version string
10 | ownerId int
11 | }
12 |
13 | type pkgRow struct {
14 | OwnerId int `json:"owner_id"`
15 | Name string `json:"name"`
16 | Version string `json:"version"`
17 | ObjectStoreId string `json:"object_store_id"`
18 | Created string `json:"created"`
19 | }
20 |
21 | type pkgQueryResponse struct {
22 | Packages []pkgRow `json:"packages"`
23 | }
24 |
--------------------------------------------------------------------------------
/chap11/exercise2/upload_package.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "io"
6 | "mime/multipart"
7 | )
8 |
9 | func uploadData(
10 | config appConfig, objectId string, f *multipart.FileHeader,
11 | ) (int64, error) {
12 | ctx := context.Background()
13 |
14 | fData, err := f.Open()
15 | if err != nil {
16 | return 0, err
17 | }
18 | defer fData.Close()
19 |
20 | w, err := config.packageBucket.NewWriter(ctx, objectId, nil)
21 | if err != nil {
22 | return 0, err
23 | }
24 |
25 | nBytes, err := io.Copy(w, fData)
26 | if err != nil {
27 | return 0, err
28 | }
29 | err = w.Close()
30 | if err != nil {
31 | return nBytes, err
32 | }
33 | return nBytes, nil
34 | }
35 |
--------------------------------------------------------------------------------
/chap2/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap2/.emptyfile
--------------------------------------------------------------------------------
/chap2/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 2.1
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Copy all the code from chap2/sub-cmd-arch
6 | 2. Create a new file, `main_test.go`
7 | 3. Create a `TestMain()` function where I build the application, run the test functions and remove the built application once done
8 | 4. In the test function, i then setup test configurations which will invoke the built application with the specified
9 | sub-command and/or options and verify the exit code as well as the expected output.
10 |
11 | Note that when verifying the expected output, I verify the lines of output expected instead of the entire
12 | output.
--------------------------------------------------------------------------------
/chap2/exercise1/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "errors"
4 |
5 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
6 |
--------------------------------------------------------------------------------
/chap2/exercise1/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return err
36 | }
37 | if fs.NArg() != 1 {
38 | return ErrNoServerSpecified
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap2/exercise1/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap2/exercise1/cmd/handle_http_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleHttp(t *testing.T) {
10 | usageMessage := `
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -verb string
17 | HTTP method (default "GET")
18 | `
19 | testConfigs := []struct {
20 | args []string
21 | output string
22 | err error
23 | }{
24 | {
25 | args: []string{},
26 | err: ErrNoServerSpecified,
27 | },
28 | {
29 | args: []string{"-h"},
30 | err: errors.New("flag: help requested"),
31 | output: usageMessage,
32 | },
33 | {
34 | args: []string{"http://localhost"},
35 | err: nil,
36 | output: "Executing http command\n",
37 | },
38 | }
39 | byteBuf := new(bytes.Buffer)
40 | for _, tc := range testConfigs {
41 | err := HandleHttp(byteBuf, tc.args)
42 | if tc.err == nil && err != nil {
43 | t.Fatalf("Expected nil error, got %v", err)
44 | }
45 |
46 | if tc.err != nil && err.Error() != tc.err.Error() {
47 | t.Fatalf("Expected error %v, got %v", tc.err, err)
48 | }
49 |
50 | if len(tc.output) != 0 {
51 | gotOutput := byteBuf.String()
52 | if tc.output != gotOutput {
53 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
54 | }
55 | }
56 | byteBuf.Reset()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/chap2/exercise1/cmd/httpCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type httpConfig struct {
10 | url string
11 | verb string
12 | }
13 |
14 | func HandleHttp(w io.Writer, args []string) error {
15 | var v string
16 | fs := flag.NewFlagSet("http", flag.ContinueOnError)
17 | fs.SetOutput(w)
18 | fs.StringVar(&v, "verb", "GET", "HTTP method")
19 |
20 | fs.Usage = func() {
21 | var usageString = `
22 | http: A HTTP client.
23 |
24 | http: server`
25 | fmt.Fprintf(w, usageString)
26 |
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | if fs.NArg() != 1 {
39 | return ErrNoServerSpecified
40 | }
41 |
42 | c := httpConfig{verb: v}
43 | c.url = fs.Arg(0)
44 | fmt.Fprintln(w, "Executing http command")
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/chap2/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap2/exercise1
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap2/exercise1/handle_command_test.go:
--------------------------------------------------------------------------------
1 | // Listing 2.6: chap2/sub-cmd-arch/handle_command_test.go
2 | package main
3 |
4 | import (
5 | "bytes"
6 | "testing"
7 | )
8 |
9 | func TestHandleCommand(t *testing.T) {
10 | usageMessage := `Usage: mync [http|grpc] -h
11 |
12 | http: A HTTP client.
13 |
14 | http: server
15 |
16 | Options:
17 | -verb string
18 | HTTP method (default "GET")
19 |
20 | grpc: A gRPC client.
21 |
22 | grpc: server
23 |
24 | Options:
25 | -body string
26 | Body of request
27 | -method string
28 | Method to call
29 | `
30 | testConfigs := []struct {
31 | args []string
32 | output string
33 | err error
34 | }{
35 | {
36 | args: []string{},
37 | err: errInvalidSubCommand,
38 | output: "Invalid sub-command specified\n" + usageMessage,
39 | },
40 | {
41 | args: []string{"-h"},
42 | err: nil,
43 | output: usageMessage,
44 | },
45 | {
46 | args: []string{"foo"},
47 | err: errInvalidSubCommand,
48 | output: "Invalid sub-command specified\n" + usageMessage,
49 | },
50 | }
51 |
52 | byteBuf := new(bytes.Buffer)
53 | for _, tc := range testConfigs {
54 | err := handleCommand(byteBuf, tc.args)
55 | if tc.err == nil && err != nil {
56 | t.Fatalf("Expected nil error, got %v", err)
57 | }
58 |
59 | if tc.err != nil && err.Error() != tc.err.Error() {
60 | t.Fatalf("Expected error %v, got %v", tc.err, err)
61 | }
62 |
63 | if len(tc.output) != 0 {
64 | gotOutput := byteBuf.String()
65 | if tc.output != gotOutput {
66 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
67 | }
68 | }
69 | byteBuf.Reset()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/chap2/exercise1/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap2/exercise1/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = errInvalidSubCommand
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = errInvalidSubCommand
37 | }
38 | }
39 | if errors.Is(err, cmd.ErrNoServerSpecified) || errors.Is(err, errInvalidSubCommand) {
40 | fmt.Fprintln(w, err)
41 | printUsage(w)
42 | }
43 | return err
44 | }
45 |
46 | func main() {
47 | err := handleCommand(os.Stdout, os.Args[1:])
48 | if err != nil {
49 | os.Exit(1)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap2/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 2.2
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Copy all the code from chap2/sub-cmd-arch
6 | 2. Add a new test configuration to `handle_http_test.go`:
7 |
8 | ```
9 | {
10 | args: []string{"-verb", "PUT", "http://localhost"},
11 | err: ErrInvalidHTTPMethod,
12 | output: "Invalid HTTP method\n",
13 | },
14 | ```
15 |
16 | 3. Add a new function to `httpCmd.go`:
17 |
18 | ```
19 | func validateConfig(c httpConfig) error {
20 | allowedVerbs := []string{"GET", "POST", "HEAD"}
21 | for _, v := range allowedVerbs {
22 | if c.verb == v {
23 | return nil
24 | }
25 | }
26 | return ErrInvalidHTTPMethod
27 | }
28 | ```
29 |
30 | 4. In `HandleHTTP()` function, after parsing the flags, call the `validateConfig()` function, check if the error
31 | returned is due to invalid HTTP verb and display the error message, if so:
32 |
33 | ```
34 | err = validateConfig(c)
35 | if err != nil {
36 | if errors.Is(err, ErrInvalidHTTPMethod) {
37 | fmt.Fprintln(w, "Invalid HTTP method")
38 | }
39 | return err
40 | }
41 | ```
42 |
43 |
--------------------------------------------------------------------------------
/chap2/exercise2/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "errors"
4 |
5 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
6 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
7 |
--------------------------------------------------------------------------------
/chap2/exercise2/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return err
36 | }
37 | if fs.NArg() != 1 {
38 | return ErrNoServerSpecified
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap2/exercise2/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap2/exercise2/cmd/handle_http_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleHttp(t *testing.T) {
10 | usageMessage := `
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -verb string
17 | HTTP method (default "GET")
18 | `
19 | testConfigs := []struct {
20 | args []string
21 | output string
22 | err error
23 | }{
24 | {
25 | args: []string{},
26 | err: ErrNoServerSpecified,
27 | },
28 | {
29 | args: []string{"-h"},
30 | err: errors.New("flag: help requested"),
31 | output: usageMessage,
32 | },
33 | {
34 | args: []string{"http://localhost"},
35 | err: nil,
36 | output: "Executing http command\n",
37 | },
38 | {
39 | args: []string{"-verb", "PUT", "http://localhost"},
40 | err: ErrInvalidHTTPMethod,
41 | output: "Invalid HTTP method\n",
42 | },
43 | }
44 | byteBuf := new(bytes.Buffer)
45 | for i, tc := range testConfigs {
46 | t.Log(i)
47 | err := HandleHttp(byteBuf, tc.args)
48 | if tc.err == nil && err != nil {
49 | t.Fatalf("Expected nil error, got %v", err)
50 | }
51 |
52 | if tc.err != nil && err == nil {
53 | t.Fatal("Expected non-nil error, got nil")
54 | }
55 |
56 | if tc.err != nil && err.Error() != tc.err.Error() {
57 | t.Fatalf("Expected error %v, got %v", tc.err, err)
58 | }
59 |
60 | if len(tc.output) != 0 {
61 | gotOutput := byteBuf.String()
62 | if tc.output != gotOutput {
63 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
64 | }
65 | }
66 | byteBuf.Reset()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/chap2/exercise2/cmd/httpCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "io"
8 | )
9 |
10 | type httpConfig struct {
11 | url string
12 | verb string
13 | }
14 |
15 | func validateConfig(c httpConfig) error {
16 | allowedVerbs := []string{"GET", "POST", "HEAD"}
17 | for _, v := range allowedVerbs {
18 | if c.verb == v {
19 | return nil
20 | }
21 | }
22 | return ErrInvalidHTTPMethod
23 | }
24 |
25 | func HandleHttp(w io.Writer, args []string) error {
26 | c := httpConfig{}
27 |
28 | fs := flag.NewFlagSet("http", flag.ContinueOnError)
29 | fs.SetOutput(w)
30 | fs.StringVar(&c.verb, "verb", "GET", "HTTP method")
31 |
32 | fs.Usage = func() {
33 | var usageString = `
34 | http: A HTTP client.
35 |
36 | http: server`
37 | fmt.Fprintf(w, usageString)
38 |
39 | fmt.Fprintln(w)
40 | fmt.Fprintln(w)
41 | fmt.Fprintln(w, "Options: ")
42 | fs.PrintDefaults()
43 | }
44 |
45 | err := fs.Parse(args)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | if fs.NArg() != 1 {
51 | return ErrNoServerSpecified
52 | }
53 |
54 | err = validateConfig(c)
55 | if err != nil {
56 | if errors.Is(err, ErrInvalidHTTPMethod) {
57 | fmt.Fprintln(w, "Invalid HTTP method")
58 | }
59 | return err
60 | }
61 |
62 | c.url = fs.Arg(0)
63 | fmt.Fprintln(w, "Executing http command")
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/chap2/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap2/exercise2
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap2/exercise2/handle_command_test.go:
--------------------------------------------------------------------------------
1 | // Listing 2.6: chap2/sub-cmd-arch/handle_command_test.go
2 | package main
3 |
4 | import (
5 | "bytes"
6 | "testing"
7 | )
8 |
9 | func TestHandleCommand(t *testing.T) {
10 | usageMessage := `Usage: mync [http|grpc] -h
11 |
12 | http: A HTTP client.
13 |
14 | http: server
15 |
16 | Options:
17 | -verb string
18 | HTTP method (default "GET")
19 |
20 | grpc: A gRPC client.
21 |
22 | grpc: server
23 |
24 | Options:
25 | -body string
26 | Body of request
27 | -method string
28 | Method to call
29 | `
30 | testConfigs := []struct {
31 | args []string
32 | output string
33 | err error
34 | }{
35 | {
36 | args: []string{},
37 | err: errInvalidSubCommand,
38 | output: "Invalid sub-command specified\n" + usageMessage,
39 | },
40 | {
41 | args: []string{"-h"},
42 | err: nil,
43 | output: usageMessage,
44 | },
45 | {
46 | args: []string{"foo"},
47 | err: errInvalidSubCommand,
48 | output: "Invalid sub-command specified\n" + usageMessage,
49 | },
50 | }
51 |
52 | byteBuf := new(bytes.Buffer)
53 | for _, tc := range testConfigs {
54 | err := handleCommand(byteBuf, tc.args)
55 | if tc.err == nil && err != nil {
56 | t.Fatalf("Expected nil error, got %v", err)
57 | }
58 |
59 | if tc.err != nil && err.Error() != tc.err.Error() {
60 | t.Fatalf("Expected error %v, got %v", tc.err, err)
61 | }
62 |
63 | if len(tc.output) != 0 {
64 | gotOutput := byteBuf.String()
65 | if tc.output != gotOutput {
66 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
67 | }
68 | }
69 | byteBuf.Reset()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/chap2/exercise2/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap2/exercise2/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = errInvalidSubCommand
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = errInvalidSubCommand
37 | }
38 | }
39 | if errors.Is(err, cmd.ErrNoServerSpecified) || errors.Is(err, errInvalidSubCommand) {
40 | fmt.Fprintln(w, err)
41 | printUsage(w)
42 | }
43 | return err
44 | }
45 |
46 | func main() {
47 | err := handleCommand(os.Stdout, os.Args[1:])
48 | if err != nil {
49 | os.Exit(1)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap2/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 2.3
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Copy all the code from chap2/user-input-timeout
6 | 2. Refactor the `getNameContext` function to now accept a `io.Reader` and `io.Writer` which will then be
7 | passed to the `getName` function as arguments
8 | 3. Create a new file, `input_timeout_test.go`
9 | 4. Create a test function `TestInputNoTimeout` inside it to test the behavior when user input is provided
10 | 5. Create a test function, `TestInputTimeout` to test the behavior when user input is not provided
11 |
12 | To implement (5), I create a reader, which will not have any data to read, using the [io.Pipe()](https://pkg.go.dev/io#Pipe)
13 | function. I discard the writer, since i don't intend to write anything to it. My initial plan was to simply
14 | use `os.Stdin` instead, but that didn't work since `go test` works by executing the test binary. I was
15 | surprised to find that `go test` wouldn't wait for the user input, so I posted the query
16 | to the golang-nuts google group and my confusion was resolved by another list member.
17 | You can see [the discussion](https://groups.google.com/g/golang-nuts/c/24pL7iQbx64/m/ZHQugkOLAgAJ) on the group.
--------------------------------------------------------------------------------
/chap2/exercise3/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap2/exercise3
2 |
3 | go 1.16
4 |
--------------------------------------------------------------------------------
/chap2/exercise3/input_timeout_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "io"
8 | "os"
9 | "strings"
10 | "testing"
11 | "time"
12 | )
13 |
14 | func TestInputNoTimeout(t *testing.T) {
15 | input := strings.NewReader("jane")
16 | byteBuf := new(bytes.Buffer)
17 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
18 | defer cancel()
19 | name, err := getNameContext(ctx, input, byteBuf)
20 |
21 | if err != nil {
22 | t.Fatalf("Expected nil error, got: %v", err)
23 | }
24 |
25 | if name != "jane" {
26 | t.Fatalf("Expected name returned to be jane, got %s", name)
27 | }
28 | }
29 |
30 | func TestInputTimeout(t *testing.T) {
31 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
32 | defer cancel()
33 |
34 | r, _ := io.Pipe()
35 | name, err := getNameContext(ctx, r, os.Stdout)
36 | if err == nil {
37 | t.Fatal("Expected non-nil error")
38 | }
39 |
40 | if err == nil {
41 | t.Fatal("Expected non-nil error, got nil")
42 | }
43 |
44 | if !errors.Is(err, context.DeadlineExceeded) {
45 | t.Fatalf("Expected error: context.DeadlineExceeded, Got: %s", err)
46 | }
47 |
48 | if name != "Default Name" {
49 | t.Fatalf("Expected name returned to be Default Name, got %s", name)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap2/exercise3/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "os"
10 | "time"
11 | )
12 |
13 | var totalDuration time.Duration = 5
14 |
15 | func getName(r io.Reader, w io.Writer) (string, error) {
16 | scanner := bufio.NewScanner(r)
17 | msg := "Your name please? Press the Enter key when done"
18 | fmt.Fprintln(w, msg)
19 |
20 | scanner.Scan()
21 | if err := scanner.Err(); err != nil {
22 | return "", err
23 | }
24 | name := scanner.Text()
25 | if len(name) == 0 {
26 | return "", errors.New("You entered an empty name")
27 | }
28 | return name, nil
29 | }
30 |
31 | func getNameContext(ctx context.Context, r io.Reader, w io.Writer) (string, error) {
32 | var err error
33 | name := "Default Name"
34 | c := make(chan error, 1)
35 |
36 | go func() {
37 | name, err = getName(r, w)
38 | c <- err
39 | }()
40 |
41 | select {
42 | case <-ctx.Done():
43 | return name, ctx.Err()
44 | case err := <-c:
45 | return name, err
46 | }
47 | }
48 |
49 | func main() {
50 | allowedDuration := totalDuration * time.Second
51 | ctx, cancel := context.WithTimeout(context.Background(), allowedDuration)
52 | defer cancel()
53 |
54 | name, err := getNameContext(ctx, os.Stdin, os.Stdout)
55 |
56 | if err != nil && !errors.Is(err, context.DeadlineExceeded) {
57 | fmt.Fprintf(os.Stdout, "%v\n", err)
58 | os.Exit(1)
59 | }
60 | fmt.Fprintln(os.Stdout, name)
61 | }
62 |
--------------------------------------------------------------------------------
/chap3/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap3/.emptyfile
--------------------------------------------------------------------------------
/chap3/exercise1/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "errors"
4 |
5 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
6 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
7 |
--------------------------------------------------------------------------------
/chap3/exercise1/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return err
36 | }
37 | if fs.NArg() != 1 {
38 | return ErrNoServerSpecified
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap3/exercise1/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap3/exercise1/cmd/handle_http_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "net/http"
8 | "net/http/httptest"
9 | "testing"
10 | )
11 |
12 | func startTestHttpServer() *httptest.Server {
13 | mux := http.NewServeMux()
14 | mux.HandleFunc("/download", func(w http.ResponseWriter, req *http.Request) {
15 | fmt.Fprintf(w, "this is a response")
16 | })
17 | return httptest.NewServer(mux)
18 | }
19 |
20 | func TestHandleHttp(t *testing.T) {
21 | usageMessage := `
22 | http: A HTTP client.
23 |
24 | http: server
25 |
26 | Options:
27 | -verb string
28 | HTTP method (default "GET")
29 | `
30 |
31 | ts := startTestHttpServer()
32 | defer ts.Close()
33 |
34 | testConfigs := []struct {
35 | args []string
36 | output string
37 | err error
38 | }{
39 | {
40 | args: []string{},
41 | err: ErrNoServerSpecified,
42 | },
43 | {
44 | args: []string{"-h"},
45 | err: errors.New("flag: help requested"),
46 | output: usageMessage,
47 | },
48 | {
49 | args: []string{ts.URL + "/download"},
50 | err: nil,
51 | output: "this is a response\n",
52 | },
53 | {
54 | args: []string{"-verb", "PUT", "http://localhost"},
55 | err: ErrInvalidHTTPMethod,
56 | output: "Invalid HTTP method\n",
57 | },
58 | }
59 | byteBuf := new(bytes.Buffer)
60 | for i, tc := range testConfigs {
61 | t.Log(i)
62 | err := HandleHttp(byteBuf, tc.args)
63 | if tc.err == nil && err != nil {
64 | t.Fatalf("Expected nil error, got %v", err)
65 | }
66 |
67 | if tc.err != nil && err == nil {
68 | t.Fatal("Expected non-nil error, got nil")
69 | }
70 |
71 | if tc.err != nil && err.Error() != tc.err.Error() {
72 | t.Fatalf("Expected error %v, got %v", tc.err, err)
73 | }
74 |
75 | if len(tc.output) != 0 {
76 | gotOutput := byteBuf.String()
77 | if tc.output != gotOutput {
78 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
79 | }
80 | }
81 | byteBuf.Reset()
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/chap3/exercise1/cmd/httpCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | )
10 |
11 | type httpConfig struct {
12 | url string
13 | verb string
14 | }
15 |
16 | func validateConfig(c httpConfig) error {
17 | allowedVerbs := []string{"GET", "POST", "HEAD"}
18 | for _, v := range allowedVerbs {
19 | if c.verb == v {
20 | return nil
21 | }
22 | }
23 | return ErrInvalidHTTPMethod
24 | }
25 |
26 | func fetchRemoteResource(url string) ([]byte, error) {
27 | r, err := http.Get(url)
28 | if err != nil {
29 | return nil, err
30 | }
31 | defer r.Body.Close()
32 | return io.ReadAll(r.Body)
33 | }
34 |
35 | func HandleHttp(w io.Writer, args []string) error {
36 | c := httpConfig{}
37 |
38 | fs := flag.NewFlagSet("http", flag.ContinueOnError)
39 | fs.SetOutput(w)
40 | fs.StringVar(&c.verb, "verb", "GET", "HTTP method")
41 |
42 | fs.Usage = func() {
43 | var usageString = `
44 | http: A HTTP client.
45 |
46 | http: server`
47 | fmt.Fprintf(w, usageString)
48 |
49 | fmt.Fprintln(w)
50 | fmt.Fprintln(w)
51 | fmt.Fprintln(w, "Options: ")
52 | fs.PrintDefaults()
53 | }
54 |
55 | err := fs.Parse(args)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | if fs.NArg() != 1 {
61 | return ErrNoServerSpecified
62 | }
63 |
64 | err = validateConfig(c)
65 | if err != nil {
66 | if errors.Is(err, ErrInvalidHTTPMethod) {
67 | fmt.Fprintln(w, "Invalid HTTP method")
68 | }
69 | return err
70 | }
71 |
72 | c.url = fs.Arg(0)
73 | data, err := fetchRemoteResource(c.url)
74 | if err != nil {
75 | return err
76 | }
77 | fmt.Fprintln(w, string(data))
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/chap3/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap3/exercise1
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap3/exercise1/handle_command_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestHandleCommand(t *testing.T) {
9 | usageMessage := `Usage: mync [http|grpc] -h
10 |
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -verb string
17 | HTTP method (default "GET")
18 |
19 | grpc: A gRPC client.
20 |
21 | grpc: server
22 |
23 | Options:
24 | -body string
25 | Body of request
26 | -method string
27 | Method to call
28 | `
29 | testConfigs := []struct {
30 | args []string
31 | output string
32 | err error
33 | }{
34 | {
35 | args: []string{},
36 | err: errInvalidSubCommand,
37 | output: "Invalid sub-command specified\n" + usageMessage,
38 | },
39 | {
40 | args: []string{"-h"},
41 | err: nil,
42 | output: usageMessage,
43 | },
44 | {
45 | args: []string{"foo"},
46 | err: errInvalidSubCommand,
47 | output: "Invalid sub-command specified\n" + usageMessage,
48 | },
49 | }
50 |
51 | byteBuf := new(bytes.Buffer)
52 | for _, tc := range testConfigs {
53 | err := handleCommand(byteBuf, tc.args)
54 | if tc.err == nil && err != nil {
55 | t.Fatalf("Expected nil error, got %v", err)
56 | }
57 |
58 | if tc.err != nil && err.Error() != tc.err.Error() {
59 | t.Fatalf("Expected error %v, got %v", tc.err, err)
60 | }
61 |
62 | if len(tc.output) != 0 {
63 | gotOutput := byteBuf.String()
64 | if tc.output != gotOutput {
65 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
66 | }
67 | }
68 | byteBuf.Reset()
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/chap3/exercise1/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap3/exercise1/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = errInvalidSubCommand
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = errInvalidSubCommand
37 | }
38 | }
39 | if errors.Is(err, cmd.ErrNoServerSpecified) || errors.Is(err, errInvalidSubCommand) {
40 | fmt.Fprintln(w, err)
41 | printUsage(w)
42 | }
43 | return err
44 | }
45 |
46 | func main() {
47 | err := handleCommand(os.Stdout, os.Args[1:])
48 | if err != nil {
49 | os.Exit(1)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap3/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 3.2
2 |
3 | This is my workflow in creating the solution:
4 |
5 | - Copy all the code from solution of chapter 3, exercise 1.
6 |
7 | In the `cmd` package:
8 |
9 | 1. Add a new test configuration in `handle_http_test.go`:
10 | ```
11 | {
12 | args: []string{"-verb", "GET", "-output", "file_path.out", "http://localhost/binary-data"},
13 | err: nil,
14 | output: "Data saved to file_path.out",
15 | },
16 | ```
17 |
18 | Now, the test function will fail.
19 |
20 | 2. Add a new option, `output` to `httpCmd.go` to have a string value
21 | 3. If this option is specified, the response from the server will now be written to a file and
22 | if that completes successfully, display a message to the user, "Data saved to: ".
23 |
--------------------------------------------------------------------------------
/chap3/exercise2/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "errors"
4 |
5 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
6 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
7 |
--------------------------------------------------------------------------------
/chap3/exercise2/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return err
36 | }
37 | if fs.NArg() != 1 {
38 | return ErrNoServerSpecified
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap3/exercise2/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap3/exercise2/cmd/httpCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | )
11 |
12 | type httpConfig struct {
13 | url string
14 | verb string
15 | }
16 |
17 | func validateConfig(c httpConfig) error {
18 | allowedVerbs := []string{"GET", "POST", "HEAD"}
19 | for _, v := range allowedVerbs {
20 | if c.verb == v {
21 | return nil
22 | }
23 | }
24 | return ErrInvalidHTTPMethod
25 | }
26 |
27 | func fetchRemoteResource(url string) ([]byte, error) {
28 | r, err := http.Get(url)
29 | if err != nil {
30 | return nil, err
31 | }
32 | defer r.Body.Close()
33 | return io.ReadAll(r.Body)
34 | }
35 |
36 | func HandleHttp(w io.Writer, args []string) error {
37 | c := httpConfig{}
38 | var outputFile string
39 |
40 | fs := flag.NewFlagSet("http", flag.ContinueOnError)
41 | fs.SetOutput(w)
42 | fs.StringVar(&c.verb, "verb", "GET", "HTTP method")
43 | fs.StringVar(&outputFile, "output", "", "File path to write the response into")
44 |
45 | fs.Usage = func() {
46 | var usageString = `
47 | http: A HTTP client.
48 |
49 | http: server`
50 | fmt.Fprintf(w, usageString)
51 |
52 | fmt.Fprintln(w)
53 | fmt.Fprintln(w)
54 | fmt.Fprintln(w, "Options: ")
55 | fs.PrintDefaults()
56 | }
57 |
58 | err := fs.Parse(args)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | if fs.NArg() != 1 {
64 | return ErrNoServerSpecified
65 | }
66 |
67 | err = validateConfig(c)
68 | if err != nil {
69 | if errors.Is(err, ErrInvalidHTTPMethod) {
70 | fmt.Fprintln(w, "Invalid HTTP method")
71 | }
72 | return err
73 | }
74 |
75 | c.url = fs.Arg(0)
76 | data, err := fetchRemoteResource(c.url)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | if len(outputFile) != 0 {
82 | f, err := os.Create(outputFile)
83 | if err != nil {
84 | return err
85 | }
86 | defer f.Close()
87 | _, err = f.Write(data)
88 | if err != nil {
89 | return err
90 | }
91 | fmt.Fprintf(w, "Data saved to: %s\n", outputFile)
92 | return err
93 | }
94 | fmt.Fprintln(w, string(data))
95 | return nil
96 | }
97 |
--------------------------------------------------------------------------------
/chap3/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap3/exercise2
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap3/exercise2/handle_command_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestHandleCommand(t *testing.T) {
9 | usageMessage := `Usage: mync [http|grpc] -h
10 |
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -output string
17 | File path to write the response into
18 | -verb string
19 | HTTP method (default "GET")
20 |
21 | grpc: A gRPC client.
22 |
23 | grpc: server
24 |
25 | Options:
26 | -body string
27 | Body of request
28 | -method string
29 | Method to call
30 | `
31 | testConfigs := []struct {
32 | args []string
33 | output string
34 | err error
35 | }{
36 | {
37 | args: []string{},
38 | err: errInvalidSubCommand,
39 | output: "Invalid sub-command specified\n" + usageMessage,
40 | },
41 | {
42 | args: []string{"-h"},
43 | err: nil,
44 | output: usageMessage,
45 | },
46 | {
47 | args: []string{"foo"},
48 | err: errInvalidSubCommand,
49 | output: "Invalid sub-command specified\n" + usageMessage,
50 | },
51 | }
52 |
53 | byteBuf := new(bytes.Buffer)
54 | for _, tc := range testConfigs {
55 | err := handleCommand(byteBuf, tc.args)
56 | if tc.err == nil && err != nil {
57 | t.Fatalf("Expected nil error, got %v", err)
58 | }
59 |
60 | if tc.err != nil && err.Error() != tc.err.Error() {
61 | t.Fatalf("Expected error %v, got %v", tc.err, err)
62 | }
63 |
64 | if len(tc.output) != 0 {
65 | gotOutput := byteBuf.String()
66 | if tc.output != gotOutput {
67 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
68 | }
69 | }
70 | byteBuf.Reset()
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/chap3/exercise2/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap3/exercise2/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = errInvalidSubCommand
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = errInvalidSubCommand
37 | }
38 | }
39 | if errors.Is(err, cmd.ErrNoServerSpecified) || errors.Is(err, errInvalidSubCommand) {
40 | fmt.Fprintln(w, err)
41 | printUsage(w)
42 | }
43 | return err
44 | }
45 |
46 | func main() {
47 | err := handleCommand(os.Stdout, os.Args[1:])
48 | if err != nil {
49 | os.Exit(1)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap3/exercise3/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "errors"
4 |
5 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
6 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
7 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
8 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
9 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
10 |
--------------------------------------------------------------------------------
/chap3/exercise3/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return err
36 | }
37 | if fs.NArg() != 1 {
38 | return ErrNoServerSpecified
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap3/exercise3/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap3/exercise3/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap3/exercise3
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap3/exercise3/handle_command_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestHandleCommand(t *testing.T) {
9 | usageMessage := `Usage: mync [http|grpc] -h
10 |
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -body string
17 | JSON data for HTTP POST request
18 | -body-file string
19 | File containing JSON data for HTTP POST request
20 | -output string
21 | File path to write the response into
22 | -verb string
23 | HTTP method (default "GET")
24 |
25 | grpc: A gRPC client.
26 |
27 | grpc: server
28 |
29 | Options:
30 | -body string
31 | Body of request
32 | -method string
33 | Method to call
34 | `
35 | testConfigs := []struct {
36 | args []string
37 | output string
38 | err error
39 | }{
40 | {
41 | args: []string{},
42 | err: errInvalidSubCommand,
43 | output: "Invalid sub-command specified\n" + usageMessage,
44 | },
45 | {
46 | args: []string{"-h"},
47 | err: nil,
48 | output: usageMessage,
49 | },
50 | {
51 | args: []string{"foo"},
52 | err: errInvalidSubCommand,
53 | output: "Invalid sub-command specified\n" + usageMessage,
54 | },
55 | }
56 |
57 | byteBuf := new(bytes.Buffer)
58 | for _, tc := range testConfigs {
59 | err := handleCommand(byteBuf, tc.args)
60 | if tc.err == nil && err != nil {
61 | t.Fatalf("Expected nil error, got %v", err)
62 | }
63 |
64 | if tc.err != nil && err.Error() != tc.err.Error() {
65 | t.Fatalf("Expected error %v, got %v", tc.err, err)
66 | }
67 |
68 | if len(tc.output) != 0 {
69 | gotOutput := byteBuf.String()
70 | if tc.output != gotOutput {
71 | t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
72 | }
73 | }
74 | byteBuf.Reset()
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/chap3/exercise3/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap3/exercise3/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = errInvalidSubCommand
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = errInvalidSubCommand
37 | }
38 | }
39 | if errors.Is(err, cmd.ErrNoServerSpecified) || errors.Is(err, errInvalidSubCommand) {
40 | fmt.Fprintln(w, err)
41 | printUsage(w)
42 | }
43 | return err
44 | }
45 |
46 | func main() {
47 | err := handleCommand(os.Stdout, os.Args[1:])
48 | if err != nil {
49 | os.Exit(1)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap3/exercise3/test.json:
--------------------------------------------------------------------------------
1 | {"id": 1}
--------------------------------------------------------------------------------
/chap3/exercise4/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "errors"
4 |
5 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
6 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
7 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
8 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
9 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
10 |
--------------------------------------------------------------------------------
/chap3/exercise4/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return err
36 | }
37 | if fs.NArg() != 1 {
38 | return ErrNoServerSpecified
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap3/exercise4/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap3/exercise4/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap3/exercise4
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap3/exercise4/handle_command_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "testing"
8 | )
9 |
10 | func TestHandleCommand(t *testing.T) {
11 | testConfigs := []struct {
12 | args []string
13 | goldenOutput string
14 | err error
15 | }{
16 | {
17 | args: []string{},
18 | err: errInvalidSubCommand,
19 | goldenOutput: "expectedGolden.0",
20 | },
21 | {
22 | args: []string{"-h"},
23 | err: nil,
24 | goldenOutput: "expectedGolden.1",
25 | },
26 | {
27 | args: []string{"foo"},
28 | err: errInvalidSubCommand,
29 | goldenOutput: "expectedGolden.2",
30 | },
31 | }
32 |
33 | byteBuf := new(bytes.Buffer)
34 | for i, tc := range testConfigs {
35 | err := handleCommand(byteBuf, tc.args)
36 | if tc.err == nil && err != nil {
37 | t.Fatalf("Expected nil error, got %v", err)
38 | }
39 |
40 | if tc.err != nil && err.Error() != tc.err.Error() {
41 | t.Fatalf("Expected error %v, got %v", tc.err, err)
42 | }
43 |
44 | gotOutput := byteBuf.String()
45 | expectedOutput, err := os.ReadFile("testdata/" + tc.goldenOutput)
46 | if err != nil {
47 | t.Fatalf("error reading expected golden output: %s:%s\n", tc.goldenOutput, err)
48 | }
49 | if string(expectedOutput) != gotOutput {
50 | gotOutputFilename := fmt.Sprintf("testdata/gotOutput.%d", i)
51 | t.Errorf(
52 | "Expected output to be:\n%s\n\nGot:\n%s\n\n"+
53 | "Writing expected data to file: %s",
54 | string(expectedOutput), gotOutput,
55 | gotOutputFilename,
56 | )
57 | if ok := os.WriteFile(gotOutputFilename, []byte(gotOutput), 0666); ok != nil {
58 | t.Fatal("Error writing expected output to file", err)
59 | }
60 | }
61 | byteBuf.Reset()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chap3/exercise4/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap3/exercise4/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = errInvalidSubCommand
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = errInvalidSubCommand
37 | }
38 | }
39 | if errors.Is(err, cmd.ErrNoServerSpecified) || errors.Is(err, errInvalidSubCommand) {
40 | fmt.Fprintln(w, err)
41 | printUsage(w)
42 | }
43 | return err
44 | }
45 |
46 | func main() {
47 | err := handleCommand(os.Stdout, os.Args[1:])
48 | if err != nil {
49 | os.Exit(1)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap3/exercise4/testdata/expectedGolden.0:
--------------------------------------------------------------------------------
1 | Invalid sub-command specified
2 | Usage: mync [http|grpc] -h
3 |
4 | http: A HTTP client.
5 |
6 | http: server
7 |
8 | Options:
9 | -body string
10 | JSON data for HTTP POST request
11 | -body-file string
12 | File containing JSON data for HTTP POST request
13 | -form-data value
14 | Add one or more key value pairs (key=value) to send as form data
15 | -output string
16 | File path to write the response into
17 | -upload string
18 | Path of file to upload
19 | -verb string
20 | HTTP method (default "GET")
21 |
22 | grpc: A gRPC client.
23 |
24 | grpc: server
25 |
26 | Options:
27 | -body string
28 | Body of request
29 | -method string
30 | Method to call
31 |
--------------------------------------------------------------------------------
/chap3/exercise4/testdata/expectedGolden.1:
--------------------------------------------------------------------------------
1 | Usage: mync [http|grpc] -h
2 |
3 | http: A HTTP client.
4 |
5 | http: server
6 |
7 | Options:
8 | -body string
9 | JSON data for HTTP POST request
10 | -body-file string
11 | File containing JSON data for HTTP POST request
12 | -form-data value
13 | Add one or more key value pairs (key=value) to send as form data
14 | -output string
15 | File path to write the response into
16 | -upload string
17 | Path of file to upload
18 | -verb string
19 | HTTP method (default "GET")
20 |
21 | grpc: A gRPC client.
22 |
23 | grpc: server
24 |
25 | Options:
26 | -body string
27 | Body of request
28 | -method string
29 | Method to call
30 |
--------------------------------------------------------------------------------
/chap3/exercise4/testdata/expectedGolden.2:
--------------------------------------------------------------------------------
1 | Invalid sub-command specified
2 | Usage: mync [http|grpc] -h
3 |
4 | http: A HTTP client.
5 |
6 | http: server
7 |
8 | Options:
9 | -body string
10 | JSON data for HTTP POST request
11 | -body-file string
12 | File containing JSON data for HTTP POST request
13 | -form-data value
14 | Add one or more key value pairs (key=value) to send as form data
15 | -output string
16 | File path to write the response into
17 | -upload string
18 | Path of file to upload
19 | -verb string
20 | HTTP method (default "GET")
21 |
22 | grpc: A gRPC client.
23 |
24 | grpc: server
25 |
26 | Options:
27 | -body string
28 | Body of request
29 | -method string
30 | Method to call
31 |
--------------------------------------------------------------------------------
/chap3/exercise4/testdata/expectedGolden.cmd.httpCmdUsage:
--------------------------------------------------------------------------------
1 |
2 | http: A HTTP client.
3 |
4 | http: server
5 |
6 | Options:
7 | -body string
8 | JSON data for HTTP POST request
9 | -body-file string
10 | File containing JSON data for HTTP POST request
11 | -form-data value
12 | Add one or more key value pairs (key=value) to send as form data
13 | -output string
14 | File path to write the response into
15 | -upload string
16 | Path of file to upload
17 | -verb string
18 | HTTP method (default "GET")
19 |
--------------------------------------------------------------------------------
/chap4/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap4/.emptyfile
--------------------------------------------------------------------------------
/chap4/exercise1/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
8 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
9 |
10 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
11 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
12 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
13 |
14 | type FlagParsingError struct {
15 | err error
16 | }
17 |
18 | func (e FlagParsingError) Error() string {
19 | return e.err.Error()
20 | }
21 |
22 | type InvalidInputError struct {
23 | Err error
24 | }
25 |
26 | func (e InvalidInputError) Error() string {
27 | return e.Err.Error()
28 | }
29 |
--------------------------------------------------------------------------------
/chap4/exercise1/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return FlagParsingError{err}
36 | }
37 | if fs.NArg() != 1 {
38 | return InvalidInputError{ErrNoServerSpecified}
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap4/exercise1/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap4/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap4/exercise1
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap4/exercise1/handle_command_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestHandleCommand(t *testing.T) {
9 | usageMessage := `Usage: mync [http|grpc] -h
10 |
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -body string
17 | JSON data for HTTP POST request
18 | -body-file string
19 | File containing JSON data for HTTP POST request
20 | -disable-redirect
21 | Do not follow redirection request
22 | -output string
23 | File path to write the response into
24 | -verb string
25 | HTTP method (default "GET")
26 |
27 | grpc: A gRPC client.
28 |
29 | grpc: server
30 |
31 | Options:
32 | -body string
33 | Body of request
34 | -method string
35 | Method to call
36 | `
37 | testConfigs := []struct {
38 | args []string
39 | output string
40 | err error
41 | }{
42 | {
43 | args: []string{},
44 | err: errInvalidSubCommand,
45 | output: "Invalid sub-command specified\n" + usageMessage,
46 | },
47 | {
48 | args: []string{"-h"},
49 | err: nil,
50 | output: usageMessage,
51 | },
52 | {
53 | args: []string{"foo"},
54 | err: errInvalidSubCommand,
55 | output: "Invalid sub-command specified\n" + usageMessage,
56 | },
57 | }
58 |
59 | byteBuf := new(bytes.Buffer)
60 | for _, tc := range testConfigs {
61 | err := handleCommand(byteBuf, tc.args)
62 | if tc.err == nil && err != nil {
63 | t.Fatalf("Expected nil error, got %v", err)
64 | }
65 |
66 | if tc.err != nil && err.Error() != tc.err.Error() {
67 | t.Fatalf("Expected error %v, got %v", tc.err, err)
68 | }
69 |
70 | if len(tc.output) != 0 {
71 | gotOutput := byteBuf.String()
72 | if tc.output != gotOutput {
73 | t.Errorf("Expected output to be: %s, Got: %s", tc.output, gotOutput)
74 | }
75 | }
76 | byteBuf.Reset()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/chap4/exercise1/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap4/exercise1/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
37 | }
38 | }
39 |
40 | // for non-nil errors, we can have three kinds of errors
41 | // 1. Flag parsing error (E.g. use of invalid option)
42 | // 2. Invalid input error (E.g. JSON body specified for a GET request)
43 | // 3. Application specific error (E.g. remote server returned an error for example)
44 | // For (1), the flag package will show the error and also print the usage, so we don't do anything here
45 | // For (2), the we want to show the error and print the usage of the program
46 | // For (3), we only want to show the error
47 | if err != nil {
48 | if !errors.As(err, &cmd.FlagParsingError{}) {
49 | fmt.Fprintln(w, err.Error())
50 | }
51 | if errors.As(err, &cmd.InvalidInputError{}) {
52 | printUsage(w)
53 | }
54 | }
55 |
56 | if err != nil {
57 | if !errors.As(err, &cmd.FlagParsingError{}) {
58 | fmt.Fprintln(w, err.Error())
59 | }
60 | if errors.As(err, &cmd.InvalidInputError{}) {
61 | printUsage(w)
62 | }
63 | }
64 | return err
65 | }
66 |
67 | func main() {
68 | err := handleCommand(os.Stdout, os.Args[1:])
69 | if err != nil {
70 | os.Exit(1)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/chap4/exercise1/test.json:
--------------------------------------------------------------------------------
1 | {"id": 1}
--------------------------------------------------------------------------------
/chap4/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 4.2
2 |
3 | This is my workflow in creating the solution:
4 |
5 | - Copy all the code from solution of chapter 4, exercise 1.
6 | - Rename go module to: `github.com/practicalgo/book-exercise-solutions/chap4/exercise2` by editing the `go.mod` file
7 |
8 |
9 | The most challenging aspect for me in this exercise was to be able to add a flag option
10 | which could be specified more than once. This is how we can do that:
11 |
12 | ```
13 | // we want the user to be able to specify the -header option one or more times
14 | // hence we use this method:
15 | // https://pkg.go.dev/flag#FlagSet.Func
16 | headerOptionFunc := func(v string) error {
17 | c.headers = append(c.headers, v)
18 | return nil
19 | }
20 | fs.Func("header", "Add one or more headers to the outgoing request (key=value)", headerOptionFunc)
21 | ```
22 |
23 | For testing the two new options, I implemented the following two endpoints in the test
24 | http server (handle_http_test.go):
25 |
26 | ```
27 | mux.HandleFunc("/debug-header-response", func(w http.ResponseWriter, req *http.Request) {
28 | headers := []string{}
29 | for k, v := range req.Header {
30 | if strings.HasPrefix(k, "Debug") {
31 | headers = append(headers, fmt.Sprintf("%s=%s", k, v[0]))
32 | }
33 | }
34 | fmt.Fprint(w, strings.Join(headers, " "))
35 | })
36 |
37 | mux.HandleFunc("/debug-basicauth", func(w http.ResponseWriter, req *http.Request) {
38 | u, p, ok := req.BasicAuth()
39 | if !ok {
40 | http.Error(w, "Basic auth missing/malformed", http.StatusBadRequest)
41 | return
42 | }
43 | fmt.Fprintf(w, "%s=%s", u, p)
44 | })
45 | ```
--------------------------------------------------------------------------------
/chap4/exercise2/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
8 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
9 |
10 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
11 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
12 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
13 |
14 | type FlagParsingError struct {
15 | err error
16 | }
17 |
18 | func (e FlagParsingError) Error() string {
19 | return e.err.Error()
20 | }
21 |
22 | type InvalidInputError struct {
23 | Err error
24 | }
25 |
26 | func (e InvalidInputError) Error() string {
27 | return e.Err.Error()
28 | }
29 |
--------------------------------------------------------------------------------
/chap4/exercise2/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return FlagParsingError{err}
36 | }
37 | if fs.NArg() != 1 {
38 | return InvalidInputError{ErrNoServerSpecified}
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap4/exercise2/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap4/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap4/exercise2
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap4/exercise2/handle_command_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestHandleCommand(t *testing.T) {
9 | usageMessage := `Usage: mync [http|grpc] -h
10 |
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -basicauth string
17 | Add basic auth (username:password) credentials to the outgoing request
18 | -body string
19 | JSON data for HTTP POST request
20 | -body-file string
21 | File containing JSON data for HTTP POST request
22 | -disable-redirect
23 | Do not follow redirection request
24 | -header value
25 | Add one or more headers to the outgoing request (key=value)
26 | -output string
27 | File path to write the response into
28 | -verb string
29 | HTTP method (default "GET")
30 |
31 | grpc: A gRPC client.
32 |
33 | grpc: server
34 |
35 | Options:
36 | -body string
37 | Body of request
38 | -method string
39 | Method to call
40 | `
41 | testConfigs := []struct {
42 | args []string
43 | output string
44 | err error
45 | }{
46 | {
47 | args: []string{},
48 | err: errInvalidSubCommand,
49 | output: "Invalid sub-command specified\n" + usageMessage,
50 | },
51 | {
52 | args: []string{"-h"},
53 | err: nil,
54 | output: usageMessage,
55 | },
56 | {
57 | args: []string{"foo"},
58 | err: errInvalidSubCommand,
59 | output: "Invalid sub-command specified\n" + usageMessage,
60 | },
61 | }
62 |
63 | byteBuf := new(bytes.Buffer)
64 | for _, tc := range testConfigs {
65 | err := handleCommand(byteBuf, tc.args)
66 | if tc.err == nil && err != nil {
67 | t.Fatalf("Expected nil error, got %v", err)
68 | }
69 |
70 | if tc.err != nil && err.Error() != tc.err.Error() {
71 | t.Fatalf("Expected error %v, got %v", tc.err, err)
72 | }
73 |
74 | if len(tc.output) != 0 {
75 | gotOutput := byteBuf.String()
76 | if tc.output != gotOutput {
77 | t.Errorf("Expected output to be: %s, Got: %s", tc.output, gotOutput)
78 | }
79 | }
80 | byteBuf.Reset()
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/chap4/exercise2/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap4/exercise2/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
37 | }
38 | }
39 |
40 | // for non-nil errors, we can have three kinds of errors
41 | // 1. Flag parsing error (E.g. use of invalid option)
42 | // 2. Invalid input error (E.g. JSON body specified for a GET request)
43 | // 3. Application specific error (E.g. remote server returned an error for example)
44 | // For (1), the flag package will show the error and also print the usage, so we don't do anything here
45 | // For (2), the we want to show the error and print the usage of the program
46 | // For (3), we only want to show the error
47 | if err != nil {
48 | if !errors.As(err, &cmd.FlagParsingError{}) {
49 | fmt.Fprintln(w, err.Error())
50 | }
51 | if errors.As(err, &cmd.InvalidInputError{}) {
52 | printUsage(w)
53 | }
54 | }
55 | return err
56 | }
57 |
58 | func main() {
59 | err := handleCommand(os.Stdout, os.Args[1:])
60 | if err != nil {
61 | os.Exit(1)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chap4/exercise2/test.json:
--------------------------------------------------------------------------------
1 | {"id": 1}
--------------------------------------------------------------------------------
/chap4/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 4.3
2 |
3 | This is my workflow in creating the solution:
4 |
5 | - Copy all the code from solution of chapter 4, exercise 2.
6 | - Rename go module to: `github.com/practicalgo/book-exercise-solutions/chap4/exercise3`
7 | by editing the `go.mod` file
8 | - Create a new package `middleware`. Inside it create a new file, `httpLatency.go` with the middleware
9 | definition.
10 | - Update HTTP client creation in `cmd\httpCmd.go` to configure the transport
11 |
12 | Example:
13 |
14 | ```
15 | C:\> go build -o mync.exe
16 | C:\> .\mync.exe http -output godev.output https://github.com
17 | 2021/11/13 11:25:26 url=https://github.com method=GET protocol=HTTP/1.1 latency=0.085177
18 | Data saved to: godev.output
19 | ```
--------------------------------------------------------------------------------
/chap4/exercise3/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
8 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
9 |
10 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
11 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
12 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
13 |
14 | type FlagParsingError struct {
15 | err error
16 | }
17 |
18 | func (e FlagParsingError) Error() string {
19 | return e.err.Error()
20 | }
21 |
22 | type InvalidInputError struct {
23 | Err error
24 | }
25 |
26 | func (e InvalidInputError) Error() string {
27 | return e.Err.Error()
28 | }
29 |
--------------------------------------------------------------------------------
/chap4/exercise3/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return FlagParsingError{err}
36 | }
37 | if fs.NArg() != 1 {
38 | return InvalidInputError{ErrNoServerSpecified}
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap4/exercise3/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap4/exercise3/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap4/exercise3
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap4/exercise3/handle_command_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestHandleCommand(t *testing.T) {
9 | usageMessage := `Usage: mync [http|grpc] -h
10 |
11 | http: A HTTP client.
12 |
13 | http: server
14 |
15 | Options:
16 | -basicauth string
17 | Add basic auth (username:password) credentials to the outgoing request
18 | -body string
19 | JSON data for HTTP POST request
20 | -body-file string
21 | File containing JSON data for HTTP POST request
22 | -disable-redirect
23 | Do not follow redirection request
24 | -header value
25 | Add one or more headers to the outgoing request (key=value)
26 | -output string
27 | File path to write the response into
28 | -verb string
29 | HTTP method (default "GET")
30 |
31 | grpc: A gRPC client.
32 |
33 | grpc: server
34 |
35 | Options:
36 | -body string
37 | Body of request
38 | -method string
39 | Method to call
40 | `
41 | testConfigs := []struct {
42 | args []string
43 | output string
44 | err error
45 | }{
46 | {
47 | args: []string{},
48 | err: errInvalidSubCommand,
49 | output: "Invalid sub-command specified\n" + usageMessage,
50 | },
51 | {
52 | args: []string{"-h"},
53 | err: nil,
54 | output: usageMessage,
55 | },
56 | {
57 | args: []string{"foo"},
58 | err: errInvalidSubCommand,
59 | output: "Invalid sub-command specified\n" + usageMessage,
60 | },
61 | }
62 |
63 | byteBuf := new(bytes.Buffer)
64 | for _, tc := range testConfigs {
65 | err := handleCommand(byteBuf, tc.args)
66 | if tc.err == nil && err != nil {
67 | t.Fatalf("Expected nil error, got %v", err)
68 | }
69 |
70 | if tc.err != nil && err.Error() != tc.err.Error() {
71 | t.Fatalf("Expected error %v, got %v", tc.err, err)
72 | }
73 |
74 | if len(tc.output) != 0 {
75 | gotOutput := byteBuf.String()
76 | if tc.output != gotOutput {
77 | t.Errorf("Expected output to be: %s, Got: %s", tc.output, gotOutput)
78 | }
79 | }
80 | byteBuf.Reset()
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/chap4/exercise3/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap4/exercise3/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
37 | }
38 | }
39 |
40 | // for non-nil errors, we can have three kinds of errors
41 | // 1. Flag parsing error (E.g. use of invalid option)
42 | // 2. Invalid input error (E.g. JSON body specified for a GET request)
43 | // 3. Application specific error (E.g. remote server returned an error for example)
44 | // For (1), the flag package will show the error and also print the usage, so we don't do anything here
45 | // For (2), the we want to show the error and print the usage of the program
46 | // For (3), we only want to show the error
47 | if err != nil {
48 | if !errors.As(err, &cmd.FlagParsingError{}) {
49 | fmt.Fprintln(w, err.Error())
50 | }
51 | if errors.As(err, &cmd.InvalidInputError{}) {
52 | printUsage(w)
53 | }
54 | }
55 | return err
56 | }
57 |
58 | func main() {
59 | err := handleCommand(os.Stdout, os.Args[1:])
60 | if err != nil {
61 | os.Exit(1)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chap4/exercise3/middleware/httpLatency.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | type HttpLatencyClient struct {
10 | Logger *log.Logger
11 | }
12 |
13 | func (c HttpLatencyClient) RoundTrip(
14 | r *http.Request,
15 | ) (*http.Response, error) {
16 | startTime := time.Now()
17 | resp, err := http.DefaultTransport.RoundTrip(r)
18 | c.Logger.Printf(
19 | "url=%s method=%s protocol=%s latency=%f\n",
20 | r.URL, r.Method, r.Proto, time.Since(startTime).Seconds(),
21 | )
22 | return resp, err
23 | }
24 |
--------------------------------------------------------------------------------
/chap4/exercise3/test.json:
--------------------------------------------------------------------------------
1 | {"id": 1}
--------------------------------------------------------------------------------
/chap4/exercise4/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 4.4
2 |
3 | This is my workflow in creating the solution:
4 |
5 | - Copy all the code from solution of chapter 4, exercise 3.
6 | - Rename go module to: `github.com/practicalgo/book-exercise-solutions/chap4/exercise4`
7 | by editing the `go.mod` file and update all package imports accordingly.
8 | - Add the new options
9 | - Update httpCmd.go to add the logic for those options
10 | - Updated the validation to exit with an error if output-file is specified with number of requests greater than 1
11 | - The latency middleware is now defined as:
12 |
13 | ```
14 | type HttpLatencyClient struct {
15 | Logger *log.Logger
16 | Transport http.RoundTripper
17 | }
18 |
19 | func (c HttpLatencyClient) RoundTrip(
20 | r *http.Request,
21 | ) (*http.Response, error) {
22 | startTime := time.Now()
23 | resp, err := c.Transport.RoundTrip(r)
24 | c.Logger.Printf(
25 | "url=%s method=%s protocol=%s latency=%f\n",
26 | r.URL, r.Method, r.Proto, time.Since(startTime).Seconds(),
27 | )
28 | return resp, err
29 | }
30 |
31 | ```
32 |
33 | In `httpCmd.go`, the client is now created as:
34 |
35 | ```
36 | // This is created with all the parameters specified
37 | // when creating http.DefaultTransport, but we configure the
38 | // MaxIdleConns as per user input
39 | t := &http.Transport{
40 | Proxy: http.ProxyFromEnvironment,
41 | DialContext: (&net.Dialer{
42 | Timeout: 30 * time.Second,
43 | KeepAlive: 30 * time.Second,
44 | }).DialContext,
45 | ForceAttemptHTTP2: true,
46 | MaxIdleConns: c.maxIdleConns,
47 | IdleConnTimeout: 90 * time.Second,
48 | TLSHandshakeTimeout: 10 * time.Second,
49 | ExpectContinueTimeout: 1 * time.Second,
50 | }
51 | httpLatencyMiddleware := middleware.HttpLatencyClient{
52 | Logger: log.New(os.Stdout, "", log.LstdFlags),
53 | Transport: t,
54 | }
55 | httpClient = http.Client{
56 | CheckRedirect: redirectPolicyFunc,
57 | Transport: httpLatencyMiddleware,
58 | }
59 | ```
--------------------------------------------------------------------------------
/chap4/exercise4/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
8 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
9 |
10 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
11 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
12 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
13 |
14 | type FlagParsingError struct {
15 | err error
16 | }
17 |
18 | func (e FlagParsingError) Error() string {
19 | return e.err.Error()
20 | }
21 |
22 | type InvalidInputError struct {
23 | Err error
24 | }
25 |
26 | func (e InvalidInputError) Error() string {
27 | return e.Err.Error()
28 | }
29 |
--------------------------------------------------------------------------------
/chap4/exercise4/cmd/grpcCmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type grpcConfig struct {
10 | server string
11 | method string
12 | body string
13 | }
14 |
15 | func HandleGrpc(w io.Writer, args []string) error {
16 | c := grpcConfig{}
17 | fs := flag.NewFlagSet("grpc", flag.ContinueOnError)
18 | fs.SetOutput(w)
19 | fs.StringVar(&c.method, "method", "", "Method to call")
20 | fs.StringVar(&c.body, "body", "", "Body of request")
21 | fs.Usage = func() {
22 | var usageString = `
23 | grpc: A gRPC client.
24 |
25 | grpc: server`
26 | fmt.Fprintf(w, usageString)
27 | fmt.Fprintln(w)
28 | fmt.Fprintln(w)
29 | fmt.Fprintln(w, "Options: ")
30 | fs.PrintDefaults()
31 | }
32 |
33 | err := fs.Parse(args)
34 | if err != nil {
35 | return FlagParsingError{err}
36 | }
37 | if fs.NArg() != 1 {
38 | return InvalidInputError{ErrNoServerSpecified}
39 | }
40 | c.server = fs.Arg(0)
41 | fmt.Fprintln(w, "Executing grpc command")
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/chap4/exercise4/cmd/handle_grpc_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestHandleGrpc(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -body string
17 | Body of request
18 | -method string
19 | Method to call
20 | `
21 | testConfigs := []struct {
22 | args []string
23 | err error
24 | output string
25 | }{
26 | {
27 | args: []string{},
28 | err: ErrNoServerSpecified,
29 | },
30 |
31 | {
32 | args: []string{"-h"},
33 | err: errors.New("flag: help requested"),
34 | output: usageMessage,
35 | },
36 |
37 | {
38 | args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"},
39 | err: nil,
40 | output: "Executing grpc command\n",
41 | },
42 | }
43 |
44 | byteBuf := new(bytes.Buffer)
45 | for _, tc := range testConfigs {
46 | err := HandleGrpc(byteBuf, tc.args)
47 | if tc.err == nil && err != nil {
48 | t.Fatalf("Expected nil error, got %v", err)
49 | }
50 |
51 | if tc.err != nil && err.Error() != tc.err.Error() {
52 | t.Fatalf("Expected error %v, got %v", tc.err, err)
53 | }
54 |
55 | if len(tc.output) != 0 {
56 | gotOutput := byteBuf.String()
57 | if tc.output != gotOutput {
58 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
59 | }
60 | }
61 |
62 | byteBuf.Reset()
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/chap4/exercise4/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap4/exercise4
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap4/exercise4/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap4/exercise4/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
37 | }
38 | }
39 |
40 | // for non-nil errors, we can have three kinds of errors
41 | // 1. Flag parsing error (E.g. use of invalid option)
42 | // 2. Invalid input error (E.g. JSON body specified for a GET request)
43 | // 3. Application specific error (E.g. remote server returned an error for example)
44 | // For (1), the flag package will show the error and also print the usage, so we don't do anything here
45 | // For (2), the we want to show the error and print the usage of the program
46 | // For (3), we only want to show the error
47 | if err != nil {
48 | if !errors.As(err, &cmd.FlagParsingError{}) {
49 | fmt.Fprintln(w, err.Error())
50 | }
51 | if errors.As(err, &cmd.InvalidInputError{}) {
52 | printUsage(w)
53 | }
54 | }
55 | return err
56 | }
57 |
58 | func main() {
59 | err := handleCommand(os.Stdout, os.Args[1:])
60 | if err != nil {
61 | os.Exit(1)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chap4/exercise4/middleware/httpLatency.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | type HttpLatencyClient struct {
10 | Logger *log.Logger
11 | Transport http.RoundTripper
12 | }
13 |
14 | func (c HttpLatencyClient) RoundTrip(
15 | r *http.Request,
16 | ) (*http.Response, error) {
17 | startTime := time.Now()
18 | resp, err := c.Transport.RoundTrip(r)
19 | c.Logger.Printf(
20 | "url=%s method=%s protocol=%s latency=%f\n",
21 | r.URL, r.Method, r.Proto, time.Since(startTime).Seconds(),
22 | )
23 | return resp, err
24 | }
25 |
--------------------------------------------------------------------------------
/chap4/exercise4/test.json:
--------------------------------------------------------------------------------
1 | {"id": 1}
--------------------------------------------------------------------------------
/chap5/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap5/.emptyfile
--------------------------------------------------------------------------------
/chap5/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 5.1
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Copy all the code from (Listing 5.2 and 5.3) `chap5/http-serve-mux`
6 | 2. Update the go module name to: `github.com/practicalgo/book-exercise-solutions/chap5/exercise1`
7 | 3. Define a new struct type for storing a log line and a function for emitting the log:
8 |
9 | ```
10 | type logLine struct {
11 | URL string `json:"url"`
12 | Method string `json:"method"`
13 | ContentLength int64 `json:"content_length"`
14 | Protocol string `json:"protocol"`
15 | }
16 |
17 | func logRequest(req *http.Request) {
18 | l := logLine{
19 | URL: req.URL.String(),
20 | Method: req.Method,
21 | ContentLength: req.ContentLength,
22 | Protocol: req.Proto,
23 | }
24 | data, err := json.Marshal(&l)
25 | if err != nil {
26 | panic(err)
27 | }
28 | log.Println(string(data))
29 | }
30 | ```
31 |
32 | 4. Update handler functions to call the `logRequest()` function before processing the request
33 | 5. I also added a "catch all" handler function so that requests for path other than `/api`
34 | and `/healthcheck` are also logged
--------------------------------------------------------------------------------
/chap5/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap5/exercise1
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap5/exercise1/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "os"
9 | )
10 |
11 | type logLine struct {
12 | URL string `json:"url"`
13 | Method string `json:"method"`
14 | ContentLength int64 `json:"content_length"`
15 | Protocol string `json:"protocol"`
16 | }
17 |
18 | func logRequest(req *http.Request) {
19 | l := logLine{
20 | URL: req.URL.String(),
21 | Method: req.Method,
22 | ContentLength: req.ContentLength,
23 | Protocol: req.Proto,
24 | }
25 | data, err := json.Marshal(&l)
26 | if err != nil {
27 | panic(err)
28 | }
29 | log.Println(string(data))
30 | }
31 |
32 | func apiHandler(w http.ResponseWriter, req *http.Request) {
33 | logRequest(req)
34 | fmt.Fprintf(w, "Hello, world!")
35 | }
36 |
37 | func healthCheckHandler(w http.ResponseWriter, req *http.Request) {
38 | logRequest(req)
39 | fmt.Fprintf(w, "ok")
40 | }
41 |
42 | func catchAllHandler(w http.ResponseWriter, req *http.Request) {
43 | logRequest(req)
44 | fmt.Fprintf(w, "your request was processed by the catch all handler")
45 | }
46 |
47 | func setupHandlers(mux *http.ServeMux) {
48 | mux.HandleFunc("/healthz", healthCheckHandler)
49 | mux.HandleFunc("/api", apiHandler)
50 | mux.HandleFunc("/", catchAllHandler)
51 | }
52 |
53 | func main() {
54 |
55 | listenAddr := os.Getenv("LISTEN_ADDR")
56 | if len(listenAddr) == 0 {
57 | listenAddr = ":8080"
58 | }
59 |
60 | mux := http.NewServeMux()
61 | setupHandlers(mux)
62 |
63 | log.Fatal(http.ListenAndServe(listenAddr, mux))
64 | }
65 |
--------------------------------------------------------------------------------
/chap5/exercise1/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "log"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 | )
10 |
11 | func TestServer(t *testing.T) {
12 |
13 | tests := []struct {
14 | name string
15 | path string
16 | expected string
17 | }{
18 | {
19 | name: "index",
20 | path: "/api",
21 | expected: "Hello, world!",
22 | },
23 | {name: "healthcheck",
24 | path: "/healthz",
25 | expected: "ok",
26 | },
27 | }
28 |
29 | mux := http.NewServeMux()
30 | setupHandlers(mux)
31 |
32 | ts := httptest.NewServer(mux)
33 | defer ts.Close()
34 |
35 | for _, tc := range tests {
36 | t.Run(tc.name, func(t *testing.T) {
37 | resp, err := http.Get(ts.URL + tc.path)
38 | respBody, err := io.ReadAll(resp.Body)
39 | resp.Body.Close()
40 | if err != nil {
41 | log.Fatal(err)
42 | }
43 | if string(respBody) != tc.expected {
44 | t.Errorf(
45 | "Expected: %s, Got: %s",
46 | tc.expected, string(respBody),
47 | )
48 | }
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chap5/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 5.2
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Copy all the code from (`chap5/streaming-decode`)
6 | 2. Update the go module name to: `github.com/practicalgo/book-exercise-solutions/chap5/exercise2`
7 | 3. We will create a new test function to verify the behavior we will implement in this exericse:
8 |
9 | ```
10 | func Test_DecodeUnknownFieldError(t *testing.T) {
11 | const jsonStream = `
12 | {"user_ip": "172.121.19.21", "event": "click_on_add_cart", "user_data":"some_data"}{"user_ip": "172.121.19.21", "event": "click_on_checkout"}
13 | `
14 | body := strings.NewReader(jsonStream)
15 |
16 | r := httptest.NewRequest("POST", "http://example.com/decode", body)
17 | w := httptest.NewRecorder()
18 |
19 | decodeHandler(w, r)
20 |
21 | if w.Result().StatusCode != http.StatusBadRequest {
22 | t.Fatalf("Expected Response Status: %v, Got: %v", http.StatusBadRequest, w.Result().StatusCode)
23 | }
24 | }
25 | ```
26 |
27 | Run the test, and see it fail.
28 |
29 | 4. Update `decodeHandler()` function to call the method `DisallowUnknownFields()` after
30 | creating the decoder:
31 |
32 | ```
33 | dec := json.NewDecoder(r.Body)
34 | dec.DisallowUnknownFields()
35 | ```
36 |
37 | The test above should now pass.
--------------------------------------------------------------------------------
/chap5/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap5/streaming-decode
2 |
3 | go 1.15
4 |
--------------------------------------------------------------------------------
/chap5/exercise2/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | type logLine struct {
11 | UserIP string `json:"user_ip"`
12 | Event string `json:"event"`
13 | }
14 |
15 | func decodeHandler(w http.ResponseWriter, r *http.Request) {
16 | dec := json.NewDecoder(r.Body)
17 | dec.DisallowUnknownFields()
18 | for {
19 | var l logLine
20 | err := dec.Decode(&l)
21 | if err == io.EOF {
22 | break
23 | }
24 | if err != nil {
25 | http.Error(w, err.Error(), http.StatusBadRequest)
26 | return
27 | }
28 | fmt.Println(l.UserIP, l.Event)
29 | }
30 | fmt.Fprintf(w, "OK")
31 | }
32 |
33 | func main() {
34 |
35 | mux := http.NewServeMux()
36 | mux.HandleFunc("/decode", decodeHandler)
37 |
38 | http.ListenAndServe(":8080", mux)
39 | }
40 |
--------------------------------------------------------------------------------
/chap5/exercise2/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func Test_DecodeHandler(t *testing.T) {
11 | const jsonStream = `
12 | {"user_ip": "172.121.19.21", "event": "click_on_add_cart"}{"user_ip": "172.121.19.21", "event": "click_on_checkout"}
13 | `
14 | body := strings.NewReader(jsonStream)
15 |
16 | r := httptest.NewRequest("POST", "http://example.com/decode", body)
17 | w := httptest.NewRecorder()
18 |
19 | decodeHandler(w, r)
20 |
21 | if w.Result().StatusCode != http.StatusOK {
22 | t.Fatalf("Expected Response Status: %v, Got: %v", http.StatusOK, w.Result().StatusCode)
23 | }
24 | }
25 |
26 | func Test_DecodeUnknownFieldError(t *testing.T) {
27 | const jsonStream = `
28 | {"user_ip": "172.121.19.21", "event": "click_on_add_cart", "user_data":"some_data"}{"user_ip": "172.121.19.21", "event": "click_on_checkout"}
29 | `
30 | body := strings.NewReader(jsonStream)
31 |
32 | r := httptest.NewRequest("POST", "http://example.com/decode", body)
33 | w := httptest.NewRecorder()
34 |
35 | decodeHandler(w, r)
36 |
37 | if w.Result().StatusCode != http.StatusBadRequest {
38 | t.Fatalf("Expected Response Status: %v, Got: %v", http.StatusBadRequest, w.Result().StatusCode)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/chap5/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 5.3
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Initialize a new go module `github.com/practicalgo/book-exercise-solutions/chap5/exercise3`
6 | 2. Define the server in server.go
7 |
8 | Test the functionality:
9 |
10 | ```
11 | $ go build
12 | ```
13 |
14 | Make a request to, `http://localhost:8080/download?filename=go.mod`
15 |
16 | **Note**
17 |
18 | It's worth noting here, this is in-general vulnerable to path traversal
19 | attacks. Hence, we place a couple of guards around it:
20 |
21 | 1. We strip away any path separators from the value of `fileName` query parameter. This
22 | restricts the file lookup to happen only in the current directory
23 | 2. We also do not allow filenames starting with "."
24 |
25 | But, there are likely other ways to exploit this.
--------------------------------------------------------------------------------
/chap5/exercise3/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap5/exercise3
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap5/exercise3/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "log"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 | )
12 |
13 | func downloadFileHandler(w http.ResponseWriter, r *http.Request) {
14 |
15 | v := r.URL.Query()
16 | fileName := v.Get("fileName")
17 | if len(fileName) == 0 {
18 | http.Error(w, "fileName query parameter not specified", http.StatusBadRequest)
19 | return
20 | }
21 |
22 | // See README.md
23 | fileName = filepath.Base(fileName)
24 | if strings.HasPrefix(fileName, ".") {
25 | http.Error(w, "Invalid fileName specified", http.StatusBadRequest)
26 | return
27 | }
28 |
29 | f, err := os.Open(fileName)
30 | if err != nil {
31 | http.Error(w, "Error opening file", http.StatusInternalServerError)
32 | return
33 | }
34 | defer f.Close()
35 |
36 | buffer := make([]byte, 512)
37 | _, err = f.Read(buffer)
38 | if err != nil {
39 | http.Error(w, "Error reading file", http.StatusInternalServerError)
40 | return
41 | }
42 | f.Seek(0, 0)
43 | contentType := http.DetectContentType(buffer)
44 |
45 | log.Println(contentType)
46 | w.Header().Set("Content-Type", contentType)
47 | w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
48 |
49 | io.Copy(w, f)
50 | }
51 |
52 | func main() {
53 | listenAddr := os.Getenv("LISTEN_ADDR")
54 | if len(listenAddr) == 0 {
55 | listenAddr = ":8080"
56 | }
57 |
58 | mux := http.NewServeMux()
59 | mux.HandleFunc("/download", downloadFileHandler)
60 | err := http.ListenAndServe(listenAddr, mux)
61 | if err != nil {
62 | log.Fatalf("Server could not start listening on %s. Error: %v", listenAddr, err)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/chap6/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap6/.emptyfile
--------------------------------------------------------------------------------
/chap6/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 6.1
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Create a new go module `github.com/practicalgo/book-exercise-solutions/chap6/exercise1`
6 | 2. Copy server.go from `chap6/http-handler-type`
7 | 3. The exercise only mentions that you return an error from the handler functions. However, if you
8 | only return an error, you end up losing the HTTP status code that should accompany the error response.
9 | Hence, we update the `app` type as follows:
10 |
11 | ```
12 | type app struct {
13 | config appConfig
14 | handler func(w http.ResponseWriter, r *http.Request, config appConfig) (int, error)
15 | }
16 | ```
17 |
18 | In the `ServeHTTP()` method we then process these returned values as follows:
19 |
20 | ```
21 | status, err := a.handler(w, r, a.config)
22 | if err != nil {
23 | log.Printf("response_status=%d error=%s\n", status, err.Error())
24 | http.Error(w, err.Error(), status)
25 | return
26 | }
27 | ```
28 |
29 |
30 | 4. Demonstration:
31 |
32 | Send a POST request to `/healthz` path and you will get back a HTTP 405 error response:
33 |
34 | ```
35 | $ curl --request POST 192.168.1.109:8080/healthz
36 | invalid request method:POST
37 | ```
38 |
39 | On the server, you will see the following log lines:
40 |
41 | ```
42 | 2021/11/20 17:39:28 server.go:38: Handling healthcheck request
43 | 2021/11/20 17:39:46 response_status=405 error=invalid request method:POST
44 | ```
--------------------------------------------------------------------------------
/chap6/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap6/exercise1
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap6/exercise1/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 | )
9 |
10 | type appConfig struct {
11 | logger *log.Logger
12 | }
13 |
14 | type app struct {
15 | config appConfig
16 | handler func(w http.ResponseWriter, r *http.Request, config appConfig) (int, error)
17 | }
18 |
19 | func (a app) ServeHTTP(w http.ResponseWriter, r *http.Request) {
20 | status, err := a.handler(w, r, a.config)
21 | if err != nil {
22 | log.Printf("response_status=%d error=%s\n", status, err.Error())
23 | http.Error(w, err.Error(), status)
24 | return
25 | }
26 | }
27 |
28 | func apiHandler(w http.ResponseWriter, r *http.Request, config appConfig) (int, error) {
29 | config.logger.Println("Handling API request")
30 | fmt.Fprintf(w, "Hello, world!")
31 | return http.StatusOK, nil
32 | }
33 |
34 | func healthCheckHandler(w http.ResponseWriter, r *http.Request, config appConfig) (int, error) {
35 | if r.Method != "GET" {
36 | return http.StatusMethodNotAllowed, fmt.Errorf("invalid request method:%s", r.Method)
37 | }
38 | config.logger.Println("Handling healthcheck request")
39 | fmt.Fprintf(w, "ok")
40 | return http.StatusOK, nil
41 | }
42 |
43 | func setupHandlers(mux *http.ServeMux, config appConfig) {
44 | mux.Handle("/healthz", &app{config: config, handler: healthCheckHandler})
45 | mux.Handle("/api", &app{config: config, handler: apiHandler})
46 | }
47 |
48 | func main() {
49 |
50 | listenAddr := os.Getenv("LISTEN_ADDR")
51 | if len(listenAddr) == 0 {
52 | listenAddr = ":8080"
53 | }
54 |
55 | config := appConfig{
56 | logger: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile),
57 | }
58 |
59 | mux := http.NewServeMux()
60 | setupHandlers(mux, config)
61 |
62 | log.Fatal(http.ListenAndServe(listenAddr, mux))
63 | }
64 |
--------------------------------------------------------------------------------
/chap6/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 6.2
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Create a new go module `github.com/practicalgo/book-exercise-solutions/chap6/exercise2`
6 | 2. Copy server.go from `chap6/middleware-chaining`
7 | 3. Update the logging middleware to log the request ID
8 | 4. Add a new middleware to add the request ID
9 | 5. I have used an external package: `github.com/google/uuid` to create a New UUID as a string and add that
10 | as the request ID
11 | 6. Update the `main()` function to also add the request middleware to the chain.
12 |
13 | In the book I write that the innermost middleware is executed first when processing
14 | a request. Unfortunately, it isn't accurate. The request flows from the outermost
15 | middleware to the innermost middleware in it's journey from the user to the application.
16 | On it's way back, it's the reverse journey - which is what I am describing in the book.
17 | Since we want to add our request ID before we log or handle panic, we add the new middleware
18 | as the first middleware.
--------------------------------------------------------------------------------
/chap6/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap6/exercise2
2 |
3 | go 1.17
4 |
5 | require github.com/google/uuid v1.3.0
6 |
--------------------------------------------------------------------------------
/chap6/exercise2/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
2 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3 |
--------------------------------------------------------------------------------
/chap6/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 6.3
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Create a new go module `github.com/practicalgo/book-exercise-solutions/chap6/exercise3`
6 | 2. Copy all the `.go` files (including the subdirectories) from `chap6/complex-server`)
7 | 3. Add a new test function to `handlers/handlers_test.go` for the health check handler
8 | 4. Since we want to test the behavior of the handler function for multiple HTTP request methods,
9 | we adopt a table driven testing approach and create a list of test configurations as follows:
10 |
11 | ```
12 | testConfigs := []struct {
13 | httpMethod string
14 | expectedStatus int
15 | expectedResponseBody string
16 | }{
17 | {
18 | httpMethod: "GET",
19 | expectedStatus: http.StatusOK,
20 | expectedResponseBody: "ok",
21 | },
22 | {
23 | httpMethod: "POST",
24 | expectedStatus: http.StatusMethodNotAllowed,
25 | expectedResponseBody: "Method not allowed\n",
26 | },
27 | {
28 | httpMethod: "PUT",
29 | expectedStatus: http.StatusMethodNotAllowed,
30 | expectedResponseBody: "Method not allowed\n",
31 | },
32 | }
33 | ```
--------------------------------------------------------------------------------
/chap6/exercise3/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "io"
5 | "log"
6 | )
7 |
8 | type AppConfig struct {
9 | Logger *log.Logger
10 | }
11 |
12 | func InitConfig(w io.Writer) AppConfig {
13 | return AppConfig{
14 | Logger: log.New(w, "", log.Ldate|log.Ltime|log.Lshortfile),
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/chap6/exercise3/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap6/exercise3
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap6/exercise3/handlers/handlers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/config"
8 | )
9 |
10 | type app struct {
11 | conf config.AppConfig
12 | handler func(w http.ResponseWriter, r *http.Request, conf config.AppConfig)
13 | }
14 |
15 | func (a app) ServeHTTP(w http.ResponseWriter, r *http.Request) {
16 | a.handler(w, r, a.conf)
17 | }
18 |
19 | func apiHandler(w http.ResponseWriter, r *http.Request, conf config.AppConfig) {
20 | fmt.Fprintf(w, "Hello, world!")
21 | }
22 |
23 | func healthCheckHandler(w http.ResponseWriter, r *http.Request, conf config.AppConfig) {
24 | if r.Method != "GET" {
25 | conf.Logger.Printf("error=\"Invalid request\" path=%s method=%s", r.URL.Path, r.Method)
26 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
27 | return
28 | }
29 | fmt.Fprintf(w, "ok")
30 | }
31 |
32 | func panicHandler(w http.ResponseWriter, r *http.Request, conf config.AppConfig) {
33 | panic("I panicked")
34 | }
35 |
--------------------------------------------------------------------------------
/chap6/exercise3/handlers/register.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/config"
7 | )
8 |
9 | func Register(mux *http.ServeMux, conf config.AppConfig) {
10 | mux.Handle("/healthz", &app{conf: conf, handler: healthCheckHandler})
11 | mux.Handle("/api", &app{conf: conf, handler: apiHandler})
12 | mux.Handle("/panic", &app{conf: conf, handler: panicHandler})
13 | }
14 |
--------------------------------------------------------------------------------
/chap6/exercise3/middleware/middleware.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/config"
9 | )
10 |
11 | func loggingMiddleware(h http.Handler, c config.AppConfig) http.Handler {
12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13 | t1 := time.Now()
14 | h.ServeHTTP(w, r)
15 | requestDuration := time.Now().Sub(t1).Seconds()
16 | c.Logger.Printf("protocol=%s path=%s method=%s duration=%f", r.Proto, r.URL.Path, r.Method, requestDuration)
17 | })
18 | }
19 |
20 | func panicMiddleware(h http.Handler, c config.AppConfig) http.Handler {
21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 | defer func() {
23 | if rValue := recover(); rValue != nil {
24 | c.Logger.Println("panic detected", rValue)
25 | w.WriteHeader(http.StatusInternalServerError)
26 | fmt.Fprintf(w, "Unexpected server error occured")
27 | }
28 | }()
29 | h.ServeHTTP(w, r)
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/chap6/exercise3/middleware/middleware_test.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 |
10 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/config"
11 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/handlers"
12 | )
13 |
14 | func TestPanicMiddleware(t *testing.T) {
15 | b := new(bytes.Buffer)
16 | c := config.InitConfig(b)
17 |
18 | m := http.NewServeMux()
19 | handlers.Register(m, c)
20 |
21 | h := panicMiddleware(m, c)
22 |
23 | r := httptest.NewRequest("GET", "/panic", nil)
24 | w := httptest.NewRecorder()
25 | h.ServeHTTP(w, r)
26 |
27 | resp := w.Result()
28 |
29 | body, err := io.ReadAll(resp.Body)
30 | if err != nil {
31 | t.Fatalf("Error reading response body: %v", err)
32 | }
33 |
34 | if resp.StatusCode != http.StatusInternalServerError {
35 | t.Errorf("Expected response status: %v, Got: %v\n", http.StatusOK, resp.StatusCode)
36 | }
37 |
38 | expectedResponseBody := "Unexpected server error occured"
39 |
40 | if string(body) != expectedResponseBody {
41 | t.Errorf("Expected response: %s, Got: %s\n", expectedResponseBody, string(body))
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/chap6/exercise3/middleware/register.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/config"
7 | )
8 |
9 | func RegisterMiddleware(mux *http.ServeMux, c config.AppConfig) http.Handler {
10 | return loggingMiddleware(panicMiddleware(mux, c), c)
11 | }
12 |
--------------------------------------------------------------------------------
/chap6/exercise3/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "log"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/config"
10 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/handlers"
11 | "github.com/practicalgo/book-exercise-solutions/chap6/exercise3/middleware"
12 | )
13 |
14 | func setupServer(mux *http.ServeMux, w io.Writer) http.Handler {
15 | conf := config.InitConfig(w)
16 |
17 | handlers.Register(mux, conf)
18 | return middleware.RegisterMiddleware(mux, conf)
19 | }
20 |
21 | func main() {
22 |
23 | listenAddr := os.Getenv("LISTEN_ADDR")
24 | if len(listenAddr) == 0 {
25 | listenAddr = ":8080"
26 | }
27 |
28 | mux := http.NewServeMux()
29 | wrappedMux := setupServer(mux, os.Stdout)
30 |
31 | log.Fatal(http.ListenAndServe(listenAddr, wrappedMux))
32 | }
33 |
--------------------------------------------------------------------------------
/chap6/exercise3/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "net/http"
7 | "net/http/httptest"
8 | "strings"
9 | "testing"
10 | )
11 |
12 | func TestSetupServer(t *testing.T) {
13 | b := new(bytes.Buffer)
14 | mux := http.NewServeMux()
15 | wrappedMux := setupServer(mux, b)
16 | ts := httptest.NewServer(wrappedMux)
17 | defer ts.Close()
18 |
19 | resp, err := http.Get(ts.URL + "/panic")
20 | if err != nil {
21 | t.Fatal(err)
22 | }
23 | defer resp.Body.Close()
24 | _, err = io.ReadAll(resp.Body)
25 | if err != nil {
26 | t.Error(err)
27 | }
28 |
29 | if resp.StatusCode != http.StatusInternalServerError {
30 | t.Errorf(
31 | "Expected response status to be: %v, Got: %v",
32 | http.StatusInternalServerError,
33 | resp.StatusCode,
34 | )
35 | }
36 |
37 | logs := b.String()
38 | expectedLogFragments := []string{
39 | "path=/panic method=GET duration=",
40 | "panic detected",
41 | }
42 | for _, log := range expectedLogFragments {
43 | if !strings.Contains(logs, log) {
44 | t.Errorf(
45 | "Expected logs to contain: %s, Got: %s",
46 | log, logs,
47 | )
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/chap7/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap7/.emptyfile
--------------------------------------------------------------------------------
/chap7/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 7.1
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Create a new go module `github.com/practicalgo/book-exercise-solutions/chap7/exercise1`
6 | 2. Copy server.go from `chap7/network-request-timeout`
7 | 3. Define a new function, `createHTTPGetRequestWithTrace()` to return a `*http.Request` object
8 | which will make a HTTP GET request and integrates `http.ClientTrace`.
9 | 4. Update `handleUserAPI()` function to use the above function to create a `*http.Request` request so
10 | as to make a HTTP GET request to the `/ping` path.
11 |
12 |
13 | Build and run the server:
14 |
15 | ```
16 | $ go build
17 | $ ./exercise1.exe
18 | ```
19 |
20 | When we make a request to `/api/users/`, we will get a HTTP 503 in the client, and the following
21 | logs in the server:
22 |
23 | ```
24 | 2021/11/22 07:53:13 I started processing the request
25 | 2021/11/22 07:53:15 Outgoing HTTP request
26 | 2021/11/22 07:53:15 Error making request: Get "http://localhost:8080/ping": context deadline exceeded
27 | ```
28 |
29 | There is nothing additional as a result of adding the client trace context - which tells us that none
30 | of the HTTP connection setup steps - DNS querying, for example, started.
31 |
32 | # Experimentation
33 |
34 | Now, update timeoutDuration in `main()` to be 5*time.Second. Build and run the server and make
35 | a request to the `/api/users/` endpoint. You will see the following server logs:
36 |
37 | ```
38 | 2021/11/22 08:00:02 I started processing the request
39 | 2021/11/22 08:00:04 Outgoing HTTP request
40 | DNS Start Info: {Host:localhost}
41 | DNS Done Info: {Addrs:[{IP:::1 Zone:} {IP:127.0.0.1 Zone:}] Err: Coalesced:false}
42 | Got Conn: {Conn:0xc000188008 Reused:false WasIdle:false IdleTime:0s}
43 | 2021/11/22 08:00:04 ping: Got a request
44 | Put Idle Conn Error:
45 | 2021/11/22 08:00:04 I finished processing the request
46 | ```
47 |
48 | 1. We first have the DNS record querying and response
49 | 2. Then, we get a connection to send the request
50 | 3. Then, the ping handler gets the request
51 | 4. the ping handler returns a response
52 | 5. The connection is put back into the pool
53 |
54 |
55 |
--------------------------------------------------------------------------------
/chap7/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap7/exercise1
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap7/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 7.2
2 |
3 | This is my workflow in creating the solution:
4 |
5 | 1. Create a new go module `github.com/practicalgo/book-exercise-solutions/chap7/exercise1`
6 | 2. Copy server.go from `chap7/client-disconnect-handling`
7 | 3. Refactor the server code so that the users API handler now accepts a configurable logger as an argument
8 | 4. Also, updated the user api handler to accept the address of the server for the ping handler as a query parameter
9 | 4. Write the test with a client timeout configured and verify that the logs are as expected
--------------------------------------------------------------------------------
/chap7/exercise2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap7/exercise2
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap7/exercise2/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "log"
6 | "net/http"
7 | "net/http/httptest"
8 | "strings"
9 | "testing"
10 | "time"
11 | )
12 |
13 | func TestUserHandlerApiTimeout(t *testing.T) {
14 |
15 | logBuf := new(bytes.Buffer)
16 | timeoutDuration := 30 * time.Second
17 | logger := log.New(logBuf, "", log.Ldate|log.Ltime|log.Lshortfile)
18 |
19 | mux := http.NewServeMux()
20 | setupHandlers(mux, timeoutDuration, logger)
21 |
22 | ts := httptest.NewServer(mux)
23 | defer ts.Close()
24 |
25 | client := http.Client{
26 | Timeout: 4 * time.Second,
27 | }
28 | _, err := client.Get(ts.URL + "/api/users/" + "?ping_server=" + ts.URL)
29 | if err == nil {
30 | t.Fatalf("Expected nil error, got:%v", err)
31 | }
32 | expectedServerLogLine := "Aborting request processing: context canceled"
33 | if !strings.Contains(logBuf.String(), expectedServerLogLine) {
34 | t.Fatalf("Expected server log to contain: %s\n Got: %s", expectedServerLogLine, logBuf.String())
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/chap8/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap8/.emptyfile
--------------------------------------------------------------------------------
/chap8/exercise1/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 8.1
2 |
3 | This is my workflow in creating the solution:
4 |
5 | - Copy all the code from solution of chapter 4, exercise 4.
6 | - Rename go module to: `github.com/practicalgo/book-exercise-solutions/chap8/exercise1`
7 | by editing the `go.mod` file and update all package imports accordingly.
8 | - Copy `service` directory from `chap8/user-service`
9 | - Update `grpCmd.go` to make a call to the users service
10 | - Write tests - i have added example tests to demonstrate the various ways of testing the grpc sub-command
11 | - `cmd/grpc_flag_parsing_test.go` - this only tests the option parsing behavior
12 | - `cmd/handle_grpc_users_svc_test.go` - this only tests the behavior when the server only has the users service registered (
13 | similar to Listing 8.6) and we use a bufconn listener
14 | - `cmd/handle_grpc_cmd_test.go` - this tests the behavior by creating a "real" gRPC server
15 |
16 | Usage:
17 |
18 | Valid request:
19 |
20 | ```
21 | C:\> .\exercise1.exe grpc -service Users -method GetUser -request '{\"email\":\"john@doe1.com\",\"id\":\"user-123\"}' localhost:50051
22 | ...
23 | ```
24 |
25 | Invalid request:
26 |
27 | ```
28 | C:\> .\exercise1.exe grpc -service Users -method GetUser -request '{\"email\":\"john@doe.com\",\"id1\":\"user-123\"}' localhost:50051
29 | proto: (line 1:25): unknown field "id1"
30 | Usage: mync [http|grpc] -h
31 |
32 | .. Usage message ..
33 | ```
34 |
35 | # JSON Pretty printing
36 |
37 | ```
38 | C:\> .\exercise1.exe grpc -service Users -method GetUser -request '{\"email\":\"john@doe.com\",\"id\":\"user-123\"}' -pretty-print localhost:50051
39 | {
40 | "user": {
41 | "id": "user-123",
42 | "firstName": "john",
43 | "lastName": "doe.com",
44 | "age": 36
45 | }
46 | }
47 | ```
--------------------------------------------------------------------------------
/chap8/exercise1/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
8 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
9 | var ErrInvalidGrpcMethod = errors.New("Invalid gRPC method")
10 |
11 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
12 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
13 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
14 |
15 | type FlagParsingError struct {
16 | err error
17 | }
18 |
19 | func (e FlagParsingError) Error() string {
20 | return e.err.Error()
21 | }
22 |
23 | type InvalidInputError struct {
24 | Err error
25 | }
26 |
27 | func (e InvalidInputError) Error() string {
28 | return e.Err.Error()
29 | }
30 |
--------------------------------------------------------------------------------
/chap8/exercise1/cmd/grpc_flag_parsing_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestGrpcCmdFlagParsing(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -method string
17 | Method to call
18 | -pretty-print
19 | Pretty print the JSON output
20 | -request string
21 | Request to send
22 | -service string
23 | gRPC service to send the request to
24 | `
25 |
26 | testConfigs := []struct {
27 | args []string
28 | output string
29 | err error
30 | }{
31 | {
32 | args: []string{},
33 | err: InvalidInputError{ErrNoServerSpecified},
34 | },
35 | {
36 | args: []string{"-h"},
37 | err: errors.New("flag: help requested"),
38 | output: usageMessage,
39 | },
40 | {
41 | args: []string{"-service", "Users", "localhost:50051"},
42 | err: errors.New("Invalid gRPC method"),
43 | },
44 | }
45 | byteBuf := new(bytes.Buffer)
46 | for i, tc := range testConfigs {
47 | t.Log(i)
48 | err := HandleGrpc(byteBuf, tc.args)
49 | if tc.err == nil && err != nil {
50 | t.Fatalf("Expected nil error, got %v", err)
51 | }
52 |
53 | if tc.err != nil && err == nil {
54 | t.Fatal("Expected non-nil error, got nil")
55 | }
56 |
57 | if tc.err != nil && err.Error() != tc.err.Error() {
58 | t.Fatalf("Expected error %v, got %v", tc.err, err)
59 | }
60 |
61 | if len(tc.output) != 0 {
62 | gotOutput := byteBuf.String()
63 | if tc.output != gotOutput {
64 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
65 | }
66 | }
67 | byteBuf.Reset()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/chap8/exercise1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise1
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/practicalgo/book-exercise-solutions/chap8/exercise1/service v0.0.0
7 | google.golang.org/grpc v1.37.0
8 |
9 | )
10 |
11 | require (
12 | github.com/golang/protobuf v1.5.0 // indirect
13 | golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect
15 | golang.org/x/text v0.3.0 // indirect
16 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
17 | google.golang.org/protobuf v1.26.0 // indirect
18 | )
19 |
20 | replace github.com/practicalgo/book-exercise-solutions/chap8/exercise1/service => ./service
21 |
--------------------------------------------------------------------------------
/chap8/exercise1/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap8/exercise1/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
37 | }
38 | }
39 |
40 | // for non-nil errors, we can have three kinds of errors
41 | // 1. Flag parsing error (E.g. use of invalid option)
42 | // 2. Invalid input error (E.g. JSON body specified for a GET request)
43 | // 3. Application specific error (E.g. remote server returned an error for example)
44 | // For (1), the flag package will show the error and also print the usage, so we don't do anything here
45 | // For (2), the we want to show the error and print the usage of the program
46 | // For (3), we only want to show the error
47 | if err != nil {
48 | if !errors.As(err, &cmd.FlagParsingError{}) {
49 | fmt.Fprintln(w, err.Error())
50 | }
51 | if errors.As(err, &cmd.InvalidInputError{}) {
52 | printUsage(w)
53 | }
54 | }
55 | return err
56 | }
57 |
58 | func main() {
59 | err := handleCommand(os.Stdout, os.Args[1:])
60 | if err != nil {
61 | os.Exit(1)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chap8/exercise1/middleware/httpLatency.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | type HttpLatencyClient struct {
10 | Logger *log.Logger
11 | Transport http.RoundTripper
12 | }
13 |
14 | func (c HttpLatencyClient) RoundTrip(
15 | r *http.Request,
16 | ) (*http.Response, error) {
17 | startTime := time.Now()
18 | resp, err := c.Transport.RoundTrip(r)
19 | c.Logger.Printf(
20 | "url=%s method=%s protocol=%s latency=%f\n",
21 | r.URL, r.Method, r.Proto, time.Since(startTime).Seconds(),
22 | )
23 | return resp, err
24 | }
25 |
--------------------------------------------------------------------------------
/chap8/exercise1/service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise1/service
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap8/exercise1/service/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap8/user-service/service";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | }
8 |
9 | message UserGetRequest {
10 | string email = 1;
11 | string id = 2;
12 | }
13 |
14 | message User {
15 | string id = 1;
16 | string first_name = 2;
17 | string last_name = 3;
18 | int32 age = 4;
19 | }
20 |
21 | message UserGetReply {
22 | User user = 1;
23 | }
24 |
--------------------------------------------------------------------------------
/chap8/exercise2/client-v2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise2/client
2 |
3 | go 1.17
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require (
8 | github.com/golang/protobuf v1.4.2 // indirect
9 | golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect
11 | golang.org/x/text v0.3.0 // indirect
12 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
13 | google.golang.org/protobuf v1.25.0 // indirect
14 | github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service v0.0.0
15 |
16 | )
17 |
18 | replace github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service => ../service-v2
19 |
--------------------------------------------------------------------------------
/chap8/exercise2/client-v2/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | users "github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service"
10 | "google.golang.org/grpc"
11 | )
12 |
13 | func setupGrpcConn(addr string) (*grpc.ClientConn, error) {
14 | return grpc.DialContext(
15 | context.Background(),
16 | addr,
17 | grpc.WithInsecure(),
18 | grpc.WithBlock(),
19 | )
20 | }
21 |
22 | func getUserServiceClient(conn *grpc.ClientConn) users.UsersClient {
23 | return users.NewUsersClient(conn)
24 | }
25 |
26 | func getUser(
27 | client users.UsersClient,
28 | u *users.UserGetRequest,
29 | ) (*users.UserGetReply, error) {
30 | return client.GetUser(context.Background(), u)
31 | }
32 |
33 | func main() {
34 | if len(os.Args) != 2 {
35 | log.Fatal(
36 | "Must specify a gRPC server address",
37 | )
38 | }
39 | conn, err := setupGrpcConn(os.Args[1])
40 | if err != nil {
41 | log.Fatal(err)
42 | }
43 | defer conn.Close()
44 |
45 | c := getUserServiceClient(conn)
46 |
47 | result, err := getUser(
48 | c,
49 | &users.UserGetRequest{Email: "jane@doe.com"},
50 | )
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 | fmt.Fprintf(os.Stdout, "%#v: %s\n", result.User, result.Location)
55 | }
56 |
--------------------------------------------------------------------------------
/chap8/exercise2/client-v2/user_client_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "net"
7 | "testing"
8 |
9 | users "github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/test/bufconn"
12 | )
13 |
14 | type dummyUserService struct {
15 | users.UnimplementedUsersServer
16 | }
17 |
18 | func (s *dummyUserService) GetUser(
19 | ctx context.Context,
20 | in *users.UserGetRequest,
21 | ) (*users.UserGetReply, error) {
22 | u := users.User{
23 | Id: "user-123-a",
24 | FirstName: "jane",
25 | LastName: "doe",
26 | Age: 36,
27 | }
28 | return &users.UserGetReply{User: &u}, nil
29 | }
30 |
31 | func startTestGrpcServer() (*grpc.Server, *bufconn.Listener) {
32 | l := bufconn.Listen(10)
33 | s := grpc.NewServer()
34 | users.RegisterUsersServer(s, &dummyUserService{})
35 | go func() {
36 | err := s.Serve(l)
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 | }()
41 | return s, l
42 | }
43 |
44 | func TestGetUser(t *testing.T) {
45 |
46 | s, l := startTestGrpcServer()
47 | defer s.GracefulStop()
48 |
49 | bufconnDialer := func(
50 | ctx context.Context, addr string,
51 | ) (net.Conn, error) {
52 | return l.Dial()
53 | }
54 |
55 | conn, err := grpc.DialContext(
56 | context.Background(),
57 | "", grpc.WithInsecure(),
58 | grpc.WithContextDialer(bufconnDialer),
59 | )
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 |
64 | c := getUserServiceClient(conn)
65 | result, err := getUser(
66 | c,
67 | &users.UserGetRequest{Email: "jane@doe.com"},
68 | )
69 | if err != nil {
70 | t.Fatal(err)
71 | }
72 |
73 | if result.User.FirstName != "jane" ||
74 | result.User.LastName != "doe" {
75 | t.Fatalf(
76 | "Expected: jane doe, Got: %s %s",
77 | result.User.FirstName,
78 | result.User.LastName,
79 | )
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/chap8/exercise2/client/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise2/client
2 |
3 | go 1.17
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require (
8 | github.com/golang/protobuf v1.4.2 // indirect
9 | golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect
11 | golang.org/x/text v0.3.0 // indirect
12 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
13 | google.golang.org/protobuf v1.25.0 // indirect
14 | github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service v0.0.0
15 |
16 | )
17 |
18 | replace github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service => ../service-v1
19 |
--------------------------------------------------------------------------------
/chap8/exercise2/client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | users "github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service"
10 | "google.golang.org/grpc"
11 | )
12 |
13 | func setupGrpcConn(addr string) (*grpc.ClientConn, error) {
14 | return grpc.DialContext(
15 | context.Background(),
16 | addr,
17 | grpc.WithInsecure(),
18 | grpc.WithBlock(),
19 | )
20 | }
21 |
22 | func getUserServiceClient(conn *grpc.ClientConn) users.UsersClient {
23 | return users.NewUsersClient(conn)
24 | }
25 |
26 | func getUser(
27 | client users.UsersClient,
28 | u *users.UserGetRequest,
29 | ) (*users.UserGetReply, error) {
30 | return client.GetUser(context.Background(), u)
31 | }
32 |
33 | func main() {
34 | if len(os.Args) != 2 {
35 | log.Fatal(
36 | "Must specify a gRPC server address",
37 | )
38 | }
39 | conn, err := setupGrpcConn(os.Args[1])
40 | if err != nil {
41 | log.Fatal(err)
42 | }
43 | defer conn.Close()
44 |
45 | c := getUserServiceClient(conn)
46 |
47 | result, err := getUser(
48 | c,
49 | &users.UserGetRequest{Email: "jane@doe.com"},
50 | )
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 | fmt.Fprintf(os.Stdout, "%#v\n", result.User)
55 | }
56 |
--------------------------------------------------------------------------------
/chap8/exercise2/client/user_client_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "net"
7 | "testing"
8 |
9 | users "github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/test/bufconn"
12 | )
13 |
14 | type dummyUserService struct {
15 | users.UnimplementedUsersServer
16 | }
17 |
18 | func (s *dummyUserService) GetUser(
19 | ctx context.Context,
20 | in *users.UserGetRequest,
21 | ) (*users.UserGetReply, error) {
22 | u := users.User{
23 | Id: "user-123-a",
24 | FirstName: "jane",
25 | LastName: "doe",
26 | Age: 36,
27 | }
28 | return &users.UserGetReply{User: &u}, nil
29 | }
30 |
31 | func startTestGrpcServer() (*grpc.Server, *bufconn.Listener) {
32 | l := bufconn.Listen(10)
33 | s := grpc.NewServer()
34 | users.RegisterUsersServer(s, &dummyUserService{})
35 | go func() {
36 | err := s.Serve(l)
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 | }()
41 | return s, l
42 | }
43 |
44 | func TestGetUser(t *testing.T) {
45 |
46 | s, l := startTestGrpcServer()
47 | defer s.GracefulStop()
48 |
49 | bufconnDialer := func(
50 | ctx context.Context, addr string,
51 | ) (net.Conn, error) {
52 | return l.Dial()
53 | }
54 |
55 | conn, err := grpc.DialContext(
56 | context.Background(),
57 | "", grpc.WithInsecure(),
58 | grpc.WithContextDialer(bufconnDialer),
59 | )
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 |
64 | c := getUserServiceClient(conn)
65 | result, err := getUser(
66 | c,
67 | &users.UserGetRequest{Email: "jane@doe.com"},
68 | )
69 | if err != nil {
70 | t.Fatal(err)
71 | }
72 |
73 | if result.User.FirstName != "jane" ||
74 | result.User.LastName != "doe" {
75 | t.Fatalf(
76 | "Expected: jane doe, Got: %s %s",
77 | result.User.FirstName,
78 | result.User.LastName,
79 | )
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/chap8/exercise2/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise2/server
2 |
3 | go 1.17
4 |
5 | require google.golang.org/grpc v1.37.1
6 |
7 | require github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service v0.0.0
8 |
9 | require (
10 | github.com/golang/protobuf v1.4.2 // indirect
11 | golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect
13 | golang.org/x/text v0.3.0 // indirect
14 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
15 | google.golang.org/protobuf v1.25.0 // indirect
16 | )
17 |
18 | replace github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service => ../service-v2
19 |
--------------------------------------------------------------------------------
/chap8/exercise2/server/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "log"
7 | "net"
8 | "os"
9 | "strings"
10 |
11 | users "github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service"
12 | "google.golang.org/grpc"
13 | )
14 |
15 | type userService struct {
16 | users.UnimplementedUsersServer
17 | }
18 |
19 | func (s *userService) GetUser(
20 | ctx context.Context,
21 | in *users.UserGetRequest,
22 | ) (*users.UserGetReply, error) {
23 | log.Printf(
24 | "Received request for user with Email: %s Id: %s\n",
25 | in.Email,
26 | in.Id,
27 | )
28 | components := strings.Split(in.Email, "@")
29 | if len(components) != 2 {
30 | return nil, errors.New("invalid email address")
31 | }
32 | u := users.User{
33 | Id: in.Id,
34 | FirstName: components[0],
35 | LastName: components[1],
36 | Age: 36,
37 | }
38 | return &users.UserGetReply{User: &u, Location: "Australia"}, nil
39 | }
40 |
41 | func registerServices(s *grpc.Server) {
42 | users.RegisterUsersServer(s, &userService{})
43 | }
44 |
45 | func startServer(s *grpc.Server, l net.Listener) error {
46 | return s.Serve(l)
47 | }
48 |
49 | func main() {
50 | listenAddr := os.Getenv("LISTEN_ADDR")
51 | if len(listenAddr) == 0 {
52 | listenAddr = ":50051"
53 | }
54 |
55 | lis, err := net.Listen("tcp", listenAddr)
56 | if err != nil {
57 | log.Fatal(err)
58 | }
59 | s := grpc.NewServer()
60 | registerServices(s)
61 | log.Fatal(startServer(s, lis))
62 | }
63 |
--------------------------------------------------------------------------------
/chap8/exercise2/server/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "net"
7 | "testing"
8 |
9 | users "github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/test/bufconn"
12 | )
13 |
14 | func startTestGrpcServer() (*grpc.Server, *bufconn.Listener) {
15 | l := bufconn.Listen(10)
16 | s := grpc.NewServer()
17 | registerServices(s)
18 | go func() {
19 | err := startServer(s, l)
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 | }()
24 | return s, l
25 | }
26 | func TestUserService(t *testing.T) {
27 |
28 | s, l := startTestGrpcServer()
29 | defer s.GracefulStop()
30 |
31 | bufconnDialer := func(
32 | ctx context.Context, addr string,
33 | ) (net.Conn, error) {
34 | return l.Dial()
35 | }
36 |
37 | client, err := grpc.DialContext(
38 | context.Background(),
39 | "", grpc.WithInsecure(),
40 | grpc.WithContextDialer(bufconnDialer),
41 | )
42 | if err != nil {
43 | t.Fatal(err)
44 | }
45 | usersClient := users.NewUsersClient(client)
46 | resp, err := usersClient.GetUser(
47 | context.Background(),
48 | &users.UserGetRequest{
49 | Email: "jane@doe.com",
50 | Id: "foo-bar",
51 | },
52 | )
53 |
54 | if err != nil {
55 | t.Fatal(err)
56 | }
57 | if resp.User.FirstName != "jane" {
58 | t.Errorf(
59 | "Expected FirstName to be: jane, Got: %s",
60 | resp.User.FirstName,
61 | )
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/chap8/exercise2/service-v1/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap8/exercise2/service-v1/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap8/user-service/service";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | }
8 |
9 | message UserGetRequest {
10 | string email = 1;
11 | string id = 2;
12 | }
13 |
14 | message User {
15 | string id = 1;
16 | string first_name = 2;
17 | string last_name = 3;
18 | int32 age = 4;
19 | }
20 |
21 | message UserGetReply {
22 | User user = 1;
23 | }
24 |
--------------------------------------------------------------------------------
/chap8/exercise2/service-v2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise2/service
2 | go 1.17
--------------------------------------------------------------------------------
/chap8/exercise2/service-v2/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap8/user-service/service";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | }
8 |
9 | message UserGetRequest {
10 | string email = 1;
11 | string id = 2;
12 | }
13 |
14 | message User {
15 | string id = 1;
16 | string first_name = 2;
17 | string last_name = 3;
18 | int32 age = 4;
19 | }
20 |
21 | message UserGetReply {
22 | User user = 1;
23 | string location = 2 ;
24 | }
25 |
--------------------------------------------------------------------------------
/chap8/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # Solution to Exercise 8.3
2 |
3 | This is my workflow in creating the solution:
4 |
5 | - Copy all the code from solution of chapter 8, exercise 1.
6 | - Rename go module to: `github.com/practicalgo/book-exercise-solutions/chap8/exercise3`
7 | by editing the `go.mod` file and update all package imports accordingly.
8 | - Copy `service` directory from the book's code repository `chap8/multiple-services`
9 | - Update `grpCmd.go` to make a call to the repository service
10 | - Updates tests in:
11 | - `cmd/grpc_flag_parsing_test.go` - this only tests the option parsing behavior
12 | - `cmd/handle_grpc_cmd_test.go` - this tests the behavior by creating a "real" gRPC server
13 | - Add new tests to `handle_grpc_repo_svc_test.go`
14 |
15 | Usage:
16 |
17 | Valid request:
18 |
19 | ```
20 | C:\> .\exercise3.exe grpc -service Users -method GetUser -request '{\"email\":\"john@doe1.com\",\"id\":\"user-123\"}' localhost:50051
21 | ...
22 | ```
23 |
24 | Invalid request:
25 |
26 | ```
27 | C:\> .\exercise3.exe grpc -service Users -method GetUser -request '{\"email\":\"john@doe.com\",\"id1\":\"user-123\"}' localhost:50051
28 | proto: (line 1:25): unknown field "id1"
29 | Usage: mync [http|grpc] -h
30 |
31 | .. Usage message ..
32 | ```
33 |
34 | # JSON Pretty printing
35 |
36 | ```
37 | C:\> .\exercise3.exe grpc -service Users -method GetUser -request '{\"email\":\"john@doe.com\",\"id\":\"user-123\"}' -pretty-print localhost:50051
38 | {
39 | "user": {
40 | "id": "user-123",
41 | "firstName": "john",
42 | "lastName": "doe.com",
43 | "age": 36
44 | }
45 | }
46 | ```
--------------------------------------------------------------------------------
/chap8/exercise3/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
8 | var ErrInvalidHTTPMethod = errors.New("Invalid HTTP method")
9 | var ErrInvalidGrpcMethod = errors.New("Invalid gRPC method")
10 |
11 | var ErrInvalidHTTPCommand = errors.New("Invalid HTTP command")
12 | var ErrInvalidHTTPPostCommand = errors.New("Cannot specify both body and body-file")
13 | var ErrInvalidHTTPPostRequest = errors.New("HTTP POST request must specify a non-empty JSON body")
14 |
15 | type FlagParsingError struct {
16 | err error
17 | }
18 |
19 | func (e FlagParsingError) Error() string {
20 | return e.err.Error()
21 | }
22 |
23 | type InvalidInputError struct {
24 | Err error
25 | }
26 |
27 | func (e InvalidInputError) Error() string {
28 | return e.Err.Error()
29 | }
30 |
--------------------------------------------------------------------------------
/chap8/exercise3/cmd/grpc_flag_parsing_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestGrpcCmdFlagParsing(t *testing.T) {
10 | usageMessage := `
11 | grpc: A gRPC client.
12 |
13 | grpc: server
14 |
15 | Options:
16 | -method string
17 | Method to call
18 | -pretty-print
19 | Pretty print the JSON output
20 | -request string
21 | Request to send
22 | -service string
23 | gRPC service to send the request to
24 | `
25 |
26 | testConfigs := []struct {
27 | args []string
28 | output string
29 | err error
30 | }{
31 | {
32 | args: []string{},
33 | err: InvalidInputError{ErrNoServerSpecified},
34 | },
35 | {
36 | args: []string{"-h"},
37 | err: errors.New("flag: help requested"),
38 | output: usageMessage,
39 | },
40 | {
41 | args: []string{"-service", "Users", "localhost:50051"},
42 | err: errors.New("Invalid gRPC method"),
43 | },
44 | }
45 | byteBuf := new(bytes.Buffer)
46 | for i, tc := range testConfigs {
47 | t.Log(i)
48 | err := HandleGrpc(byteBuf, tc.args)
49 | if tc.err == nil && err != nil {
50 | t.Fatalf("Expected nil error, got %v", err)
51 | }
52 |
53 | if tc.err != nil && err == nil {
54 | t.Fatal("Expected non-nil error, got nil")
55 | }
56 |
57 | if tc.err != nil && err.Error() != tc.err.Error() {
58 | t.Fatalf("Expected error %v, got %v", tc.err, err)
59 | }
60 |
61 | if len(tc.output) != 0 {
62 | gotOutput := byteBuf.String()
63 | if tc.output != gotOutput {
64 | t.Fatalf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput)
65 | }
66 | }
67 | byteBuf.Reset()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/chap8/exercise3/cmd/test_utils.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "log"
6 | "strings"
7 |
8 | svc "github.com/practicalgo/book-exercise-solutions/chap8/exercise3/service"
9 | "google.golang.org/grpc"
10 | "google.golang.org/grpc/test/bufconn"
11 | )
12 |
13 | type dummyUserService struct {
14 | svc.UnimplementedUsersServer
15 | }
16 |
17 | func (s *dummyUserService) GetUser(
18 | ctx context.Context,
19 | in *svc.UserGetRequest,
20 | ) (*svc.UserGetReply, error) {
21 | components := strings.Split(in.Email, "@")
22 | u := svc.User{
23 | Id: in.Id,
24 | FirstName: components[0],
25 | LastName: components[1],
26 | Age: 36,
27 | }
28 | return &svc.UserGetReply{User: &u}, nil
29 | }
30 |
31 | type dummyReposService struct {
32 | svc.UnimplementedRepoServer
33 | }
34 |
35 | func (s *dummyReposService) GetRepos(
36 | ctx context.Context,
37 | in *svc.RepoGetRequest,
38 | ) (*svc.RepoGetReply, error) {
39 |
40 | repos := []*svc.Repository{
41 | {
42 | Id: "repo-123",
43 | Name: "practicalgo/book-exercise-solutions",
44 | Url: "git.example.com/practicalgo/book-exercise-solutions",
45 | Owner: &svc.User{Id: "user-123"},
46 | },
47 | }
48 | return &svc.RepoGetReply{Repo: repos}, nil
49 | }
50 |
51 | func startTestGrpcServer() (*grpc.Server, *bufconn.Listener) {
52 | l := bufconn.Listen(10)
53 | s := grpc.NewServer()
54 | svc.RegisterUsersServer(s, &dummyUserService{})
55 | svc.RegisterRepoServer(s, &dummyReposService{})
56 | go func() {
57 | err := s.Serve(l)
58 | if err != nil {
59 | log.Fatal(err)
60 | }
61 | }()
62 | return s, l
63 | }
64 |
--------------------------------------------------------------------------------
/chap8/exercise3/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap8/exercise3
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/practicalgo/book-exercise-solutions/chap8/exercise3/service v0.0.0
7 | google.golang.org/grpc v1.37.0
8 |
9 | )
10 |
11 | require (
12 | github.com/golang/protobuf v1.5.0 // indirect
13 | golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect
15 | golang.org/x/text v0.3.0 // indirect
16 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
17 | google.golang.org/protobuf v1.26.0 // indirect
18 | )
19 |
20 | replace github.com/practicalgo/book-exercise-solutions/chap8/exercise3/service => ./service
21 |
--------------------------------------------------------------------------------
/chap8/exercise3/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/practicalgo/book-exercise-solutions/chap8/exercise3/cmd"
10 | )
11 |
12 | var errInvalidSubCommand = errors.New("Invalid sub-command specified")
13 |
14 | func printUsage(w io.Writer) {
15 | fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
16 | cmd.HandleHttp(w, []string{"-h"})
17 | cmd.HandleGrpc(w, []string{"-h"})
18 | }
19 |
20 | func handleCommand(w io.Writer, args []string) error {
21 | var err error
22 |
23 | if len(args) < 1 {
24 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
25 | } else {
26 | switch args[0] {
27 | case "http":
28 | err = cmd.HandleHttp(w, args[1:])
29 | case "grpc":
30 | err = cmd.HandleGrpc(w, args[1:])
31 | case "-h":
32 | printUsage(w)
33 | case "-help":
34 | printUsage(w)
35 | default:
36 | err = cmd.InvalidInputError{Err: errInvalidSubCommand}
37 | }
38 | }
39 |
40 | // for non-nil errors, we can have three kinds of errors
41 | // 1. Flag parsing error (E.g. use of invalid option)
42 | // 2. Invalid input error (E.g. JSON body specified for a GET request)
43 | // 3. Application specific error (E.g. remote server returned an error for example)
44 | // For (1), the flag package will show the error and also print the usage, so we don't do anything here
45 | // For (2), the we want to show the error and print the usage of the program
46 | // For (3), we only want to show the error
47 | if err != nil {
48 | if !errors.As(err, &cmd.FlagParsingError{}) {
49 | fmt.Fprintln(w, err.Error())
50 | }
51 | if errors.As(err, &cmd.InvalidInputError{}) {
52 | printUsage(w)
53 | }
54 | }
55 | return err
56 | }
57 |
58 | func main() {
59 | err := handleCommand(os.Stdout, os.Args[1:])
60 | if err != nil {
61 | os.Exit(1)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chap8/exercise3/middleware/httpLatency.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | type HttpLatencyClient struct {
10 | Logger *log.Logger
11 | Transport http.RoundTripper
12 | }
13 |
14 | func (c HttpLatencyClient) RoundTrip(
15 | r *http.Request,
16 | ) (*http.Response, error) {
17 | startTime := time.Now()
18 | resp, err := c.Transport.RoundTrip(r)
19 | c.Logger.Printf(
20 | "url=%s method=%s protocol=%s latency=%f\n",
21 | r.URL, r.Method, r.Proto, time.Since(startTime).Seconds(),
22 | )
23 | return resp, err
24 | }
25 |
--------------------------------------------------------------------------------
/chap8/exercise3/service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap8/multiple-sevices/service
2 |
3 | go 1.16
4 |
--------------------------------------------------------------------------------
/chap8/exercise3/service/repositories.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "users.proto";
4 |
5 | option go_package = "github.com/practicalgo/code/chap8/multiple-services/service";
6 |
7 | service Repo {
8 | rpc GetRepos (RepoGetRequest) returns (RepoGetReply) {}
9 | }
10 |
11 | message RepoGetRequest {
12 | string id = 2;
13 | string creator_id = 1;
14 | }
15 |
16 | message Repository {
17 | string id = 1;
18 | string name = 2;
19 | string url = 3;
20 | User owner = 4;
21 | }
22 |
23 | message RepoGetReply {
24 | repeated Repository repo = 1;
25 | }
26 |
--------------------------------------------------------------------------------
/chap8/exercise3/service/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap8/multiple-services/service";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | }
8 |
9 | message UserGetRequest {
10 | string email = 1;
11 | string id = 2;
12 | }
13 |
14 | message User {
15 | string id = 1;
16 | string first_name = 2;
17 | string last_name = 3;
18 | int32 age = 4;
19 | }
20 |
21 | message UserGetReply {
22 | User user = 1;
23 | }
24 |
--------------------------------------------------------------------------------
/chap9/.emptyfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practicalgo/book-exercise-solutions/fec2ab4bd8ae51af162c4be7530a161646d2fc54/chap9/.emptyfile
--------------------------------------------------------------------------------
/chap9/exercise1/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap9/exercise1/server
2 |
3 | go 1.17
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require github.com/practicalgo/book-exercise-solutions/chap9/exercise1/service v0.0.0
8 |
9 | require (
10 | github.com/golang/protobuf v1.4.2 // indirect
11 | golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect
13 | golang.org/x/text v0.3.0 // indirect
14 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
15 | google.golang.org/protobuf v1.25.0 // indirect
16 | )
17 |
18 | replace github.com/practicalgo/book-exercise-solutions/chap9/exercise1/service => ../service
19 |
--------------------------------------------------------------------------------
/chap9/exercise1/service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap9/exercise1/service
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/chap9/exercise1/service/repositories.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/timestamp.proto";
4 | import "users.proto";
5 |
6 | option go_package = "github.com/practicalgo/code/chap9/server-streaming/service";
7 |
8 | service Repo {
9 | rpc GetRepos (RepoGetRequest) returns (stream RepoGetReply) {}
10 | rpc CreateBuild (Repository) returns (stream RepoBuildLog) {}
11 | }
12 |
13 | message RepoGetRequest {
14 | string id = 2;
15 | string creator_id = 1;
16 | }
17 |
18 | message Repository {
19 | string id = 1;
20 | string name = 2;
21 | string url = 3;
22 | User owner = 4;
23 | }
24 |
25 | message RepoGetReply {
26 | Repository repo = 1;
27 | }
28 |
29 | message RepoBuildLog {
30 | string log_line = 1;
31 | google.protobuf.Timestamp timestamp = 2;
32 | }
33 |
--------------------------------------------------------------------------------
/chap9/exercise1/service/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap9/server-streaming/service";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | }
8 |
9 | message UserGetRequest {
10 | string email = 1;
11 | string id = 2;
12 | }
13 |
14 | message User {
15 | string id = 1;
16 | string first_name = 2;
17 | string last_name = 3;
18 | int32 age = 4;
19 | }
20 |
21 | message UserGetReply {
22 | User user = 1;
23 | }
24 |
--------------------------------------------------------------------------------
/chap9/exercise2/README.md:
--------------------------------------------------------------------------------
1 | # Worklow for Solution 9.2
2 |
3 | This is my workflow for creating the solution.
4 |
5 | - Copy `server` and `service` directories from the book's code repository, `chap9/bindata-client-streaming`
6 |
7 | - Create a new `client` directory where we will create our client
8 | - Initialize a new module, `go mod init github.com/practicalgo/book-exercise-solutions/chap9/exercise1/client`
9 | - Use the import path for the service definitions as we did in chap9/ code:
10 |
11 | ```
12 | svc "github.com/practicalgo/code/chap9/bindata-client-streaming/service"
13 | ```
14 |
15 | The `go.mod` for the client will be as follows:
16 |
17 | ```
18 | module github.com/practicalgo/book-exercise-solutions/chap9/exercise1/client
19 |
20 | require (
21 | github.com/practicalgo/code/chap9/bindata-client-streaming/service v0.0.0
22 | google.golang.org/grpc v1.42.0
23 | )
24 |
25 |
26 | replace github.com/practicalgo/code/chap9/bindata-client-streaming/service => ../service
27 |
28 | go 1.17
29 |
30 | ```
31 |
32 |
33 | ## Demonstration
34 |
35 | Run the server and then run the client.
36 |
37 | ```
38 | .\client.exe -file-path .\client.exe localhost:50051
39 | Uploaded 11435520 bytes
40 | ```
41 |
42 | (The exercise mentions a tar.gz file, but of course we can use any file for testing the upload behavior)
--------------------------------------------------------------------------------
/chap9/exercise2/client/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/book-exercise-solutions/chap9/exercise1/client
2 |
3 | require (
4 | github.com/practicalgo/code/chap9/bindata-client-streaming/service v0.0.0
5 | google.golang.org/grpc v1.42.0
6 | )
7 |
8 | require (
9 | github.com/golang/protobuf v1.5.0 // indirect
10 | golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
11 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
12 | golang.org/x/text v0.3.0 // indirect
13 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
14 | google.golang.org/protobuf v1.27.1 // indirect
15 | )
16 |
17 | replace github.com/practicalgo/code/chap9/bindata-client-streaming/service => ../service
18 |
19 | go 1.17
20 |
--------------------------------------------------------------------------------
/chap9/exercise2/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap9/bindata-client-streaming/server
2 |
3 | go 1.16
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require github.com/practicalgo/code/chap9/bindata-client-streaming/service v0.0.0
8 |
9 | replace github.com/practicalgo/code/chap9/bindata-client-streaming/service v0.0.0 => ../service
10 |
--------------------------------------------------------------------------------
/chap9/exercise2/server/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "log"
7 | "net"
8 | "os"
9 |
10 | svc "github.com/practicalgo/code/chap9/bindata-client-streaming/service"
11 | "google.golang.org/grpc"
12 | "google.golang.org/grpc/codes"
13 | "google.golang.org/grpc/status"
14 | )
15 |
16 | type repoService struct {
17 | svc.UnimplementedRepoServer
18 | }
19 |
20 | func (s *repoService) CreateRepo(
21 | stream svc.Repo_CreateRepoServer,
22 | ) error {
23 | var repoContext *svc.RepoContext
24 | var data []byte
25 | for {
26 | r, err := stream.Recv()
27 | if err == io.EOF {
28 | break
29 | }
30 | if err != nil {
31 | return status.Error(
32 | codes.Unknown,
33 | err.Error(),
34 | )
35 |
36 | }
37 | switch t := r.Body.(type) {
38 | case *svc.RepoCreateRequest_Context:
39 | repoContext = r.GetContext()
40 | case *svc.RepoCreateRequest_Data:
41 | b := r.GetData()
42 | data = append(data, b...)
43 | case nil:
44 | return status.Error(
45 | codes.InvalidArgument,
46 | "Message doesn't contain context or data",
47 | )
48 | default:
49 | return status.Errorf(
50 | codes.FailedPrecondition,
51 | "Unexpected message type: %s",
52 | t,
53 | )
54 | }
55 | }
56 | repo := svc.Repository{
57 | Name: repoContext.Name,
58 | Url: fmt.Sprintf(
59 | "https://git.example.com/%s/%s",
60 | repoContext.CreatorId,
61 | repoContext.Name,
62 | ),
63 | }
64 | r := svc.RepoCreateReply{
65 | Repo: &repo,
66 | Size: int32(len(data)),
67 | }
68 | log.Printf("%#v\n", string(data))
69 | return stream.SendAndClose(&r)
70 | }
71 |
72 | func registerServices(s *grpc.Server) {
73 | svc.RegisterRepoServer(s, &repoService{})
74 | }
75 |
76 | func startServer(s *grpc.Server, l net.Listener) error {
77 | return s.Serve(l)
78 | }
79 |
80 | func main() {
81 | listenAddr := os.Getenv("LISTEN_ADDR")
82 | if len(listenAddr) == 0 {
83 | listenAddr = ":50051"
84 | }
85 |
86 | lis, err := net.Listen("tcp", listenAddr)
87 | if err != nil {
88 | log.Fatal(err)
89 | }
90 | s := grpc.NewServer()
91 | registerServices(s)
92 | log.Fatal(startServer(s, lis))
93 | }
94 |
--------------------------------------------------------------------------------
/chap9/exercise2/service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap9/bindata-client-streaming/service
2 |
3 | go 1.16
4 |
--------------------------------------------------------------------------------
/chap9/exercise2/service/repositories.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap9/bindata-client-streaming/service";
4 |
5 | service Repo {
6 | rpc CreateRepo (stream RepoCreateRequest) returns (RepoCreateReply) {}
7 | }
8 |
9 | message RepoCreateRequest {
10 | oneof body {
11 | RepoContext context = 1;
12 | bytes data = 2;
13 | }
14 | }
15 |
16 | message RepoContext {
17 | string creator_id = 1;
18 | string name = 2;
19 | }
20 |
21 | message Repository {
22 | string id = 1;
23 | string name = 2;
24 | string url = 3;
25 | }
26 |
27 | message RepoCreateReply {
28 | Repository repo = 1;
29 | int32 size = 2;
30 | }
31 |
--------------------------------------------------------------------------------
/chap9/exercise3/README.md:
--------------------------------------------------------------------------------
1 | # Workflow for solution to Exercise 9.3
2 |
3 | - Copy all of the contents of the directory from the book's source repository, `chap9/interceptor-chain`
4 |
5 | ## Updating the server interceptor
6 |
7 | - update `wrappedServerStream` to add two new fields, `messageSent` and `messageRecvd`:
8 |
9 | ```
10 | type wrappedServerStream struct {
11 | grpc.ServerStream
12 | messageSent int
13 | messageRcvd int
14 | }
15 | ```
16 |
17 | - Update the SendMsg() and RecvMsg() methods to now have pointer receivers and then increment the counters
18 | - Update the logging interceptor to not use the wrapped stream
19 | - Update the metric interceptor to wrap the incoming stream inside a `wrappedStream` value:
20 |
21 | ```
22 | serverStream := &wrappedServerStream{
23 | ServerStream: stream,
24 | messageSent: 0,
25 | messageRcvd: 0,
26 | }
27 | ```
28 |
29 | ## Updating the client interceptor
30 |
31 | - update `wrappedClientStream` to add two new fields, `messageSent` and `messageRecvd`:
32 |
33 | ```
34 | type wrappedClientStream struct {
35 | grpc.ClientStream
36 | messageSent int
37 | messageRcvd int
38 | }
39 | ```
40 |
41 | - Update the SendMsg() and RecvMsg() methods to now have pointer receivers and then increment the counters
42 | - Update the `loggingStreamingInterceptor` to initialize the `wrappedClientStream` as follows:
43 |
44 | ```
45 | clientStream := &wrappedClientStream{
46 | ClientStream: stream,
47 | messageRcvd: 0,
48 | messageSent: 0,
49 | }
50 | ```
51 | - Finally update the CloseSend() method of the interceptor to log the messages received and sent count:
52 |
53 | ```
54 | func (s *wrappedClientStream) CloseSend() error {
55 | log.Println("CloseSend() called")
56 | v := s.Context().Value(streamDurationContextKey{})
57 |
58 | if m, ok := v.(streamDurationContextValue); ok {
59 | log.Printf("Duration:%v", time.Since(m.startTime))
60 | }
61 | err := s.ClientStream.CloseSend()
62 | log.Printf("Messages Sent: %d, Messages Received:%d\n",
63 | s.messageSent,
64 | s.messageRcvd,
65 | )
66 | return err
67 | }
68 | ```
--------------------------------------------------------------------------------
/chap9/exercise3/client/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap9/interceptor-chain/client
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/practicalgo/code/chap9/interceptor-chain/service v0.0.0
7 | google.golang.org/grpc v1.37.0
8 | )
9 |
10 | replace github.com/practicalgo/code/chap9/interceptor-chain/service => ../service
11 |
--------------------------------------------------------------------------------
/chap9/exercise3/client/user_client_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "net"
7 | "testing"
8 |
9 | users "github.com/practicalgo/code/chap9/interceptor-chain/service"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/test/bufconn"
12 | )
13 |
14 | type dummyUserService struct {
15 | users.UnimplementedUsersServer
16 | }
17 |
18 | func (s *dummyUserService) GetUser(
19 | ctx context.Context,
20 | in *users.UserGetRequest,
21 | ) (*users.UserGetReply, error) {
22 | u := users.User{
23 | Id: "user-123-a",
24 | FirstName: "jane",
25 | LastName: "doe",
26 | Age: 36,
27 | }
28 | return &users.UserGetReply{User: &u}, nil
29 | }
30 |
31 | func startTestGrpcServer() *bufconn.Listener {
32 | l := bufconn.Listen(10)
33 | s := grpc.NewServer()
34 | users.RegisterUsersServer(s, &dummyUserService{})
35 | go func() {
36 | log.Fatal(s.Serve(l))
37 | }()
38 | return l
39 | }
40 |
41 | func TestGetUser(t *testing.T) {
42 |
43 | l := startTestGrpcServer()
44 |
45 | bufconnDialer := func(
46 | ctx context.Context, addr string,
47 | ) (net.Conn, error) {
48 | return l.Dial()
49 | }
50 |
51 | conn, err := grpc.DialContext(
52 | context.Background(),
53 | "", grpc.WithInsecure(),
54 | grpc.WithContextDialer(bufconnDialer),
55 | )
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 |
60 | c := getUserServiceClient(conn)
61 | result, err := getUser(
62 | c,
63 | &users.UserGetRequest{Email: "jane@doe.com"},
64 | )
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 |
69 | if result.User.FirstName != "jane" ||
70 | result.User.LastName != "doe" {
71 | t.Fatalf(
72 | "Expected: jane doe, Got: %s %s",
73 | result.User.FirstName,
74 | result.User.LastName,
75 | )
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/chap9/exercise3/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap9/interceptor-chain/server
2 |
3 | go 1.16
4 |
5 | require google.golang.org/grpc v1.37.0
6 |
7 | require github.com/practicalgo/code/chap9/interceptor-chain/service v0.0.0
8 |
9 | replace github.com/practicalgo/code/chap9/interceptor-chain/service => ../service
10 |
--------------------------------------------------------------------------------
/chap9/exercise3/server/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net"
10 | "os"
11 | "strings"
12 |
13 | svc "github.com/practicalgo/code/chap9/interceptor-chain/service"
14 | "google.golang.org/grpc"
15 | )
16 |
17 | type userService struct {
18 | svc.UnimplementedUsersServer
19 | }
20 |
21 | func (s *userService) GetUser(
22 | ctx context.Context,
23 | in *svc.UserGetRequest,
24 | ) (*svc.UserGetReply, error) {
25 |
26 | log.Printf(
27 | "Received request for user with Email: %s Id: %s\n",
28 | in.Email,
29 | in.Id,
30 | )
31 | components := strings.Split(in.Email, "@")
32 | if len(components) != 2 {
33 | return nil, errors.New("invalid email address")
34 | }
35 | u := svc.User{
36 | Id: in.Id,
37 | FirstName: components[0],
38 | LastName: components[1],
39 | Age: 36,
40 | }
41 | return &svc.UserGetReply{User: &u}, nil
42 | }
43 |
44 | func (s *userService) GetHelp(
45 | stream svc.Users_GetHelpServer,
46 | ) error {
47 | for {
48 |
49 | request, err := stream.Recv()
50 | if err == io.EOF {
51 | break
52 | }
53 | if err != nil {
54 | return err
55 | }
56 | fmt.Printf("Request receieved: %s\n", request.Request)
57 | response := svc.UserHelpReply{
58 | Response: request.Request,
59 | }
60 | err = stream.Send(&response)
61 | if err != nil {
62 | return err
63 | }
64 | }
65 | return nil
66 | }
67 |
68 | func registerServices(s *grpc.Server) {
69 | svc.RegisterUsersServer(s, &userService{})
70 | }
71 |
72 | func startServer(s *grpc.Server, l net.Listener) error {
73 | return s.Serve(l)
74 | }
75 |
76 | func main() {
77 | listenAddr := os.Getenv("LISTEN_ADDR")
78 | if len(listenAddr) == 0 {
79 | listenAddr = ":50051"
80 | }
81 |
82 | lis, err := net.Listen("tcp", listenAddr)
83 | if err != nil {
84 | log.Fatal(err)
85 | }
86 | s := grpc.NewServer(
87 | grpc.ChainUnaryInterceptor(
88 | metricUnaryInterceptor,
89 | loggingUnaryInterceptor,
90 | ),
91 | grpc.ChainStreamInterceptor(
92 | metricStreamInterceptor,
93 | loggingStreamInterceptor,
94 | ),
95 | )
96 | registerServices(s)
97 | log.Fatal(startServer(s, lis))
98 | }
99 |
--------------------------------------------------------------------------------
/chap9/exercise3/server/server_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "net"
7 | "testing"
8 |
9 | users "github.com/practicalgo/code/chap9/interceptor-chain/service"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/test/bufconn"
12 | )
13 |
14 | func startTestGrpcServer() *bufconn.Listener {
15 | l := bufconn.Listen(10)
16 | s := grpc.NewServer()
17 | registerServices(s)
18 | go func() {
19 | log.Fatal(startServer(s, l))
20 | }()
21 | return l
22 | }
23 | func TestUserService(t *testing.T) {
24 |
25 | l := startTestGrpcServer()
26 |
27 | bufconnDialer := func(
28 | ctx context.Context, addr string,
29 | ) (net.Conn, error) {
30 | return l.Dial()
31 | }
32 |
33 | client, err := grpc.DialContext(
34 | context.Background(),
35 | "", grpc.WithInsecure(),
36 | grpc.WithContextDialer(bufconnDialer),
37 | )
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 | usersClient := users.NewUsersClient(client)
42 | resp, err := usersClient.GetUser(
43 | context.Background(),
44 | &users.UserGetRequest{
45 | Email: "jane@doe.com",
46 | Id: "foo-bar",
47 | },
48 | )
49 |
50 | if err != nil {
51 | t.Fatal(err)
52 | }
53 | if resp.User.FirstName != "jane" {
54 | t.Errorf(
55 | "Expected FirstName to be: jane, Got: %s",
56 | resp.User.FirstName,
57 | )
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/chap9/exercise3/service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/practicalgo/code/chap9/interceptor-chain/service
2 |
3 | go 1.16
4 |
--------------------------------------------------------------------------------
/chap9/exercise3/service/users.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/practicalgo/code/chap9/interceptors/service/users";
4 |
5 | service Users {
6 | rpc GetUser (UserGetRequest) returns (UserGetReply) {}
7 | rpc GetHelp (stream UserHelpRequest) returns (stream UserHelpReply) {}
8 | }
9 |
10 | message UserGetRequest {
11 | string email = 1;
12 | string id = 2;
13 | }
14 |
15 | message User {
16 | string id = 1;
17 | string first_name = 2;
18 | string last_name = 3;
19 | int32 age = 4;
20 | }
21 |
22 | message UserGetReply {
23 | User user = 1;
24 | }
25 |
26 | message UserHelpRequest {
27 | User user = 1;
28 | string request = 2;
29 | }
30 |
31 | message UserHelpReply {
32 | string response = 1;
33 | }
34 |
--------------------------------------------------------------------------------
/errors.md:
--------------------------------------------------------------------------------
1 | Exercise 1.1 - functions in the `os/exec` package.
2 | Exercise 2.2 - the `-verb` option. If the verb specifies is anything other than these values, the program should exit with a non-zero exit code and ..
3 |
4 | Chapter 6, Page: 11:
5 |
6 | "
7 | In the book I write that the innermost middleware is executed first when processing
8 | a request. Unfortunately, it isn't accurate. The request flows from the outermost
9 | middleware to the innermost middleware in it's journey from the user to the application.
10 | On it's way back, it's the reverse journey - which is what I am describing in the book.
11 | Since we want to add our request ID before we log or handle panic, we add the new middleware
12 | as the first middleware."
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/housekeeping/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | go build -o application
3 |
--------------------------------------------------------------------------------
/housekeeping/check_binaries.sh:
--------------------------------------------------------------------------------
1 | find chap3 -type f -perm +0111 -print
2 |
--------------------------------------------------------------------------------
/housekeeping/check_consistency.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | grep -r fmt.Printf "$1"
4 | grep -r fmt.Println "$1"
5 | grep -r writeString "$1"
6 | grep -r bufio.NewWriter "$1"
7 | grep -r bufio.NewReader "$1"
8 |
--------------------------------------------------------------------------------
/housekeeping/copy_code.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cat $1 | expand -t 8 | pbcopy
4 |
--------------------------------------------------------------------------------
/housekeeping/git_commit_push.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ex
3 |
4 | detect-os-executables -c "$1"
5 | git add -A
6 | git commit -m "Update"
7 | git push
8 |
--------------------------------------------------------------------------------
/housekeeping/golint.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import os
4 | import subprocess
5 | import sys
6 |
7 | failed = 0
8 |
9 | if len(sys.argv) != 2:
10 | sys.exit('Must specify a directory path to lint')
11 |
12 | for root, dirs, files in os.walk(sys.argv[1]):
13 | src_dir = None
14 | if root.startswith('./.git'):
15 | continue
16 | for f in files:
17 | if f.endswith('.go'):
18 | src_dir = root
19 | break
20 | if not src_dir or "parked" in src_dir or "solutions" in src_dir or "service" in src_dir:
21 | print("Ignoring: {0}".format(src_dir))
22 | continue
23 | print('Linting: {0}\n-------'.format(src_dir))
24 | try:
25 | print(subprocess.check_output(["golint"], cwd=src_dir,
26 | stderr=subprocess.PIPE).decode("utf-8"))
27 | except subprocess.CalledProcessError as e:
28 | print('Lint failure: {0}'.format(src_dir))
29 | failed = 1
30 |
31 | sys.exit(failed)
32 |
--------------------------------------------------------------------------------
/housekeeping/gotest.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import os
4 | import subprocess
5 | import sys
6 |
7 | failed = 0
8 | if len(sys.argv) != 2:
9 | sys.exit('Must specify a directory path to test')
10 |
11 | for root, dirs, files in os.walk(sys.argv[1]):
12 | src_dir = None
13 | if root.startswith('./.git'):
14 | continue
15 | for f in files:
16 | if '.go' in f:
17 | src_dir = root
18 | break
19 | if not src_dir or "parked" in src_dir or "solutions" in src_dir or "service" in src_dir:
20 | print("Ignoring: {0}".format(src_dir))
21 | continue
22 | try:
23 | subprocess.check_output(["go", "test", "-v"], cwd=src_dir,
24 | stderr=subprocess.PIPE)
25 | except subprocess.CalledProcessError as e:
26 | print('Test failure: {0}'.format(src_dir))
27 | failed = 1
28 |
29 | sys.exit(failed)
30 |
--------------------------------------------------------------------------------
/housekeeping/govet.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import os
4 | import subprocess
5 | import sys
6 |
7 | failed = 0
8 |
9 | if len(sys.argv) != 2:
10 | sys.exit('Must specify a directory path to vet')
11 |
12 | for root, dirs, files in os.walk(sys.argv[1]):
13 | src_dir = None
14 | if root.startswith('./.git'):
15 | continue
16 | for f in files:
17 | if '.go' in f:
18 | src_dir = root
19 | break
20 | if not src_dir or "parked" in src_dir or "solutions" in src_dir or "service" in src_dir:
21 | print("Ignoring: {0}".format(src_dir))
22 | continue
23 | try:
24 | subprocess.check_output(["go", "vet"], cwd=src_dir,
25 | stderr=subprocess.PIPE)
26 | except subprocess.CalledProcessError as e:
27 | print('Vet failure: {0}'.format(src_dir))
28 | failed = 1
29 | sys.exit(failed)
--------------------------------------------------------------------------------
/housekeeping/show_coverage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | go test -coverprofile cover.out
4 | go tool cover -html=cover.out
5 |
--------------------------------------------------------------------------------