Skip to content

Commit

Permalink
Merge pull request #9 from hs3city/golangci
Browse files Browse the repository at this point in the history
Linter, workspace & docs
  • Loading branch information
szmktk authored Jan 27, 2024
2 parents 9b88bf4 + 003b1cb commit 5fd97ac
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 36 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: golangci-lint
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
# pull-requests: read

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
# Require: The version of golangci-lint to use.
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
version: v1.54

# Optional: working directory, useful for monorepos
# working-directory: somedir

# Optional: golangci-lint command line arguments.
#
# Note: By default, the `.golangci.yml` file should be at the root of the repository.
# The location of the configuration file can be changed by using `--config=`
# args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0
args: ./cyoa/main ./quiz ./urlshort ./urlshort/main

# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

# Optional: if set to true, then all caching functionality will be completely disabled,
# takes precedence over all other caching options.
# skip-cache: true

# Optional: if set to true, then the action won't cache or restore ~/go/pkg.
# skip-pkg-cache: true

# Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
# skip-build-cache: true

# Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
# install-mode: "goinstall"
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# vendor/

# Go workspace file
go.work
# go.work # explicitly un-ignored, we want to have the same workspace experience

### GoLand ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,72 @@
# goexplorers

This repository contains various separate modules for [Go](https://go.dev/) self-study coding sessions held at [Hackerspace Tricity](https://github.com/hs3city/).

In order to have multiple modules in a single repo we use [Go workspaces](https://go.dev/doc/tutorial/workspaces).

`go.work` file is already created and checked in to the repository, allowing all session participants to have the same developer experience.

In order to initialize a new Go module in a workspace use the following commands:

```
export NEW_MODULE_DIRECTORY=mymodule
mkdir $NEW_MODULE_DIRECTORY
go work use $NEW_MODULE_DIRECTORY
```

The last command will modify [go.work](./go.work) file. When adding a new module please also remember to add the directory to [linter configuration](./.github/workflows/golangci-lint.yml#39)<br>
The `args` (`jobs.golangci.steps[golangci-lint].args`) key in the linter config defines a set of module directories to be checked by a suite of linters.

Having a linter in place allows us to learn best practices and Go idioms while we're learning to walk 🙂


## How to run modules

### [Quiz](./quiz/)

```
go run ./quiz
```


### [Url Shortener](./urlshort/)

Start the server
```
go run ./urlshort/...
```

Then send a request for a shortened URL to get redirected to it
```
curl -v localhost:8080/hs3
```


### [Choose Your Own Adventure](./cyoa/)

Start the server
```
cd cyoa/main
go run ./...
```

Then point your browser to [localhost:8080/intro](http://localhost:8080/intro) to start the game.


## How to run tests for all modules

```
go test ./cyoa/... ./quiz/... ./urlshort/...
```

From the structure of the command above one can easily guess how to run tests for a single module or how to add more modules.<br>
What if the number of modules is so high that it does not make sense to use that command anymore?<br>
We can use `find` with `maxdepth` flag to select any non-hidden top-level directory, pass it to `sed` to append the dots and finally use `xargs` to invoke the sacred `go` command 💪💪💪

```
find . -maxdepth 1 -type d -not -path '.' -not -path '*/.*' | sed 's|$|/...|' | xargs go test
```

Note that the command above depends on the existence of `go.work` file.

![](https://traust.duckdns.org/api/public/dl/Q9Haw2rT?inline=true)
30 changes: 19 additions & 11 deletions cyoa/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,62 @@ package main

import (
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"os"
"strings"
)

func main() {
fmt.Println("Server starting...")
log.Println("Server starting...")
http.HandleFunc("/", handlerHttp)
http.ListenAndServe(":8080", nil)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}

func handlerHttp(w http.ResponseWriter, r *http.Request) {

pathSegments := strings.Split(r.URL.Path, "/")
if r.Method != "GET" {
fmt.Println("Method not allowed")
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
expectedArc := pathSegments[1]

jsonInput, err := os.ReadFile("data.json")
if err != nil {
panic(err)
logWithServerError(w, "Error reading JSON data file", err)
return
}

var story StoryLine

err = json.Unmarshal(jsonInput, &story)
if err != nil {
panic(err)
logWithServerError(w, "Error parsing JSON data", err)
return
}

t, _ := template.ParseFiles("template.html")

retVal, ok := story[expectedArc]

if !ok {
fmt.Printf("path %s not found in the data source\n", expectedArc)
log.Printf("Path '%s' not found in the data source", expectedArc)
http.Error(w, "Path not found", http.StatusNotFound)
return
}

err = t.Execute(w, retVal)

if err != nil {
fmt.Println("whaa")
fmt.Println(err)
panic(err)
logWithServerError(w, "Error parsing template", err)
}
}

func logWithServerError(w http.ResponseWriter, msg string, err error) {
log.Printf("%s: %v", msg, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
7 changes: 7 additions & 0 deletions go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
go 1.21.3

use (
./cyoa
./quiz
./urlshort
)
13 changes: 8 additions & 5 deletions urlshort/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"encoding/json"
"log/slog"
"net/http"
"os"

"gopkg.in/yaml.v3"
)

var logger = slog.New(slog.NewTextHandler(os.Stdout, nil))

type UrlMapperEntry struct {
Path string `json:"path" yaml:"path"`
Url string `json:"url" yaml:"url"`
Expand All @@ -23,14 +26,14 @@ type UrlMapper []UrlMapperEntry
// http.Handler will be called instead.
func MapHandler(pathsToUrls map[string]string, fallback http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slog.Info("Request url: " + r.URL.String())
logger.Info("Request url: " + r.URL.String())

shortenedUrl, exists := pathsToUrls[r.URL.String()]
if !exists {
slog.Warn("No url in map")
logger.Warn("No url in map")
fallback.ServeHTTP(w, r)
} else {
slog.Info("Redirect...")
logger.Info("Redirect...")
http.Redirect(w, r, shortenedUrl, http.StatusMovedPermanently)
}
}
Expand Down Expand Up @@ -58,7 +61,7 @@ func YAMLHandler(yml []byte, fallback http.Handler) (http.HandlerFunc, error) {

err := yaml.Unmarshal(yml, &urlMapper)
if err != nil {
slog.Error("error: " + err.Error())
logger.Error("Error: " + err.Error())
return nil, err
}

Expand All @@ -75,7 +78,7 @@ func JSONHandler(jsonInput []byte, fallback http.Handler) (http.HandlerFunc, err

err := json.Unmarshal(jsonInput, &urlMapper)
if err != nil {
slog.Error("error: " + err.Error())
logger.Error("Error: " + err.Error())
return nil, err
}

Expand Down
34 changes: 15 additions & 19 deletions urlshort/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,20 @@ func main() {
}
mapHandler := urlshort.MapHandler(pathsToUrls, mux)

// Build the YAMLHandler using the mapHandler as the fallback
yamlFile, err := os.ReadFile(urlMapYaml)
if err != nil {
panic(err)
}

jsonFile, err := os.ReadFile(urlMapJson)
if err != nil {
panic(err)
}
yamlFile := catch(os.ReadFile(urlMapYaml))
jsonFile := catch(os.ReadFile(urlMapJson))

yaml := string(yamlFile)
json := string(jsonFile)

yamlHandler, err := urlshort.YAMLHandler([]byte(yaml), mapHandler)
if err != nil {
panic(err)
}
yamlHandler := catch(urlshort.YAMLHandler([]byte(yaml), mapHandler))
jsonHandler := catch(urlshort.JSONHandler([]byte(json), yamlHandler))

jsonHandler, err := urlshort.JSONHandler([]byte(json), yamlHandler)
if err != nil {
panic(err)
logger.Info("Starting the server on :8080")
if err := http.ListenAndServe(":8080", jsonHandler); err != nil {
logger.Error("Error starting server", err)
}

logger.Info("Starting the server on :8080")
http.ListenAndServe(":8080", jsonHandler)
}

func defaultMux() *http.ServeMux {
Expand All @@ -60,3 +48,11 @@ func defaultMux() *http.ServeMux {
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}

func catch[T any](val T, err error) T {
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
return val
}

0 comments on commit 5fd97ac

Please sign in to comment.