Various improvements.
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
bin/
|
||||||
|
|
||||||
15
Makefile
Normal file
15
Makefile
Normal 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
|
||||||
27
README.md
27
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
56
cmd/triplex-example/main.go
Normal file
56
cmd/triplex-example/main.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
MaxIndex = block
|
MaxIndex = block
|
||||||
|
MaxRandomAttempts = 1024
|
||||||
|
|
||||||
letters = 26
|
letters = 26
|
||||||
numbers = 900 // 100–999
|
numbers = 900 // 100–999
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user