Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09d7c98069 | |||
| b46a998aac | |||
| dbd513f7b4 | |||
| 7e2b50faf0 | |||
| 9ea29d3ba4 | |||
| bc637b3a77 | |||
| 4b8a092201 |
@ -12,7 +12,7 @@
|
||||
"remoteUser": "vscode",
|
||||
|
||||
// Run once after the container is created.
|
||||
"postCreateCommand": "make init",
|
||||
"postCreateCommand": "make init && make tools",
|
||||
|
||||
// Fix ownership of the mounted ~/.claude so the vscode user can read host auth.
|
||||
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude 2>/dev/null || true",
|
||||
@ -35,6 +35,7 @@
|
||||
"github.copilot.inlineSuggest.enable": false,
|
||||
|
||||
"go.useLanguageServer": true,
|
||||
"go.toolsManagement.autoUpdate": false,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fast"],
|
||||
"go.lintOnSave": "workspace",
|
||||
@ -59,6 +60,7 @@
|
||||
"forwardPorts": [8080],
|
||||
|
||||
"remoteEnv": {
|
||||
"CONFIG_PATH": "${containerWorkspaceFolder}/config/dev.yaml"
|
||||
"CONFIG_PATH": "${containerWorkspaceFolder}/config/dev.yaml",
|
||||
"GOPRIVATE": "gitea.djmil.dev"
|
||||
}
|
||||
}
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -1,12 +1,10 @@
|
||||
{
|
||||
// ── Go ─────────────────────────────────────────────────────────────────────
|
||||
"go.useLanguageServer": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fast"],
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.testFlags": ["-race"],
|
||||
"go.coverOnSave": false,
|
||||
"go.generateOnSave": false,
|
||||
|
||||
// ── Editor ─────────────────────────────────────────────────────────────────
|
||||
"[go]": {
|
||||
@ -16,9 +14,6 @@
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||
},
|
||||
|
||||
// ── Files ──────────────────────────────────────────────────────────────────
|
||||
"files.exclude": {
|
||||
@ -27,16 +22,16 @@
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/bin": true,
|
||||
"**/mocks": true,
|
||||
"**/vendor": true
|
||||
},
|
||||
|
||||
// ── Test explorer ──────────────────────────────────────────────────────────
|
||||
"go.testExplorer.enable": true,
|
||||
"makefile.configureOnOpen": false,
|
||||
"cSpell.words": [
|
||||
"djmil",
|
||||
"Expectf",
|
||||
"gitea",
|
||||
"golangci",
|
||||
"testutil"
|
||||
]
|
||||
}
|
||||
|
||||
7
.vscode/tasks.json
vendored
7
.vscode/tasks.json
vendored
@ -41,13 +41,6 @@
|
||||
"presentation": { "reveal": "always", "panel": "shared" },
|
||||
"problemMatcher": "$go"
|
||||
},
|
||||
{
|
||||
"label": "mocks",
|
||||
"type": "shell",
|
||||
"command": "make mocks",
|
||||
"group": "none",
|
||||
"presentation": { "reveal": "always", "panel": "shared" }
|
||||
},
|
||||
{
|
||||
"label": "security scan",
|
||||
"type": "shell",
|
||||
|
||||
@ -39,7 +39,13 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
|
||||
- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...`
|
||||
- **Packages** — keep `cmd/` thin (wiring only); business logic belongs in `internal/`
|
||||
- **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free)
|
||||
- **Errors** — wrap with `fmt.Errorf("context: %w", err)`; never swallow errors silently
|
||||
- **Errors** — `pkg/result` is the default error-handling mechanism for all code in this repo, including public APIs:
|
||||
- functions return `result.Expect[T]` instead of `(T, error)`
|
||||
- callers unwrap with `.Expect("context")` (panics with annotated error + stack trace) or `.Must()` (panics with raw error)
|
||||
- top-level entry points (e.g. `cmd/` functions, HTTP handlers) defer `result.Catch(&err)` to convert any result panic into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked
|
||||
- bridge existing `(T, error)` stdlib/third-party calls with `result.Of(...)`: `result.Of(os.ReadFile("cfg.json")).Expect("read config")`
|
||||
- use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error
|
||||
- still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Fail`
|
||||
- **Logging** — use `log.WithField("key", val)` for structured context; never `fmt.Sprintf` in log messages; `log/slog` is the backend
|
||||
- **Config** — all configuration through `internal/config` (flag-parsed); no hard-coded values in logic packages
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@ -1,4 +1,4 @@
|
||||
.PHONY: help init setup build run test test-race lint lint-fix security clean
|
||||
.PHONY: help init setup build run test test-race lint lint-fix security docs clean
|
||||
|
||||
include tools.versions
|
||||
|
||||
@ -14,6 +14,7 @@ help: ## Show this help message
|
||||
|
||||
# ── First-time setup ───────────────────────────────────────────────────────────
|
||||
init: ## First-time project init: fetch deps, configure git hooks
|
||||
go env -w GOPRIVATE=gitea.djmil.dev
|
||||
go mod tidy
|
||||
$(MAKE) setup
|
||||
@echo "Done! Run 'make build' to verify."
|
||||
@ -30,6 +31,7 @@ tools: ## Install tool binaries to GOPATH/bin (versions from tools.versions)
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@$(GOSEC_VERSION)
|
||||
go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)
|
||||
go install golang.org/x/pkgsite/cmd/pkgsite@$(PKGSITE_VERSION)
|
||||
|
||||
# ── Build ──────────────────────────────────────────────────────────────────────
|
||||
build: ## Compile the binary to ./bin/
|
||||
@ -64,6 +66,10 @@ security: ## Run gosec + govulncheck
|
||||
@echo "--- govulncheck ---"
|
||||
go run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) ./...
|
||||
|
||||
# ── Docs ───────────────────────────────────────────────────────────────────────
|
||||
docs: ## Serve package documentation locally via pkgsite (http://localhost:8080)
|
||||
go run golang.org/x/pkgsite/cmd/pkgsite@$(PKGSITE_VERSION) -open .
|
||||
|
||||
# ── Release ────────────────────────────────────────────────────────────────────
|
||||
release: ## List releases, or tag+push a new one (usage: make release VERSION=v0.1.0)
|
||||
ifdef VERSION
|
||||
|
||||
@ -100,6 +100,31 @@ func Example_nonErrorPanic() {
|
||||
// non-error panic: unexpected runtime problem
|
||||
}
|
||||
|
||||
// Example_expectf shows Expectf for context messages that include runtime
|
||||
// values — equivalent to Expect(fmt.Sprintf(...)) but more concise.
|
||||
func Example_expectf() {
|
||||
port := parsePort("3000").Expectf("read port from arg %d", 1)
|
||||
fmt.Println(port)
|
||||
// Output:
|
||||
// 3000
|
||||
}
|
||||
|
||||
// Example_expectfError shows that Expectf annotates the error message with the
|
||||
// formatted context, just like Expect does.
|
||||
func Example_expectfError() {
|
||||
run := func() (err error) {
|
||||
defer result.Catch(&err)
|
||||
_ = parsePort("99999").Expectf("arg %d port value", 2)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := run(); err != nil {
|
||||
fmt.Println("caught:", err)
|
||||
}
|
||||
// Output:
|
||||
// caught: arg 2 port value: parsePort: 99999 out of range
|
||||
}
|
||||
|
||||
// Example_fail shows constructing a failed Expect explicitly, e.g. when a
|
||||
// function detects an error condition before calling any fallible op.
|
||||
func Example_fail() {
|
||||
|
||||
@ -59,6 +59,20 @@ func (r Expect[T]) Expect(msg string) T {
|
||||
return r.value
|
||||
}
|
||||
|
||||
// Expectf is like [Expect.Expect] but accepts a fmt.Sprintf-style format string
|
||||
// for the context message. The wrapped error is always appended as ": <err>".
|
||||
//
|
||||
// data := Parse(raw).Expectf("parse user input id=%d", id)
|
||||
func (r Expect[T]) Expectf(format string, args ...any) T {
|
||||
if r.err != nil {
|
||||
panic(&stackError{
|
||||
err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err),
|
||||
stack: debug.Stack(),
|
||||
})
|
||||
}
|
||||
return r.value
|
||||
}
|
||||
|
||||
// Unwrap returns the value and error in the standard Go (value, error) form.
|
||||
// Useful at the boundary where you want to re-join normal error-return code.
|
||||
func (r Expect[T]) Unwrap() (T, error) {
|
||||
|
||||
23
rename.sh
23
rename.sh
@ -126,11 +126,24 @@ sedi() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: rename module path in a file, preserving pkg/result imports ───────
|
||||
# pkg/result is a standalone publishable package; its import path must not
|
||||
# change when the consuming project is renamed.
|
||||
RESULT_PKG="${OLD_MODULE}/pkg/result"
|
||||
PLACEHOLDER="__RESULT_PKG_PLACEHOLDER__"
|
||||
|
||||
rename_module_in() {
|
||||
local file="$1"
|
||||
sedi "s|${RESULT_PKG}|${PLACEHOLDER}|g" "$file"
|
||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$file"
|
||||
sedi "s|${PLACEHOLDER}|${RESULT_PKG}|g" "$file"
|
||||
}
|
||||
|
||||
# ── Apply substitutions ───────────────────────────────────────────────────────
|
||||
heading "Applying changes"
|
||||
|
||||
# 1. go.mod — module declaration
|
||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" go.mod
|
||||
rename_module_in go.mod
|
||||
info "go.mod"
|
||||
|
||||
# 2. All Go source files — import paths
|
||||
@ -143,7 +156,7 @@ GO_FILES=$(find . \
|
||||
CHANGED_GO=0
|
||||
for f in $GO_FILES; do
|
||||
if grep -q "$OLD_MODULE" "$f" 2>/dev/null; then
|
||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$f"
|
||||
rename_module_in "$f"
|
||||
CHANGED_GO=$((CHANGED_GO + 1))
|
||||
fi
|
||||
done
|
||||
@ -159,19 +172,19 @@ fi
|
||||
# 4. README.md — heading + all module path occurrences
|
||||
if [[ -f README.md ]]; then
|
||||
sedi "s|^# ${OLD_PROJECT}$|# ${NEW_PROJECT}|g" README.md
|
||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" README.md
|
||||
rename_module_in README.md
|
||||
info "README.md"
|
||||
fi
|
||||
|
||||
# 5. CLAUDE.md — Module line
|
||||
if [[ -f CLAUDE.md ]]; then
|
||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" CLAUDE.md
|
||||
rename_module_in CLAUDE.md
|
||||
info "CLAUDE.md"
|
||||
fi
|
||||
|
||||
# 6. .golangci.yml — goimports local-prefixes
|
||||
if [[ -f .golangci.yml ]]; then
|
||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" .golangci.yml
|
||||
rename_module_in .golangci.yml
|
||||
info ".golangci.yml"
|
||||
fi
|
||||
|
||||
|
||||
@ -2,3 +2,4 @@ DELVE_VERSION=v1.26.1
|
||||
GOLANGCI_LINT_VERSION=v1.64.8
|
||||
GOSEC_VERSION=v2.24.7
|
||||
GOVULNCHECK_VERSION=v1.1.4
|
||||
PKGSITE_VERSION=latest
|
||||
|
||||
Loading…
Reference in New Issue
Block a user