diff options
| -rw-r--r-- | .editorconfig | 36 | ||||
| -rw-r--r-- | .envrc.template | 3 | ||||
| -rw-r--r-- | .gitattributes | 21 | ||||
| -rwxr-xr-x | .githooks/commit-msg | 15 | ||||
| -rwxr-xr-x | .githooks/pre-commit | 42 | ||||
| -rwxr-xr-x | .githooks/pre-push | 18 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | .golangci.yaml | 8 | ||||
| -rw-r--r-- | CODEOWNERS | 1 | ||||
| -rw-r--r-- | CONTRIBUTING.md | 14 | ||||
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | Taskfile.go.yaml | 30 | ||||
| -rw-r--r-- | Taskfile.yaml | 17 | ||||
| -rw-r--r-- | flake.nix | 47 | ||||
| -rw-r--r-- | sync/README.md | 10 | ||||
| -rw-r--r-- | sync/chan.go | 48 | ||||
| -rw-r--r-- | sync/chan_fan.go | 79 | ||||
| -rw-r--r-- | sync/chan_pipeline.go | 48 |
18 files changed, 439 insertions, 1 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d2f1e21 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +# EditorConfig coding styles definitions. For more information about the +# properties used in this file, please see the EditorConfig documentation: +# http://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = LF +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +[*.{md}] +indent_style = tab +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json}] +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{nix}] +indent_style = space +indent_size = 2 +insert_final_newline = false +trim_trailing_whitespace = true + +[{*.go,go.mod,go.sum}] +indent_style = tab diff --git a/.envrc.template b/.envrc.template new file mode 100644 index 0000000..cf8df4b --- /dev/null +++ b/.envrc.template @@ -0,0 +1,3 @@ +dotenv + +use flake diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..74fa6b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# Automatically normalize line endings for all text-based files +# http://git-scm.com/docs/gitattributes#_end_of_line_conversion +* text=auto + +assets/* linguist-vendored +internal/infrastructure/sqlc/* linguist-generated + +# For the following file types, normalize line endings to LF on checking and +# prevent conversion to CRLF when they are checked out (this is required in +# order to prevent newline related issues) +.* text eol=lf +*.md text eol=lf +*.go text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.html text eol=lf +*.txt text eol=lf +*.css text eol=lf +*.js text eol=lf +*.json text eol=lf +LICENSE text eol=lf diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..33d6a5f --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Commit message hook - enforce conventional commit format (optional) + +commit_regex='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf)(\(.+\))?!?: .{1,50}|^fixup!' + +if ! grep -qE "$commit_regex" "$1"; then + echo "โ Invalid commit message format!" + echo "" + echo "Format: type(scope): description" + echo "Types: feat, fix, docs, style, refactor, test, chore, build, ci, perf" + echo "Example: feat(auth): add user login functionality" + echo "" + echo "Your message: $(cat $1)" + exit 1 +fi diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..95ecd68 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Pre-commit hook - fast checks only +# dependencies: php git bash grep xargs composer + +set -e + +echo "๐ Running pre-commit checks..." + +# Check if we have staged files +if git diff --cached --quiet; then + echo "No staged changes found." + exit 0 +fi + +# Lint only staged files +echo "๐ Linting staged files..." + +# Get staged Go files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' || true) + +if [ -n "$STAGED_FILES" ]; then + # Run Go linting on staged files + go vet ./... + if [ $? -eq 0 ]; then + echo "โ
go vet passed" + else + echo "โ go vet failed" + exit 1 + fi + + golangci-lint run ./... + if [ $? -eq 0 ]; then + echo "โ
golangci-lint passed" + else + echo "โ golangci-lint failed" + exit 1 + fi +else + echo "โญ๏ธ Nothing to lint, skipping..." +fi + +echo "๐ Pre-commit checks completed successfully!" diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..daf4be4 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Pre-push hook - run tests before pushing + +set -e + +echo "๐งช Running tests before push..." + +# Run the full test suite +go test ./... + +if [ $? -eq 0 ]; then + echo "โ
All tests passed! Push allowed." + exit 0 +else + echo "โ Tests failed! Push blocked." + echo "Fix the failing tests before pushing." + exit 1 +fi @@ -27,8 +27,8 @@ go.work.sum # env file .env +.envrc # Editor/IDE # .idea/ # .vscode/ - diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..100f715 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,8 @@ +version: "2" +linters: + default: standard + enable: + - testifylint +formatters: + enable: + - gofumpt diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..ecd6e62 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @dijitol diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6ddb5e3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +## Commit Messages + +We use conventional commits $^{1}$ for all commit messages. + +## Code Conventions + +We follow uber's style guide $^{2}$. + +## References + +- [1](https://www.conventionalcommits.org/en/v1.0.0/) +- [2](https://github.com/uber-go/guide/blob/master/style.md) @@ -5,3 +5,4 @@ My go standard library ## Resources - https://go.dev/doc/modules/layout#multiple-packages +- https://pkg.go.dev/std diff --git a/Taskfile.go.yaml b/Taskfile.go.yaml new file mode 100644 index 0000000..495aa59 --- /dev/null +++ b/Taskfile.go.yaml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +tasks: + test: + desc: run go tests + cmd: go test -v ./... + + test:coverage: + desc: run go tests + cmds: + - cmd: go test -v -coverprofile=tmp/cover.out ./... + - cmd: go tool cover -html=tmp/cover.out -o=tmp/cover.html + - cmd: xdg-open tmp/cover.html + + docs: + desc: open autogenerated code documentation + cmd: go tool pkgsite -open + + lint: + desc: run various linters + cmds: + - cmd: golangci-lint version + - cmd: golangci-lint run + + lint:fix: + desc: run various linters + cmds: + - cmd: golangci-lint version + - cmd: golangci-lint run --fix diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..9c2f746 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +dotenv: + - .env + +tasks: + default: + desc: lists all tasks + cmd: task --list-all + + git:hooks: + desc: sets up the githooks for the project + cmd: git config core.hooksPath .githooks + +includes: + go: ./Taskfile.go.yaml diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f8c6811 --- /dev/null +++ b/flake.nix @@ -0,0 +1,47 @@ +{ + inputs = { + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs-unstable, + nixpkgs-stable, + flake-utils, + ... + }@_inputs: + flake-utils.lib.eachDefaultSystem ( + system: + let + upkgs = import nixpkgs-unstable { + inherit system; + }; + + _spkgs = import nixpkgs-stable { + inherit system; + }; + + pkgs = upkgs; + in + { + devShells.default = pkgs.mkShell { + buildInputs = + let + lang = with pkgs; [ + go + gopls + delve + golangci-lint + ]; + tools = with pkgs; [ + go-task + ]; + in + lang ++ tools; + }; + } + ); +} diff --git a/sync/README.md b/sync/README.md new file mode 100644 index 0000000..93a4526 --- /dev/null +++ b/sync/README.md @@ -0,0 +1,10 @@ +# Sync + +## Resources + +- https://gobyexample.com/channels +- https://go.dev/tour/concurrency/2 +- https://www.oreilly.com/library/view/concurrency-in-go/9781491941294/ +- https://youtu.be/gz4DKBKe58A?si=Ks5VhvUMNaa2NQ60 +- https://go.dev/doc/effective_go#channels +- https://dave.cheney.net/2014/03/19/channel-axioms diff --git a/sync/chan.go b/sync/chan.go new file mode 100644 index 0000000..91501f9 --- /dev/null +++ b/sync/chan.go @@ -0,0 +1,48 @@ +package sync + +import "context" + +func OrDone[T any](ctx context.Context, in <-chan T) <-chan T { + if in == nil { + // we only expect this to happen during development + panic("in-channel is nil") + } + + out := make(chan T) + go func() { + defer close(out) + for { + v, ok := CancelOrReceive(ctx, in) + if !ok { + return + } + + if !CancelOrSend(ctx, out, v) { + return + } + } + }() + return out +} + +// CancelOrReceive is trying to solve the same problem as OrDone but does so +// without an extra goroutine. +func CancelOrReceive[T any](ctx context.Context, in <-chan T) (v T, ok bool) { + select { + case <-ctx.Done(): + return // <zero value of type T>, false + case v, ok = <-in: + return // v, ok + } +} + +// CancelOrSend is a helper function to simplify sending values to channels +// while keeping the context in mind. +func CancelOrSend[T any](ctx context.Context, out chan<- T, val T) bool { + select { + case <-ctx.Done(): + return false + case out <- val: + return true + } +} diff --git a/sync/chan_fan.go b/sync/chan_fan.go new file mode 100644 index 0000000..13957ed --- /dev/null +++ b/sync/chan_fan.go @@ -0,0 +1,79 @@ +package sync + +import ( + "context" + "sync" +) + +func FanOut[T any](ctx context.Context, in <-chan T, channelCount int, buffer int) []<-chan T { + if in == nil { + // we only expect this to happen during development + panic("in-channel is nil") + } + if channelCount < 1 { + channelCount = 1 + } + if buffer < 0 { + buffer = 0 + } + + chs := make([]chan T, channelCount) + for i := range channelCount { + chs[i] = make(chan T, buffer) + } + + go func() { + defer func() { + for _, out := range chs { + close(out) + } + }() + + for v := range OrDone(ctx, in) { + for _, out := range chs { + // N.b. this will block if *any* of the out channels has a full buffer + if !CancelOrSend(ctx, out, v) { + return + } + } + } + }() + + outs := make([]<-chan T, channelCount) + for i, out := range chs { + outs[i] = out + } + + return outs +} + +func FanIn[T any](ctx context.Context, buffer int, ins ...<-chan T) <-chan T { + if buffer < 0 { + buffer = 0 + } + + wg := sync.WaitGroup{} + + out := make(chan T, buffer) + + for _, in := range ins { + wg.Go(func() { + if in == nil { + return + } + + for v := range OrDone(ctx, in) { + if !CancelOrSend(ctx, out, v) { + return + } + } + }) + } + + go func() { + wg.Wait() + close(out) + }() + + return out +} diff --git a/sync/chan_pipeline.go b/sync/chan_pipeline.go new file mode 100644 index 0000000..6b5f37f --- /dev/null +++ b/sync/chan_pipeline.go @@ -0,0 +1,48 @@ +package sync + +import ( + "context" + "sync" +) + +func Pipe[IN, OUT any]( + ctx context.Context, + in <-chan IN, + transform func(context.Context, IN) OUT, + workerCount int, + buffer int, +) <-chan OUT { + if in == nil { + // we only expect this to happen during development + panic("in-channel is nil") + } + if workerCount < 1 { + workerCount = 1 + } + if buffer < 0 { + buffer = 0 + } + + wg := sync.WaitGroup{} + + out := make(chan OUT, buffer) + + for range workerCount { + wg.Go(func() { + for v := range OrDone(ctx, in) { + w := transform(ctx, v) + + if !CancelOrSend(ctx, out, w) { + return + } + } + }) + } + + go func() { + wg.Wait() + close(out) + }() + + return out +} |
