From 09a1c8dd2687e341d412fe68b975c026c7af4905 Mon Sep 17 00:00:00 2001 From: micha Date: Sat, 21 Feb 2026 00:34:57 +0100 Subject: [PATCH] Various improvements. --- .gitignore | 2 ++ Makefile | 15 ++++++++++ README.md | 27 +++++++++++++++++- cmd/triplex-example/main.go | 56 +++++++++++++++++++++++++++++++++++++ serial/serial.go | 43 ++++++++++++++++++++++------ serial/serial_test.go | 28 +++++++++++++++++-- 6 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/triplex-example/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d855be8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cf93246 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +APP_EXAMPLE := triplex-example + +.PHONY: test build-example run-example fmt + +test: + go test ./... + +build-example: + go build -o bin/$(APP_EXAMPLE) ./cmd/$(APP_EXAMPLE) + +run-example: + go run ./cmd/$(APP_EXAMPLE) + +fmt: + gofmt -w ./serial/*.go ./cmd/$(APP_EXAMPLE)/*.go diff --git a/README.md b/README.md index c9f00fb..3e4e83b 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,30 @@ func main() { - `serial.RandomCode(isInUse ...func(uint32) bool) (string, uint32, error)` - Generates a random valid `LLL-NNN-LC` code and its corresponding index. - If provided, the callback is used to skip indices already in use by the client. +- `serial.RandomCodeWithOptions(options serial.RandomCodeOptions) (string, uint32, error)` + - Configurable variant with `MaxAttempts`, `IsInUse`, and custom `RandomIndex` source. + +Example with options: + +```go +// See the runnable command at ./cmd/triplex-example/main.go +``` + +## Commands + +Runnable example command: + +```bash +go run ./cmd/triplex-example +``` + +Using Makefile: + +```bash +make run-example +make build-example +make test +``` Exported errors: @@ -85,7 +109,8 @@ Exported errors: - `serial.ErrNumberOutOfRange` - `serial.ErrInvalidChecksum` - `serial.ErrIndexOutOfRange` -- `serial.ErrRandomGenerationFailed` +- `serial.ErrRandomSourceFailed` +- `serial.ErrNoAvailableIndex` ## Testing diff --git a/cmd/triplex-example/main.go b/cmd/triplex-example/main.go new file mode 100644 index 0000000..6709105 --- /dev/null +++ b/cmd/triplex-example/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + + "git.hoiting.org/micha/triplex/serial" +) + +func alreadyUsedByClient(idx uint32) bool { + return idx%2 == 0 +} + +func myRandomIndex(max uint32) (uint32, error) { + if max == 0 { + return 0, fmt.Errorf("max must be > 0") + } + + var buf [4]byte + if _, err := rand.Read(buf[:]); err != nil { + return 0, err + } + + return binary.LittleEndian.Uint32(buf[:]) % max, nil +} + +func main() { + opts := serial.RandomCodeOptions{ + MaxAttempts: 500, + IsInUse: func(idx uint32) bool { + return alreadyUsedByClient(idx) + }, + RandomIndex: func(max uint32) (uint32, error) { + return myRandomIndex(max) + }, + } + + code, idx, err := serial.RandomCodeWithOptions(opts) + if err != nil { + if errors.Is(err, serial.ErrRandomSourceFailed) { + fmt.Println("random source failed") + return + } + if errors.Is(err, serial.ErrNoAvailableIndex) { + fmt.Println("no available index found") + return + } + fmt.Println("unexpected error:", err) + return + } + + fmt.Println("code:", code) + fmt.Println("idx:", idx) +} diff --git a/serial/serial.go b/serial/serial.go index cf74716..a0ca049 100644 --- a/serial/serial.go +++ b/serial/serial.go @@ -8,7 +8,8 @@ import ( ) const ( - MaxIndex = block + MaxIndex = block + MaxRandomAttempts = 1024 letters = 26 numbers = 900 // 100–999 @@ -23,9 +24,16 @@ var ( ErrNumberOutOfRange = errors.New("number out of range") ErrInvalidChecksum = errors.New("invalid checksum") ErrIndexOutOfRange = errors.New("index out of range") - ErrRandomGenerationFailed = errors.New("random code generation failed") + ErrRandomSourceFailed = errors.New("random source failed") + ErrNoAvailableIndex = errors.New("no available index found") ) +type RandomCodeOptions struct { + MaxAttempts int + IsInUse func(uint32) bool + RandomIndex func(max uint32) (uint32, error) +} + func Encode(code string) (uint32, error) { // Expected format: L1L2L3-NNN-L4C => length 10, positions: 0,1,2,4,5,6,8,9 if len(code) != 10 || code[3] != '-' || code[7] != '-' { @@ -103,15 +111,34 @@ func Decode(idx uint32) (string, error) { } func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) { - var inUse func(uint32) bool + options := RandomCodeOptions{} if len(isInUse) > 0 { - inUse = isInUse[0] + options.IsInUse = isInUse[0] } - for attempts := 0; attempts < 1024; attempts++ { - idx, err := randomUint32n(MaxIndex) + return RandomCodeWithOptions(options) +} + +func RandomCodeWithOptions(options RandomCodeOptions) (string, uint32, error) { + maxAttempts := options.MaxAttempts + if maxAttempts <= 0 { + maxAttempts = MaxRandomAttempts + } + + inUse := options.IsInUse + randomIndex := options.RandomIndex + if randomIndex == nil { + randomIndex = randomUint32n + } + + for attempts := 0; attempts < maxAttempts; attempts++ { + idx, err := randomIndex(MaxIndex) if err != nil { - return "", 0, err + return "", 0, fmt.Errorf("%w: %v", ErrRandomSourceFailed, err) + } + + if idx >= MaxIndex { + return "", 0, fmt.Errorf("%w: index out of range", ErrRandomSourceFailed) } if inUse != nil && inUse(idx) { @@ -129,7 +156,7 @@ func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) { return "", 0, err } - return "", 0, ErrRandomGenerationFailed + return "", 0, ErrNoAvailableIndex } func checksumLetter(idx uint32) byte { diff --git a/serial/serial_test.go b/serial/serial_test.go index 59008fa..98108c5 100644 --- a/serial/serial_test.go +++ b/serial/serial_test.go @@ -2,6 +2,7 @@ package serial_test import ( "errors" + "fmt" "testing" "testing/quick" @@ -243,7 +244,30 @@ func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) { _, _, err := serial.RandomCode(func(candidate uint32) bool { return true }) - if !errors.Is(err, serial.ErrRandomGenerationFailed) { - t.Fatalf("expected random generation failed error, got: %v", err) + if !errors.Is(err, serial.ErrNoAvailableIndex) { + t.Fatalf("expected no available index error, got: %v", err) + } +} + +func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) { + _, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{ + MaxAttempts: 1, + IsInUse: func(candidate uint32) bool { + return true + }, + }) + if !errors.Is(err, serial.ErrNoAvailableIndex) { + t.Fatalf("expected no available index error, got: %v", err) + } +} + +func TestRandomCodeWithOptionsRandomSourceFailure(t *testing.T) { + _, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{ + RandomIndex: func(max uint32) (uint32, error) { + return 0, fmt.Errorf("rng down") + }, + }) + if !errors.Is(err, serial.ErrRandomSourceFailed) { + t.Fatalf("expected random source failed error, got: %v", err) } }