├── .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 | --------------------------------------------------------------------------------