Repository: xataio/pgstream
Pull request #239 (2025-04-03 10:38:18)

Rewrite phone number transformer using generators

github-actions[bot]: ### Merging this branch will **decrease** overall coverage

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 90.10% (**-2.04%**) | :thumbsdown: |

---

<details>

<summary>Coverage by file</summary>

### Changed files (no unit tests)

| Changed File | Coverage Δ | Total | Covered | Missed | :robot: |
|--------------|------------|-------|---------|--------|---------|
| github.com/xataio/pgstream/pkg/transformers/phone_number_transformer.go | 78.26% (**-1.15%**) | 46 (+12) | 36 (+9) | 10 (+3) | :thumbsdown: |

_Please note that the "Total", "Covered", and "Missed" counts above refer to ***code statements*** instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code._

### Changed unit test files

- github.com/xataio/pgstream/pkg/transformers/phone_number_transformer_test.go

</details>

github-actions[bot]: ### Merging this branch will **decrease** overall coverage

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 89.42% (**-2.71%**) | :thumbsdown: |

---

<details>

<summary>Coverage by file</summary>

### Changed files (no unit tests)

| Changed File | Coverage Δ | Total | Covered | Missed | :robot: |
|--------------|------------|-------|---------|--------|---------|
| github.com/xataio/pgstream/pkg/transformers/phone_number_transformer.go | 77.55% (**-1.86%**) | 49 (+15) | 38 (+11) | 11 (+4) | :thumbsdown: |

_Please note that the "Total", "Covered", and "Missed" counts above refer to ***code statements*** instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code._

### Changed unit test files

- github.com/xataio/pgstream/pkg/transformers/phone_number_transformer_test.go

</details>

github-actions[bot]: ### Merging this branch will **decrease** overall coverage

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 89.42% (**-2.71%**) | :thumbsdown: |

---

<details>

<summary>Coverage by file</summary>

### Changed files (no unit tests)

| Changed File | Coverage Δ | Total | Covered | Missed | :robot: |
|--------------|------------|-------|---------|--------|---------|
| github.com/xataio/pgstream/pkg/transformers/phone_number_transformer.go | 77.55% (**-1.86%**) | 49 (+15) | 38 (+11) | 11 (+4) | :thumbsdown: |

_Please note that the "Total", "Covered", and "Missed" counts above refer to ***code statements*** instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code._

### Changed unit test files

- github.com/xataio/pgstream/pkg/transformers/phone_number_transformer_test.go

</details>

github-actions[bot]: ### Merging this branch will **decrease** overall coverage

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 88.79% (**-3.35%**) | :thumbsdown: |

---

<details>

<summary>Coverage by file</summary>

### Changed files (no unit tests)

| Changed File | Coverage Δ | Total | Covered | Missed | :robot: |
|--------------|------------|-------|---------|--------|---------|
| github.com/xataio/pgstream/pkg/transformers/phone_number_transformer.go | 76.92% (**-2.49%**) | 52 (+18) | 40 (+13) | 12 (+5) | :thumbsdown: |

_Please note that the "Total", "Covered", and "Missed" counts above refer to ***code statements*** instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code._

### Changed unit test files

- github.com/xataio/pgstream/pkg/transformers/phone_number_transformer_test.go

</details>


