hike-go

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT

README

Hike

Hike contains the bits and pieces of code we reuse across our Go projects: layered configuration, encryption and secret token handling, and HTTP request parsing.

It is a library — there is no binary. Add it with:

go get github.com/urban-connect/hike-go

Packages

config — layered configuration

Reader is the core abstraction. Each reader fills in a config struct you own, and you compose them by running several in order — later readers override earlier ones.

type Config struct {
    config.BaseConfig

    API struct {
        Host string `json:"host" env:"api_host"`
        Port int    `json:"port" env:"api_port"`
    } `json:"api"`
}

func NewConfig() (*Config, error) {
    cfg := &Config{}

    readers := []config.Reader{
        config.FromEnv("MYAPP_"),                        // MYAPP_API_HOST, MYAPP_CRYPTO_KEY, ...
        config.FromFile("config/settings.json", false),  // required
        config.FromFile("config/settings.local.json", true), // optional, gitignored
    }

    for _, reader := range readers {
        if err := reader.Read(cfg); err != nil {
            return nil, err
        }
    }

    return cfg, nil
}

Embed config.BaseConfig to pick up env, version and the crypto key/nonce fields. Fields carry both json and env tags because the same struct is filled from either source. Env lookup is prefixed and upper-cased, so env:"crypto_key" with prefix MYAPP_ reads MYAPP_CRYPTO_KEY.

For secrets committed to the repo, FromEncryptedFile decrypts before parsing. It takes anything satisfying config.Crypto (Encrypt/Decrypt) — crypto.AES does:

aes, err := crypto.NewAES(cfg.Crypto.Key, cfg.Crypto.Nonce)
if err != nil {
    return nil, err
}

reader := config.FromEncryptedFile("config/settings.json.encrypted", true, aes)

config.NewLogger(cfg.BaseConfig) returns a stdlib *slog.Logger — a JSON handler at info level when env is production, and a human-readable text handler at debug level everywhere else.

crypto — encryption and tokens

crypto.AES is AES-GCM with a fixed key and nonce, both base64-encoded. The fixed nonce is deliberate: it makes encrypting a config file reproducible, so re-encrypting unchanged settings produces no diff. That also means it is not suitable for encrypting many distinct messages under one key — use it for config at rest, not as a general-purpose cipher.

crypto.Token wraps a secret string, hashes it with bcrypt for storage, and checks a candidate against a stored digest — the usual pattern behind static access-token header auth:

token, err := crypto.RandomToken()  // give this to the caller
digest, err := token.Digest()       // store this in settings

// later, on each request
err := crypto.NewToken(header).Validate(digest)
api — request parameter binding

api.ParseParams binds request data into a struct via params:"key,source" tags, where source is path, query (the default), header, or form. Path values use r.PathValue, i.e. Go 1.22 net/http routing patterns; form values use r.PostFormValue.

A separate, optional transform tag normalizes a string field's value after it is populated — a comma-separated list of transforms applied in order, currently lowercase and uppercase.

type Params struct {
    ID    string `params:"id,path" transform:"lowercase"`
    Token string `params:"authorization,header"`
    Limit int    `params:"limit,query"`
    Turn  string `params:"turn,form"`
}

var params Params
if err := api.ParseParams(r, &params); err != nil {
    // ...
}

api.ParsePayload decodes a JSON request body and applies the same transform tags, so a payload can be normalized the same way params are. api.ApplyTransforms runs the transforms on an already-populated struct if you need it standalone.

type Payload struct {
    DeviceID string `json:"device_id" transform:"lowercase"`
}

var payload Payload
if err := api.ParsePayload(r, &payload); err != nil {
    // ...
}

Supported field kinds are string, int, int64, float32, float64, and bool. An absent value leaves the field untouched rather than erroring.

Design notes

The three packages are independent — none of them imports another, so you can use api without pulling in config, and so on.

Hike stays deliberately thin on dependencies. Logging is stdlib log/slog, so the library brings no logging framework of its own and leaves the choice of handler to you. That keeps the direct requirements down to golang.org/x/crypto and go-envconfig.

The packages are written to stay generic rather than to serve one application. Readers take any and fill a config struct you own, NewLogger takes the embeddable BaseConfig instead of your concrete type, and FromEncryptedFile accepts any config.Crypto implementation rather than depending on the crypto package directly.

Development

go build ./...
go test ./...
go vet ./...
gofmt -l .   # must print nothing

# CI also gates on the modernize analyzer
go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest ./...

License

MIT — see LICENSE.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL