Various improvements.

This commit is contained in:
2026-02-21 00:34:57 +01:00
parent 8670c509e8
commit 09a1c8dd26
6 changed files with 160 additions and 11 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin/

15
Makefile Normal file
View File

@@ -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

View File

@@ -75,6 +75,30 @@ func main() {
- `serial.RandomCode(isInUse ...func(uint32) bool) (string, uint32, error)` - `serial.RandomCode(isInUse ...func(uint32) bool) (string, uint32, error)`
- Generates a random valid `LLL-NNN-LC` code and its corresponding index. - 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. - 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: Exported errors:
@@ -85,7 +109,8 @@ Exported errors:
- `serial.ErrNumberOutOfRange` - `serial.ErrNumberOutOfRange`
- `serial.ErrInvalidChecksum` - `serial.ErrInvalidChecksum`
- `serial.ErrIndexOutOfRange` - `serial.ErrIndexOutOfRange`
- `serial.ErrRandomGenerationFailed` - `serial.ErrRandomSourceFailed`
- `serial.ErrNoAvailableIndex`
## Testing ## Testing

View File

@@ -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)
}

View File

@@ -9,6 +9,7 @@ import (
const ( const (
MaxIndex = block MaxIndex = block
MaxRandomAttempts = 1024
letters = 26 letters = 26
numbers = 900 // 100999 numbers = 900 // 100999
@@ -23,9 +24,16 @@ var (
ErrNumberOutOfRange = errors.New("number out of range") ErrNumberOutOfRange = errors.New("number out of range")
ErrInvalidChecksum = errors.New("invalid checksum") ErrInvalidChecksum = errors.New("invalid checksum")
ErrIndexOutOfRange = errors.New("index out of range") 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) { func Encode(code string) (uint32, error) {
// Expected format: L1L2L3-NNN-L4C => length 10, positions: 0,1,2,4,5,6,8,9 // Expected format: L1L2L3-NNN-L4C => length 10, positions: 0,1,2,4,5,6,8,9
if len(code) != 10 || code[3] != '-' || code[7] != '-' { 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) { func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) {
var inUse func(uint32) bool options := RandomCodeOptions{}
if len(isInUse) > 0 { if len(isInUse) > 0 {
inUse = isInUse[0] options.IsInUse = isInUse[0]
} }
for attempts := 0; attempts < 1024; attempts++ { return RandomCodeWithOptions(options)
idx, err := randomUint32n(MaxIndex) }
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 { 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) { if inUse != nil && inUse(idx) {
@@ -129,7 +156,7 @@ func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) {
return "", 0, err return "", 0, err
} }
return "", 0, ErrRandomGenerationFailed return "", 0, ErrNoAvailableIndex
} }
func checksumLetter(idx uint32) byte { func checksumLetter(idx uint32) byte {

View File

@@ -2,6 +2,7 @@ package serial_test
import ( import (
"errors" "errors"
"fmt"
"testing" "testing"
"testing/quick" "testing/quick"
@@ -243,7 +244,30 @@ func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) {
_, _, err := serial.RandomCode(func(candidate uint32) bool { _, _, err := serial.RandomCode(func(candidate uint32) bool {
return true return true
}) })
if !errors.Is(err, serial.ErrRandomGenerationFailed) { if !errors.Is(err, serial.ErrNoAvailableIndex) {
t.Fatalf("expected random generation failed error, got: %v", err) 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)
} }
} }