Skip to content
This repository was archived by the owner on Dec 22, 2025. It is now read-only.

Commit a0d9648

Browse files
authored
Merge pull request #130 from hellofresh/patch/EES-6173-func-args
Support arguments on generation functions
2 parents 4b6cca5 + c052835 commit a0d9648

File tree

4 files changed

+168
-16
lines changed

4 files changed

+168
-16
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ You can anonymise specific columns in your table using the `Anonymise` key. Anon
211211
[Tables.Anonymise]
212212
email = "EmailAddress"
213213
firstName = "FirstName"
214+
postalCode = "DigitsN:5"
215+
creditCard = "CreditCardNum"
216+
voucher = "Password:3:5:true"
214217

215218
[[Tables]]
216219
Name = "users"
@@ -219,7 +222,7 @@ You can anonymise specific columns in your table using the `Anonymise` key. Anon
219222
password = "literal:1234"
220223
```
221224

222-
This would replace these 4 columns from the `customer` and `users` tables and run `fake.EmailAddress` and `fake.FirstName` against them respectively. We can use `literal:[some-constant-value]` to specify a constant we want to write for a column. In this case, `password = "literal:1234"` would write `1234` for every row in the password column of the users table.
225+
This would replace all the specified columns from the `customer` and `users` tables with the spcified fake function. If a function requires arguments to be passed, we can specify them splitting with the `:` character, the default value of a argument type will be used in case the provided one is invalid or missing. There is also a special function `literal:[some-constant-value]` to specify a constant we want to write for a column. In this case, `password = "literal:1234"` would write `1234` for every row in the password column of the users table.
223226

224227
#### Available data types for anonymisation
225228

pkg/anonymiser/anonymiser.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/hex"
66
"fmt"
77
"reflect"
8+
"strconv"
89
"strings"
910

1011
"github.com/hellofresh/klepto/pkg/config"
@@ -19,9 +20,19 @@ const (
1920
literalPrefix = "literal:"
2021
email = "EmailAddress"
2122
username = "UserName"
22-
password = "Password"
2323
)
2424

25+
var requireArgs = map[string]bool{
26+
"CharactersN": true,
27+
"DigitsN": true,
28+
"ParagraphsN": true,
29+
"SentencesN": true,
30+
"WordsN": true,
31+
"CreditCardNum": true,
32+
"Password": true,
33+
"Year": true,
34+
}
35+
2536
type (
2637
anonymiser struct {
2738
reader.Reader
@@ -66,6 +77,7 @@ func (a *anonymiser) ReadTable(tableName string, rowChan chan<- database.Row, op
6677
continue
6778
}
6879

80+
fakerType, args := getTypeArgs(fakerType)
6981
faker, found := Functions[fakerType]
7082
if !found {
7183
logger.WithField("anonymiser", fakerType).Error("Anonymiser is not found")
@@ -87,7 +99,7 @@ func (a *anonymiser) ReadTable(tableName string, rowChan chan<- database.Row, op
8799
hex.EncodeToString(b),
88100
)
89101
default:
90-
value = faker.Call([]reflect.Value{})[0].String()
102+
value = faker.Call(args)[0].String()
91103
}
92104
row[column] = value
93105
}
@@ -102,3 +114,47 @@ func (a *anonymiser) ReadTable(tableName string, rowChan chan<- database.Row, op
102114

103115
return nil
104116
}
117+
118+
func getTypeArgs(fakerType string) (string, []reflect.Value) {
119+
parts := strings.Split(fakerType, ":")
120+
fType := parts[0]
121+
if !requireArgs[fType] {
122+
return fType, nil
123+
}
124+
125+
return fType, parseArgs(Functions[fType], parts[1:])
126+
}
127+
128+
func parseArgs(function reflect.Value, values []string) []reflect.Value {
129+
t := function.Type()
130+
argsN := t.NumIn()
131+
if argsN > len(values) {
132+
log.WithFields(log.Fields{"expected": argsN, "received": len(values)}).Warn("Not enough arguments passed. Falling back to defaults")
133+
values = append(values, make([]string, argsN-len(values))...)
134+
}
135+
136+
argsV := make([]reflect.Value, argsN)
137+
for i := 0; i < argsN; i++ {
138+
argT := t.In(i)
139+
v := reflect.New(argT).Elem()
140+
switch argT.Kind() {
141+
case reflect.String:
142+
v.SetString(values[i])
143+
case reflect.Int:
144+
n, err := strconv.ParseInt(values[i], 10, 0)
145+
if err != nil {
146+
log.WithField("argument", values[i]).Warn("Failed to parse argument as string. Falling back to default")
147+
}
148+
v.SetInt(n)
149+
case reflect.Bool:
150+
b, err := strconv.ParseBool(values[i])
151+
if err != nil {
152+
log.WithField("argument", values[i]).Warn("Failed to parse argument as boolean. Falling back to default")
153+
}
154+
v.SetBool(b)
155+
}
156+
157+
argsV[i] = v
158+
}
159+
return argsV
160+
}

pkg/anonymiser/anonymiser_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,30 @@ func TestReadTable(t *testing.T) {
5454
opts: reader.ReadTableOpt{},
5555
config: config.Tables{{Name: "test", Anonymise: map[string]string{"column_test": "Hello"}}},
5656
},
57+
{
58+
scenario: "when column anonymiser require args",
59+
function: testWhenColumnAnonymiserRequireArgs,
60+
opts: reader.ReadTableOpt{},
61+
config: config.Tables{{Name: "test", Anonymise: map[string]string{"column_test": "DigitsN:20"}}},
62+
},
63+
{
64+
scenario: "when column anonymiser require multiple args",
65+
function: testWhenColumnAnonymiserRequireMultipleArgs,
66+
opts: reader.ReadTableOpt{},
67+
config: config.Tables{{Name: "test", Anonymise: map[string]string{"column_test": "Year:2020:2021"}}},
68+
},
69+
{
70+
scenario: "when column anonymiser require args but no values are passed",
71+
function: testWhenColumnAnonymiserRequireArgsNoValues,
72+
opts: reader.ReadTableOpt{},
73+
config: config.Tables{{Name: "test", Anonymise: map[string]string{"column_test": "CreditCardNum"}}},
74+
},
75+
{
76+
scenario: "when column anonymiser require args but the value passed is invalid",
77+
function: testWhenColumnAnonymiserRequireArgsInvalidValues,
78+
opts: reader.ReadTableOpt{},
79+
config: config.Tables{{Name: "test", Anonymise: map[string]string{"column_test1": "CharactersN:invalid", "column_test2": "Password:1:2:yes"}}},
80+
},
5781
}
5882

5983
for _, test := range tests {
@@ -137,6 +161,80 @@ func testWhenColumnAnonymiserIsInvalid(t *testing.T, opts reader.ReadTableOpt, t
137161
}
138162
}
139163

164+
func testWhenColumnAnonymiserRequireArgs(t *testing.T, opts reader.ReadTableOpt, tables config.Tables) {
165+
anonymiser := NewAnonymiser(&mockReader{}, tables)
166+
167+
rowChan := make(chan database.Row)
168+
defer close(rowChan)
169+
170+
err := anonymiser.ReadTable("test", rowChan, opts)
171+
require.NoError(t, err)
172+
173+
timeoutChan := time.After(waitTimeout)
174+
select {
175+
case row := <-rowChan:
176+
assert.NotEqual(t, "to_be_anonimised", row["column_test"])
177+
assert.Len(t, row["column_test"], 20)
178+
case <-timeoutChan:
179+
assert.FailNow(t, "Failing due to timeout")
180+
}
181+
}
182+
183+
func testWhenColumnAnonymiserRequireMultipleArgs(t *testing.T, opts reader.ReadTableOpt, tables config.Tables) {
184+
anonymiser := NewAnonymiser(&mockReader{}, tables)
185+
186+
rowChan := make(chan database.Row)
187+
defer close(rowChan)
188+
189+
err := anonymiser.ReadTable("test", rowChan, opts)
190+
require.NoError(t, err)
191+
192+
timeoutChan := time.After(waitTimeout)
193+
select {
194+
case row := <-rowChan:
195+
assert.NotEqual(t, "to_be_anonimised", row["column_test"])
196+
case <-timeoutChan:
197+
assert.FailNow(t, "Failing due to timeout")
198+
}
199+
}
200+
201+
func testWhenColumnAnonymiserRequireArgsNoValues(t *testing.T, opts reader.ReadTableOpt, tables config.Tables) {
202+
anonymiser := NewAnonymiser(&mockReader{}, tables)
203+
204+
rowChan := make(chan database.Row)
205+
defer close(rowChan)
206+
207+
err := anonymiser.ReadTable("test", rowChan, opts)
208+
require.NoError(t, err)
209+
210+
timeoutChan := time.After(waitTimeout)
211+
select {
212+
case row := <-rowChan:
213+
assert.NotEqual(t, "to_be_anonimised", row["column_test"])
214+
case <-timeoutChan:
215+
assert.FailNow(t, "Failing due to timeout")
216+
}
217+
}
218+
219+
func testWhenColumnAnonymiserRequireArgsInvalidValues(t *testing.T, opts reader.ReadTableOpt, tables config.Tables) {
220+
anonymiser := NewAnonymiser(&mockReader{}, tables)
221+
222+
rowChan := make(chan database.Row)
223+
defer close(rowChan)
224+
225+
err := anonymiser.ReadTable("test", rowChan, opts)
226+
require.NoError(t, err)
227+
228+
timeoutChan := time.After(waitTimeout)
229+
select {
230+
case row := <-rowChan:
231+
assert.NotEqual(t, "to_be_anonimised", row["column_test1"])
232+
assert.NotEqual(t, "to_be_anonimised", row["column_test2"])
233+
case <-timeoutChan:
234+
assert.FailNow(t, "Failing due to timeout")
235+
}
236+
}
237+
140238
type mockReader struct{}
141239

142240
func (m *mockReader) GetTables() ([]string, error) { return []string{"table_test"}, nil }

pkg/anonymiser/fake.go

Lines changed: 8 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)