Patch:
diff --git a/pkg/transformers/phone_number_transformer.go b/pkg/transformers/phone_number_transformer.go
index bc19894..b4fe2ee 100644
--- a/pkg/transformers/phone_number_transformer.go
+++ b/pkg/transformers/phone_number_transformer.go
@@ -5,13 +5,14 @@ package transformers
 import (
 	"fmt"
 
-	"golang.org/x/exp/rand"
+	"github.com/xataio/pgstream/pkg/transformers/generators"
 )
 
 type PhoneNumberTransformer struct {
 	prefix    string
 	maxLength int
 	minLength int
+	generator generators.Generator
 }
 
 func NewPhoneNumberTransformer(params Parameters) (*PhoneNumberTransformer, error) {
@@ -39,6 +40,10 @@ func NewPhoneNumberTransformer(params Parameters) (*PhoneNumberTransformer, erro
 		minLength = 6
 	}
 
+	if minLength < 0 {
+		return nil, fmt.Errorf("phone_number: min_length must be greater than 0")
+	}
+
 	if maxLength < minLength {
 		return nil, fmt.Errorf("phone_number: max_length must be greater than min_length")
 	}
@@ -47,42 +52,68 @@ func NewPhoneNumberTransformer(params Parameters) (*PhoneNumberTransformer, erro
 		return nil, fmt.Errorf("phone_number: prefix must be less than min_length")
 	}
 
+	generatorType, _, err := FindParameter[string](params, "generator")
+	if err != nil {
+		return nil, fmt.Errorf("phone_number: generator must be a string: %w", err)
+	}
+	var generator generators.Generator
+	if generatorType == "deterministic" {
+		// Add an extra byte to be used for randomizing the output length as well
+		generator, err = generators.NewDeterministicBytesGenerator(maxLength + 1)
+		if err != nil {
+			return nil, fmt.Errorf("phone_number: error creating deterministic generator: %w", err)
+		}
+	} else {
+		generator = generators.NewRandomBytesGenerator(maxLength + 1)
+	}
+
 	return &PhoneNumberTransformer{
 		prefix:    prefix,
 		maxLength: maxLength,
 		minLength: minLength,
+		generator: generator,
 	}, nil
 }
 
 func (t *PhoneNumberTransformer) Transform(value any) (any, error) {
 	switch v := value.(type) {
 	case string:
-		return t.transform(v), nil
+		return t.transform([]byte(v))
 	case []byte:
-		return t.transform(string(v)), nil
+		return t.transform(v)
 	default:
 		return nil, ErrUnsupportedValueType
 	}
 }
 
-func (t *PhoneNumberTransformer) transform(str string) string {
+func (t *PhoneNumberTransformer) transform(value []byte) (string, error) {
 	const letterBytes = "0123456789"
 
+	data, err := t.generator.Generate(value)
+	if err != nil {
+		return "", err
+	}
+
 	// Generate random length between min and max (accounting for prefix)
 	targetLen := t.minLength
+	firstByte, data := data[0], data[1:] // Remove the first byte used for length calculation
 	if t.maxLength > t.minLength {
-		targetLen += rand.Intn(t.maxLength - t.minLength + 1)
+		targetLen += int(firstByte) % (t.maxLength - t.minLength + 1)
 	}
 
 	b := make([]byte, targetLen)
 	// Add prefix
 	prefixLen := len(t.prefix)
+	remainingLen := targetLen - prefixLen
+	if remainingLen > len(data) {
+		return "", fmt.Errorf("phone_number: generated data not enough for target length")
+	}
 	copy(b[:prefixLen], t.prefix)
 
 	// Fill remaining space with random digits
-	for i := prefixLen; i < targetLen; i++ {
-		b[i] = letterBytes[rand.Intn(len(letterBytes))]
+	for i := 0; i < remainingLen; i++ {
+		b[prefixLen+i] = letterBytes[int(data[i])%len(letterBytes)]
 	}
 
-	return string(b[:targetLen])
+	return string(b[:targetLen]), nil
 }


Test patch:
diff --git a/pkg/transformers/phone_number_transformer_test.go b/pkg/transformers/phone_number_transformer_test.go
index c58662e..a739b2f 100644
--- a/pkg/transformers/phone_number_transformer_test.go
+++ b/pkg/transformers/phone_number_transformer_test.go
@@ -42,6 +42,18 @@ func TestPhoneNumberTransformer_Transform(t *testing.T) {
 			wantLen:    6,
 			wantErr:    nil,
 		},
+		{
+			name: "ok - []byte without prefix, deterministic generator",
+			params: Parameters{
+				"min_length": 6,
+				"max_length": 6,
+				"generator":  "deterministic",
+			},
+			value:      []byte("12345"),
+			wantPrefix: "457059", // not prefix but the actual string, deterministic
+			wantLen:    6,
+			wantErr:    nil,
+		},
 		{
 			name: "error - prefix longer than min_length",
 			params: Parameters{