Skip to content

Commit 8ef876c

Browse files
authored
Add dsl.Randomiser that lets you pass your own example randomiser (goadesign#3191)
* Add dsl.Randomiser that lets you pass your own example randomiser 1. Rename `expr.Random` to `expr.FakerRandom` 2. Add an interface `expr.Randomiser` that covers the methods we need 3. Split out `expr.ExampleGenerator` that handles the 'do we already have an example for this' logic from the 'generate examples' stuff 4. Change `(expr.APIExpr).random` to `(expr.APIExpr).ExampleGenerator`, and initialise it with a faker-based random example generator 5. Add `dsl.Randomiser` which overrides `(expr.APIExpr).ExampleGenerator` with whatever randomiser you want to pass in, wrapped in the non-customisable 'only generate one example for each thing' logic. I've also added an `ArrayLength` method to the randomiser interface, and updated places that previously would call `r.Int()` to generate an array length to use that. The motivation for this PR is that we have a large design and many people working on it, and the diffs we get on OpenAPI schema files can be quite hard to read with most of the changes coming from changes to the random examples due to the order of example generation when we add new types (for example). We'd therefore like to disable all random example generation and use hard-coded default examples, and define manual examples with `dsl.Example` where we'd like them. * Randomiser -> Randomizer * NewRandom -> NewFakerRandomizer and NewRandomExampleGenerator -> NewRandom to maintain the existing interface * Reorder the expr/random.go file to be clearer * Rename FakerRandom -> FakerRandomizer It implements `Randomizer`, so should really have that in its name * Add DeterministicRandomizer, which returns hard-coded values * Try to match the old randomization logic exactly * Fix the openapi v2 tests * remove spew * Actually use `ArrayLength()` I over-did it a bit in my effort to fix tests. * Sort out the examples
1 parent 3c0e3da commit 8ef876c

18 files changed

+252
-127
lines changed

codegen/service/service_data.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,7 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData {
912912
payloadDesc = fmt.Sprintf("%s is the payload type of the %s service %s method.",
913913
payloadName, m.Service.Name, m.Name)
914914
}
915-
payloadEx = m.Payload.Example(expr.Root.API.Random())
915+
payloadEx = m.Payload.Example(expr.Root.API.ExampleGenerator)
916916
}
917917
if m.Result.Type != expr.Empty {
918918
rname = scope.GoTypeName(m.Result)
@@ -926,7 +926,7 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData {
926926
resultDesc = fmt.Sprintf("%s is the result type of the %s service %s method.",
927927
rname, m.Service.Name, m.Name)
928928
}
929-
resultEx = m.Result.Example(expr.Root.API.Random())
929+
resultEx = m.Result.Example(expr.Root.API.ExampleGenerator)
930930
}
931931
if len(m.Errors) > 0 {
932932
errors = make([]*ErrorInitData, len(m.Errors))
@@ -1002,7 +1002,7 @@ func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRe
10021002
spayloadDesc = fmt.Sprintf("%s is the streaming payload type of the %s service %s method.",
10031003
spayloadName, m.Service.Name, m.Name)
10041004
}
1005-
spayloadEx = m.StreamingPayload.Example(expr.Root.API.Random())
1005+
spayloadEx = m.StreamingPayload.Example(expr.Root.API.ExampleGenerator)
10061006
}
10071007
svrStream := &StreamData{
10081008
Interface: vname + "ServerStream",

dsl/api.go

+32
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,38 @@ func License(fn func()) {
150150
eval.IncompatibleDSL()
151151
}
152152

153+
// Randomizer sets the API example randomizer.
154+
//
155+
// Randomizer must appear in a API expression.
156+
//
157+
// Randomizer takes a single argument which is an implementation of
158+
// expr.Randomizer.
159+
//
160+
// The default randomizer uses the API name as the seed, to get consistent
161+
// random examples.
162+
//
163+
// Example:
164+
//
165+
// var _ = API("divider", func() {
166+
// Randomizer(expr.NewFakerRandomizer("different seed"))
167+
// })
168+
//
169+
// There's also a deterministic randomizer which will only generate one example
170+
// for each type, so all strings are "abc123", all ints are 1, etc.
171+
//
172+
// Example:
173+
//
174+
// var _ = API("divider", func() {
175+
// Randomizer(expr.NewDeterministicRandomizer())
176+
// })
177+
func Randomizer(randomizer expr.Randomizer) {
178+
if s, ok := eval.Current().(*expr.APIExpr); ok {
179+
s.ExampleGenerator = &expr.ExampleGenerator{Randomizer: randomizer}
180+
return
181+
}
182+
eval.IncompatibleDSL()
183+
}
184+
153185
// Docs provides external documentation URLs. It is used by the generated
154186
// OpenAPI specification.
155187
//

expr/api.go

+6-14
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type (
4242
GRPC *GRPCExpr
4343

4444
// random generator used to build examples for the API types.
45-
random *Random
45+
ExampleGenerator *ExampleGenerator
4646
}
4747

4848
// ContactExpr contains the API contact information.
@@ -75,10 +75,11 @@ type (
7575
// NewAPIExpr initializes an API expression.
7676
func NewAPIExpr(name string, dsl func()) *APIExpr {
7777
return &APIExpr{
78-
Name: name,
79-
HTTP: new(HTTPExpr),
80-
GRPC: new(GRPCExpr),
81-
DSLFunc: dsl,
78+
Name: name,
79+
HTTP: new(HTTPExpr),
80+
GRPC: new(GRPCExpr),
81+
DSLFunc: dsl,
82+
ExampleGenerator: NewRandom(name),
8283
}
8384
}
8485

@@ -102,15 +103,6 @@ func (a *APIExpr) Schemes() []string {
102103
return ss
103104
}
104105

105-
// Random returns the random generator associated with a. APIs with identical
106-
// names return generators that return the same sequence of pseudo random values.
107-
func (a *APIExpr) Random() *Random {
108-
if a.random == nil {
109-
a.random = NewRandom(a.Name)
110-
}
111-
return a.random
112-
}
113-
114106
// DefaultServer returns a server expression that describes a server which
115107
// exposes all the services in the design and listens on localhost port 80 for
116108
// HTTP requests and port 8080 for gRPC requests.

expr/example.go

+18-18
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const (
1717
// Example returns the example set on the attribute at design time. If there
1818
// isn't such a value then Example computes a random value for the attribute
1919
// using the given random value producer.
20-
func (a *AttributeExpr) Example(r *Random) interface{} {
20+
func (a *AttributeExpr) Example(r *ExampleGenerator) interface{} {
2121
if ex := a.ExtractUserExamples(); len(ex) > 0 {
2222
// Return the last item in the slice so that examples can be overridden
2323
// in the DSL. Overridden examples are always appended to the UserExamples
@@ -80,7 +80,7 @@ func (a *AttributeExpr) Example(r *Random) interface{} {
8080

8181
// NewLength returns an int that validates the generator attribute length
8282
// validations if any.
83-
func NewLength(a *AttributeExpr, r *Random) int {
83+
func NewLength(a *AttributeExpr, r *ExampleGenerator) int {
8484
if hasLengthValidation(a) {
8585
minlength, maxlength := math.Inf(1), math.Inf(-1)
8686
if a.Validation.MinLength != nil {
@@ -110,7 +110,7 @@ func NewLength(a *AttributeExpr, r *Random) int {
110110
}
111111
return count
112112
}
113-
return r.Int()%3 + 2
113+
return r.ArrayLength()
114114
}
115115

116116
func hasLengthValidation(a *AttributeExpr) bool {
@@ -143,13 +143,13 @@ func hasMinMaxValidation(a *AttributeExpr) bool {
143143
}
144144

145145
// byLength generates a random size array of examples based on what's given.
146-
func byLength(a *AttributeExpr, r *Random) interface{} {
146+
func byLength(a *AttributeExpr, r *ExampleGenerator) interface{} {
147147
count := NewLength(a, r)
148148
switch a.Type.Kind() {
149149
case StringKind:
150-
return r.faker.Characters(count)
150+
return r.Characters(count)
151151
case BytesKind:
152-
return []byte(r.faker.Characters(count))
152+
return []byte(r.Characters(count))
153153
case MapKind:
154154
raw := make(map[interface{}]interface{})
155155
m := a.Type.(*Map)
@@ -170,7 +170,7 @@ func byLength(a *AttributeExpr, r *Random) interface{} {
170170
}
171171

172172
// byEnum returns a random selected enum value.
173-
func byEnum(a *AttributeExpr, r *Random) interface{} {
173+
func byEnum(a *AttributeExpr, r *ExampleGenerator) interface{} {
174174
if !hasEnumValidation(a) {
175175
return nil
176176
}
@@ -181,20 +181,20 @@ func byEnum(a *AttributeExpr, r *Random) interface{} {
181181
}
182182

183183
// byFormat returns a random example based on the format the user asks.
184-
func byFormat(a *AttributeExpr, r *Random) interface{} {
184+
func byFormat(a *AttributeExpr, r *ExampleGenerator) interface{} {
185185
if !hasFormatValidation(a) {
186186
return nil
187187
}
188188
format := a.Validation.Format
189189
if res, ok := map[ValidationFormat]interface{}{
190-
FormatEmail: r.faker.Email(),
191-
FormatHostname: r.faker.DomainName() + "." + r.faker.DomainSuffix(),
190+
FormatEmail: r.Email(),
191+
FormatHostname: r.Hostname(),
192192
FormatDate: time.Unix(int64(r.Int())%1454957045, 0).UTC().Format("2006-01-02"), // to obtain a "fixed" rand
193193
FormatDateTime: time.Unix(int64(r.Int())%1454957045, 0).UTC().Format(time.RFC3339), // to obtain a "fixed" rand
194-
FormatIPv4: r.faker.IPv4Address().String(),
195-
FormatIPv6: r.faker.IPv6Address().String(),
196-
FormatIP: r.faker.IPv4Address().String(),
197-
FormatURI: r.faker.URL(),
194+
FormatIPv4: r.IPv4Address().String(),
195+
FormatIPv6: r.IPv6Address().String(),
196+
FormatIP: r.IPv4Address().String(),
197+
FormatURI: r.URL(),
198198
FormatMAC: func() string {
199199
res, err := regen.Generate(`([0-9A-F]{2}-){5}[0-9A-F]{2}`)
200200
if err != nil {
@@ -203,7 +203,7 @@ func byFormat(a *AttributeExpr, r *Random) interface{} {
203203
return res
204204
}(),
205205
FormatCIDR: "192.168.100.14/24",
206-
FormatRegexp: r.faker.Characters(3) + ".*",
206+
FormatRegexp: r.Characters(3) + ".*",
207207
FormatRFC1123: time.Unix(int64(r.Int())%1454957045, 0).UTC().Format(time.RFC1123), // to obtain a "fixed" rand
208208
FormatUUID: func() string {
209209
res, err := regen.Generate(`[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}`)
@@ -222,19 +222,19 @@ func byFormat(a *AttributeExpr, r *Random) interface{} {
222222
// byPattern generates a random value that satisfies the pattern.
223223
//
224224
// Note: if multiple patterns are given, only one of them is used.
225-
func byPattern(a *AttributeExpr, r *Random) interface{} {
225+
func byPattern(a *AttributeExpr, r *ExampleGenerator) interface{} {
226226
if !hasPatternValidation(a) {
227227
return false
228228
}
229229
pattern := a.Validation.Pattern
230230
gen, err := regen.NewGenerator(pattern, &regen.GeneratorArgs{MaxUnboundedRepeatCount: 6})
231231
if err != nil {
232-
return r.faker.Name()
232+
return r.Name()
233233
}
234234
return gen.Generate()
235235
}
236236

237-
func byMinMax(a *AttributeExpr, r *Random) interface{} {
237+
func byMinMax(a *AttributeExpr, r *ExampleGenerator) interface{} {
238238
if !hasMinMaxValidation(a) {
239239
return nil
240240
}

0 commit comments

Comments
 (0)