Repository: goadesign/goa
Pull request #3191 (2022-11-08 13:24:48)

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.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7645ebeb..e6cae97b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,7 +34,7 @@ jobs:
         GOA_BRANCH: ${{ steps.extract_branch.outputs.branch }}
 
     - name: Compute code coverage
-      run: go test -v -json -coverprofile=coverage.out ./...> ./test-report.json || true 
+      run: go test -v -json -coverprofile=coverage.out ./... > ./test-report.json || true 
 
     - name: Analyze Test and/or Coverage Results
       uses: runforesight/foresight-test-kit-action@v1
@@ -80,7 +80,7 @@ jobs:
         GOA_BRANCH: ${{ steps.extract_branch.outputs.branch }}
 
     - name: Compute code coverage
-      run: go test -v -json -coverprofile=coverage.out ./...> ./test-report.json || true 
+      run: go test -v -json -coverprofile=coverage ./... > test-report.json
 
     - name: Analyze Test and/or Coverage Results
       uses: runforesight/foresight-test-kit-action@v1
@@ -88,6 +88,6 @@ jobs:
       with:
         test_format: JSON
         test_framework: GOLANG
-        test_path: "./test-report.json"
+        test_path: test-report.json
         coverage_format: GOLANG
-        coverage_path: ./coverage.out
\ No newline at end of file
+        coverage_path: .\coverage
\ No newline at end of file
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 00000000..8b55e466
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,63 @@
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "v3" ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ "v3" ]
+  schedule:
+    - cron: '23 12 * * 4'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'go' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        
+        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+        
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+    #   If the Autobuild fails above, remove it and uncomment the following three lines. 
+    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+    # - run: |
+    #   echo "Run, Build Application using script"
+    #   ./location_of_script_within_repo/buildscript.sh
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
+      with:
+        category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 00000000..fe461b42
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,20 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+  contents: read
+
+jobs:
+  dependency-review:
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Checkout Repository'
+        uses: actions/checkout@v3
+      - name: 'Dependency Review'
+        uses: actions/dependency-review-action@v2
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index 2dbd3257..3e38b166 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -912,7 +912,7 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData {
 			payloadDesc = fmt.Sprintf("%s is the payload type of the %s service %s method.",
 				payloadName, m.Service.Name, m.Name)
 		}
-		payloadEx = m.Payload.Example(expr.Root.API.Random())
+		payloadEx = m.Payload.Example(expr.Root.API.ExampleGenerator)
 	}
 	if m.Result.Type != expr.Empty {
 		rname = scope.GoTypeName(m.Result)
@@ -926,7 +926,7 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData {
 			resultDesc = fmt.Sprintf("%s is the result type of the %s service %s method.",
 				rname, m.Service.Name, m.Name)
 		}
-		resultEx = m.Result.Example(expr.Root.API.Random())
+		resultEx = m.Result.Example(expr.Root.API.ExampleGenerator)
 	}
 	if len(m.Errors) > 0 {
 		errors = make([]*ErrorInitData, len(m.Errors))
@@ -1002,7 +1002,7 @@ func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRe
 			spayloadDesc = fmt.Sprintf("%s is the streaming payload type of the %s service %s method.",
 				spayloadName, m.Service.Name, m.Name)
 		}
-		spayloadEx = m.StreamingPayload.Example(expr.Root.API.Random())
+		spayloadEx = m.StreamingPayload.Example(expr.Root.API.ExampleGenerator)
 	}
 	svrStream := &StreamData{
 		Interface:      vname + "ServerStream",
diff --git a/dsl/api.go b/dsl/api.go
index 72a7149a..536d511d 100644
--- a/dsl/api.go
+++ b/dsl/api.go
@@ -150,6 +150,38 @@ func License(fn func()) {
 	eval.IncompatibleDSL()
 }
 
+// Randomizer sets the API example randomizer.
+//
+// Randomizer must appear in a API expression.
+//
+// Randomizer takes a single argument which is an implementation of
+// expr.Randomizer.
+//
+// The default randomizer uses the API name as the seed, to get consistent
+// random examples.
+//
+// Example:
+//
+//    var _ = API("divider", func() {
+//        Randomizer(expr.NewFakerRandomizer("different seed"))
+//    })
+//
+// There's also a deterministic randomizer which will only generate one example
+// for each type, so all strings are "abc123", all ints are 1, etc.
+//
+// Example:
+//
+//    var _ = API("divider", func() {
+//        Randomizer(expr.NewDeterministicRandomizer())
+//    })
+func Randomizer(randomizer expr.Randomizer) {
+	if s, ok := eval.Current().(*expr.APIExpr); ok {
+		s.ExampleGenerator = &expr.ExampleGenerator{Randomizer: randomizer}
+		return
+	}
+	eval.IncompatibleDSL()
+}
+
 // Docs provides external documentation URLs. It is used by the generated
 // OpenAPI specification.
 //
diff --git a/expr/api.go b/expr/api.go
index 224e0ce6..32751abb 100644
--- a/expr/api.go
+++ b/expr/api.go
@@ -42,7 +42,7 @@ type (
 		GRPC *GRPCExpr
 
 		// random generator used to build examples for the API types.
-		random *Random
+		ExampleGenerator *ExampleGenerator
 	}
 
 	// ContactExpr contains the API contact information.
@@ -75,10 +75,11 @@ type (
 // NewAPIExpr initializes an API expression.
 func NewAPIExpr(name string, dsl func()) *APIExpr {
 	return &APIExpr{
-		Name:    name,
-		HTTP:    new(HTTPExpr),
-		GRPC:    new(GRPCExpr),
-		DSLFunc: dsl,
+		Name:             name,
+		HTTP:             new(HTTPExpr),
+		GRPC:             new(GRPCExpr),
+		DSLFunc:          dsl,
+		ExampleGenerator: NewRandom(name),
 	}
 }
 
@@ -102,15 +103,6 @@ func (a *APIExpr) Schemes() []string {
 	return ss
 }
 
-// Random returns the random generator associated with a. APIs with identical
-// names return generators that return the same sequence of pseudo random values.
-func (a *APIExpr) Random() *Random {
-	if a.random == nil {
-		a.random = NewRandom(a.Name)
-	}
-	return a.random
-}
-
 // DefaultServer returns a server expression that describes a server which
 // exposes all the services in the design and listens on localhost port 80 for
 // HTTP requests and port 8080 for gRPC requests.
diff --git a/expr/example.go b/expr/example.go
index 57958a5c..95a5849e 100644
--- a/expr/example.go
+++ b/expr/example.go
@@ -17,7 +17,7 @@ const (
 // Example returns the example set on the attribute at design time. If there
 // isn't such a value then Example computes a random value for the attribute
 // using the given random value producer.
-func (a *AttributeExpr) Example(r *Random) interface{} {
+func (a *AttributeExpr) Example(r *ExampleGenerator) interface{} {
 	if ex := a.ExtractUserExamples(); len(ex) > 0 {
 		// Return the last item in the slice so that examples can be overridden
 		// in the DSL. Overridden examples are always appended to the UserExamples
@@ -80,7 +80,7 @@ func (a *AttributeExpr) Example(r *Random) interface{} {
 
 // NewLength returns an int that validates the generator attribute length
 // validations if any.
-func NewLength(a *AttributeExpr, r *Random) int {
+func NewLength(a *AttributeExpr, r *ExampleGenerator) int {
 	if hasLengthValidation(a) {
 		minlength, maxlength := math.Inf(1), math.Inf(-1)
 		if a.Validation.MinLength != nil {
@@ -110,7 +110,7 @@ func NewLength(a *AttributeExpr, r *Random) int {
 		}
 		return count
 	}
-	return r.Int()%3 + 2
+	return r.ArrayLength()
 }
 
 func hasLengthValidation(a *AttributeExpr) bool {
@@ -143,13 +143,13 @@ func hasMinMaxValidation(a *AttributeExpr) bool {
 }
 
 // byLength generates a random size array of examples based on what's given.
-func byLength(a *AttributeExpr, r *Random) interface{} {
+func byLength(a *AttributeExpr, r *ExampleGenerator) interface{} {
 	count := NewLength(a, r)
 	switch a.Type.Kind() {
 	case StringKind:
-		return r.faker.Characters(count)
+		return r.Characters(count)
 	case BytesKind:
-		return []byte(r.faker.Characters(count))
+		return []byte(r.Characters(count))
 	case MapKind:
 		raw := make(map[interface{}]interface{})
 		m := a.Type.(*Map)
@@ -170,7 +170,7 @@ func byLength(a *AttributeExpr, r *Random) interface{} {
 }
 
 // byEnum returns a random selected enum value.
-func byEnum(a *AttributeExpr, r *Random) interface{} {
+func byEnum(a *AttributeExpr, r *ExampleGenerator) interface{} {
 	if !hasEnumValidation(a) {
 		return nil
 	}
@@ -181,20 +181,20 @@ func byEnum(a *AttributeExpr, r *Random) interface{} {
 }
 
 // byFormat returns a random example based on the format the user asks.
-func byFormat(a *AttributeExpr, r *Random) interface{} {
+func byFormat(a *AttributeExpr, r *ExampleGenerator) interface{} {
 	if !hasFormatValidation(a) {
 		return nil
 	}
 	format := a.Validation.Format
 	if res, ok := map[ValidationFormat]interface{}{
-		FormatEmail:    r.faker.Email(),
-		FormatHostname: r.faker.DomainName() + "." + r.faker.DomainSuffix(),
+		FormatEmail:    r.Email(),
+		FormatHostname: r.Hostname(),
 		FormatDate:     time.Unix(int64(r.Int())%1454957045, 0).UTC().Format("2006-01-02"), // to obtain a "fixed" rand
 		FormatDateTime: time.Unix(int64(r.Int())%1454957045, 0).UTC().Format(time.RFC3339), // to obtain a "fixed" rand
-		FormatIPv4:     r.faker.IPv4Address().String(),
-		FormatIPv6:     r.faker.IPv6Address().String(),
-		FormatIP:       r.faker.IPv4Address().String(),
-		FormatURI:      r.faker.URL(),
+		FormatIPv4:     r.IPv4Address().String(),
+		FormatIPv6:     r.IPv6Address().String(),
+		FormatIP:       r.IPv4Address().String(),
+		FormatURI:      r.URL(),
 		FormatMAC: func() string {
 			res, err := regen.Generate(`([0-9A-F]{2}-){5}[0-9A-F]{2}`)
 			if err != nil {
@@ -203,7 +203,7 @@ func byFormat(a *AttributeExpr, r *Random) interface{} {
 			return res
 		}(),
 		FormatCIDR:    "192.168.100.14/24",
-		FormatRegexp:  r.faker.Characters(3) + ".*",
+		FormatRegexp:  r.Characters(3) + ".*",
 		FormatRFC1123: time.Unix(int64(r.Int())%1454957045, 0).UTC().Format(time.RFC1123), // to obtain a "fixed" rand
 		FormatUUID: func() string {
 			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{} {
 // byPattern generates a random value that satisfies the pattern.
 //
 // Note: if multiple patterns are given, only one of them is used.
-func byPattern(a *AttributeExpr, r *Random) interface{} {
+func byPattern(a *AttributeExpr, r *ExampleGenerator) interface{} {
 	if !hasPatternValidation(a) {
 		return false
 	}
 	pattern := a.Validation.Pattern
 	gen, err := regen.NewGenerator(pattern, &regen.GeneratorArgs{MaxUnboundedRepeatCount: 6})
 	if err != nil {
-		return r.faker.Name()
+		return r.Name()
 	}
 	return gen.Generate()
 }
 
-func byMinMax(a *AttributeExpr, r *Random) interface{} {
+func byMinMax(a *AttributeExpr, r *ExampleGenerator) interface{} {
 	if !hasMinMaxValidation(a) {
 		return nil
 	}
diff --git a/expr/random.go b/expr/random.go
index f58680ba..372975b2 100644
--- a/expr/random.go
+++ b/expr/random.go
@@ -4,25 +4,89 @@ import (
 	"crypto/md5"
 	"encoding/binary"
 	"math/rand"
+	"net"
+	"strings"
 
 	"github.com/manveru/faker"
 )
 
-// Random generates consistent random values of different types given a seed.
-// The random values are consistent in that given the same seed the same random values get
-// generated.
-// The generator tracks the user types that it has processed to avoid infinite recursions, this
-// means a new generator should be created when wanting to generate a new random value for a user
-// type.
-type Random struct {
-	Seed  string
-	Seen  map[string]*interface{}
-	faker *faker.Faker
-	rand  *rand.Rand
+// Randomizer generates consistent random values of different types given a seed.
+//
+// The random values should be consistent in that given the same seed the same
+// random values get generated.
+type Randomizer interface {
+	// ArrayLength decides how long an example array will be
+	ArrayLength() int
+	// Int generates an integer example
+	Int() int
+	// Int32 generates an int32 example
+	Int32() int32
+	// Int64 generates an int64 example
+	Int64() int64
+	// String generates a string example
+	String() string
+	// Bool generates a bool example
+	Bool() bool
+	// Float32 generates a float32 example
+	Float32() float32
+	// Float64 generates a float64 example
+	Float64() float64
+	// UInt generates a uint example
+	UInt() uint
+	// UInt32 generates a uint example
+	UInt32() uint32
+	// UInt64 generates a uint example
+	UInt64() uint64
+	// Name generates a human name example
+	Name() string
+	// Email generates an example email address
+	Email() string
+	// Hostname generates an example hostname
+	Hostname() string
+	// IPv4Address generates an example IPv4 address
+	IPv4Address() net.IP
+	// IPv6Address generates an example IPv6 address
+	IPv6Address() net.IP
+	// URL generates an example URL
+	URL() string
+	// Characters generates a n-character string example
+	Characters(n int) string
+}
+
+// NewRandom returns a random value generator seeded from the given string
+// value, using the faker library to generate random but realistic values.
+func NewRandom(seed string) *ExampleGenerator {
+	return &ExampleGenerator{
+		Randomizer: NewFakerRandomizer(seed),
+	}
+}
+
+type ExampleGenerator struct {
+	Randomizer
+	seen map[string]*interface{}
+}
+
+// PreviouslySeen returns the previously seen value for a given ID
+func (r *ExampleGenerator) PreviouslySeen(typeID string) (*interface{}, bool) {
+	if r.seen == nil {
+		return nil, false
+	}
+	val, haveSeen := r.seen[typeID]
+	return val, haveSeen
 }
 
-// NewRandom returns a random value generator seeded from the given string value.
-func NewRandom(seed string) *Random {
+// HaveSeen stores the seen value in the randomizer, for reuse later
+func (r *ExampleGenerator) HaveSeen(typeID string, val *interface{}) {
+	if r.seen == nil {
+		r.seen = make(map[string]*interface{})
+	}
+
+	r.seen[typeID] = val
+}
+
+// NewFakerRandomizer creates a randomizer that uses the faker library to
+// generate fake but reasonable values.
+func NewFakerRandomizer(seed string) Randomizer {
 	hasher := md5.New()
 	hasher.Write([]byte(seed))
 	sint := int64(binary.BigEndian.Uint64(hasher.Sum(nil)))
@@ -33,60 +97,101 @@ func NewRandom(seed string) *Random {
 		Dict:     faker.Dict["en"],
 		Rand:     ran,
 	}
-	return &Random{
+
+	return &FakerRandomizer{
 		Seed:  seed,
 		faker: faker,
 		rand:  ran,
 	}
 }
 
-// Int produces a random integer.
-func (r *Random) Int() int {
-	return r.rand.Int()
+// FakerRandomizer implements the Random interface, using the Faker library.
+type FakerRandomizer struct {
+	Seed  string
+	faker *faker.Faker
+	rand  *rand.Rand
 }
 
-// Int32 produces a random 32-bit integer.
-func (r *Random) Int32() int32 {
+func (r *FakerRandomizer) ArrayLength() int {
+	return r.Int()%3 + 2
+}
+func (r *FakerRandomizer) Int() int {
+	return r.rand.Int()
+}
+func (r *FakerRandomizer) Int32() int32 {
 	return r.rand.Int31()
 }
-
-// Int64 produces a random 64-bit integer.
-func (r *Random) Int64() int64 {
+func (r *FakerRandomizer) Int64() int64 {
 	return r.rand.Int63()
 }
-
-// String produces a random string.
-func (r *Random) String() string {
+func (r *FakerRandomizer) String() string {
 	return r.faker.Sentence(2, false)
-
 }
-
-// Bool produces a random boolean.
-func (r *Random) Bool() bool {
+func (r *FakerRandomizer) Bool() bool {
 	return r.rand.Int()%2 == 0
 }
-
-// Float32 produces a random float32 value.
-func (r *Random) Float32() float32 {
+func (r *FakerRandomizer) Float32() float32 {
 	return r.rand.Float32()
 }
-
-// Float64 produces a random float64 value.
-func (r *Random) Float64() float64 {
+func (r *FakerRandomizer) Float64() float64 {
 	return r.rand.Float64()
 }
-
-// UInt produces a random uint value.
-func (r *Random) UInt() uint {
+func (r *FakerRandomizer) UInt() uint {
 	return uint(r.UInt64())
 }
-
-// UInt32 produces a random uint32 value.
-func (r *Random) UInt32() uint32 {
+func (r *FakerRandomizer) UInt32() uint32 {
 	return r.rand.Uint32()
 }
-
-// UInt64 produces a random uint64 value.
-func (r *Random) UInt64() uint64 {
+func (r *FakerRandomizer) UInt64() uint64 {
 	return r.rand.Uint64()
 }
+func (r *FakerRandomizer) Email() string {
+	return r.faker.Email()
+}
+func (r *FakerRandomizer) Hostname() string {
+	return r.faker.DomainName() + "." + r.faker.DomainSuffix()
+}
+func (r *FakerRandomizer) IPv4Address() net.IP {
+	return r.faker.IPv4Address()
+}
+func (r *FakerRandomizer) IPv6Address() net.IP {
+	return r.faker.IPv6Address()
+}
+func (r *FakerRandomizer) URL() string {
+	return r.faker.URL()
+}
+func (r *FakerRandomizer) Characters(n int) string {
+	return r.faker.Characters(n)
+}
+func (r *FakerRandomizer) Name() string {
+	return r.faker.Name()
+}
+
+// NewDeterministicRandomizer builds a Randomizer that will return hard-coded
+// values, removing all randomness from example generation.
+func NewDeterministicRandomizer() Randomizer {
+	return &DeterministicRandomizer{}
+}
+
+// DeterministicRandomizer returns hard-coded values, removing all randomness
+// from example generation
+type DeterministicRandomizer struct{}
+
+func (DeterministicRandomizer) ArrayLength() int        { return 1 }
+func (DeterministicRandomizer) Int() int                { return 1 }
+func (DeterministicRandomizer) Int32() int32            { return 1 }
+func (DeterministicRandomizer) Int64() int64            { return 1 }
+func (DeterministicRandomizer) String() string          { return "abc123" }
+func (DeterministicRandomizer) Bool() bool              { return false }
+func (DeterministicRandomizer) Float32() float32        { return 1 }
+func (DeterministicRandomizer) Float64() float64        { return 1 }
+func (DeterministicRandomizer) UInt() uint              { return 1 }
+func (DeterministicRandomizer) UInt32() uint32          { return 1 }
+func (DeterministicRandomizer) UInt64() uint64          { return 1 }
+func (DeterministicRandomizer) Name() string            { return "Alice" }
+func (DeterministicRandomizer) Email() string           { return "alice@example.com" }
+func (DeterministicRandomizer) Hostname() string        { return "example.com" }
+func (DeterministicRandomizer) IPv4Address() net.IP     { return net.IPv4zero }
+func (DeterministicRandomizer) IPv6Address() net.IP     { return net.IPv6zero }
+func (DeterministicRandomizer) URL() string             { return "https://example.com/foo" }
+func (DeterministicRandomizer) Characters(n int) string { return strings.Repeat("a", n) }
diff --git a/expr/types.go b/expr/types.go
index 40f4bc73..47dadbcf 100644
--- a/expr/types.go
+++ b/expr/types.go
@@ -21,7 +21,7 @@ type (
 		// type.
 		IsCompatible(interface{}) bool
 		// Example generates a pseudo-random value using the given random generator.
-		Example(*Random) interface{}
+		Example(*ExampleGenerator) interface{}
 		// Hash returns a unique hash value for the instance of the type.
 		Hash() string
 	}
@@ -346,7 +346,7 @@ func (p Primitive) IsCompatible(val interface{}) bool {
 
 // Example generates a pseudo-random primitive value using the given random
 // generator.
-func (p Primitive) Example(r *Random) interface{} {
+func (p Primitive) Example(r *ExampleGenerator) interface{} {
 	switch p {
 	case Boolean:
 		return r.Bool()
@@ -411,8 +411,8 @@ func (a *Array) IsCompatible(val interface{}) bool {
 
 // Example generates a pseudo-random array value using the given random
 // generator.
-func (a *Array) Example(r *Random) interface{} {
-	count := r.Int()%3 + 2
+func (a *Array) Example(r *ExampleGenerator) interface{} {
+	count := NewLength(a.ElemType, r)
 	res := make([]interface{}, count)
 	for i := 0; i < count; i++ {
 		res[i] = a.ElemType.Example(r)
@@ -530,7 +530,7 @@ func (o *Object) IsCompatible(val interface{}) bool {
 }
 
 // Example returns a random value of the object.
-func (o *Object) Example(r *Random) interface{} {
+func (o *Object) Example(r *ExampleGenerator) interface{} {
 	res := make(map[string]interface{})
 	for _, nat := range *o {
 		if v := nat.Attribute.Example(r); v != nil {
@@ -569,7 +569,7 @@ func (m *Map) IsCompatible(val interface{}) bool {
 }
 
 // Example returns a random example value.
-func (m *Map) Example(r *Random) interface{} {
+func (m *Map) Example(r *ExampleGenerator) interface{} {
 	if IsObject(m.KeyType.Type) || IsArray(m.KeyType.Type) || IsMap(m.KeyType.Type) {
 		// not much we can do for non hashable Go types
 		return nil
@@ -635,7 +635,7 @@ func (u *Union) IsCompatible(val interface{}) bool {
 }
 
 // Example returns a random example value.
-func (u *Union) Example(r *Random) interface{} {
+func (u *Union) Example(r *ExampleGenerator) interface{} {
 	if len(u.Values) == 0 {
 		return nil
 	}
diff --git a/expr/user_type.go b/expr/user_type.go
index e7eaa9c1..c28f73d2 100644
--- a/expr/user_type.go
+++ b/expr/user_type.go
@@ -82,23 +82,20 @@ func (u *UserTypeExpr) Hash() string {
 
 // Example produces an example for the user type which is JSON serialization
 // compatible.
-func (u *UserTypeExpr) Example(r *Random) interface{} {
+func (u *UserTypeExpr) Example(r *ExampleGenerator) interface{} {
 	if ex := u.recExample(r); ex != nil {
 		return *ex
 	}
 	return nil
 }
 
-func (u *UserTypeExpr) recExample(r *Random) *interface{} {
-	if ex, ok := r.Seen[u.ID()]; ok {
+func (u *UserTypeExpr) recExample(r *ExampleGenerator) *interface{} {
+	if ex, ok := r.PreviouslySeen(u.ID()); ok {
 		return ex
 	}
-	if r.Seen == nil {
-		r.Seen = make(map[string]*interface{})
-	}
 	var ex interface{}
 	pex := &ex
-	r.Seen[u.ID()] = pex
+	r.HaveSeen(u.ID(), pex)
 	actual := u.Type.Example(r)
 	*pex = actual
 	return pex
diff --git a/go.sum b/go.sum
index bea5a768..7fe6ae74 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,4 @@
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI=
 github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI=
 github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ=
diff --git a/grpc/codegen/service_data.go b/grpc/codegen/service_data.go
index e0fbb0cc..0300d08f 100644
--- a/grpc/codegen/service_data.go
+++ b/grpc/codegen/service_data.go
@@ -545,7 +545,7 @@ func (d ServicesData) analyze(gs *expr.GRPCServiceExpr) *ServiceData {
 					Ref:      "message",
 					TypeName: protoBufGoFullTypeName(e.Request, sd.PkgName, sd.Scope),
 					TypeRef:  protoBufGoFullTypeRef(e.Request, sd.PkgName, sd.Scope),
-					Example:  e.Request.Example(expr.Root.API.Random()),
+					Example:  e.Request.Example(expr.Root.API.ExampleGenerator),
 				})
 			}
 			// pass the metadata as arguments to client CLI args
@@ -1045,7 +1045,7 @@ func buildInitData(source, target *expr.AttributeExpr, sourceVar, targetVar stri
 				Ref:      sourceVar,
 				TypeName: srcCtx.Scope.Name(source, srcCtx.Pkg(source), srcCtx.Pointer, srcCtx.UseDefault),
 				TypeRef:  srcCtx.Scope.Ref(source, srcCtx.Pkg(source)),
-				Example:  source.Example(expr.Root.API.Random()),
+				Example:  source.Example(expr.Root.API.ExampleGenerator),
 			}}
 		}
 	}
@@ -1301,7 +1301,7 @@ func extractMetadata(a *expr.MappedAttributeExpr, service *expr.AttributeExpr, s
 				expr.AsArray(mp.ElemType.Type).ElemType.Type.Kind() == expr.StringKind,
 			Validate:     codegen.ValidationCode(c, nil, ctx, required, false, varn),
 			DefaultValue: c.DefaultValue,
-			Example:      c.Example(expr.Root.API.Random()),
+			Example:      c.Example(expr.Root.API.ExampleGenerator),
 		})
 		return nil
 	})
diff --git a/http/codegen/openapi/json_schema.go b/http/codegen/openapi/json_schema.go
index e632c824..99bed3f0 100644
--- a/http/codegen/openapi/json_schema.go
+++ b/http/codegen/openapi/json_schema.go
@@ -465,7 +465,7 @@ func buildAttributeSchema(api *expr.APIExpr, s *Schema, at *expr.AttributeExpr)
 	}
 	s.DefaultValue = ToStringMap(at.DefaultValue)
 	s.Description = at.Description
-	s.Example = at.Example(api.Random())
+	s.Example = at.Example(api.ExampleGenerator)
 	s.Extensions = ExtensionsFromExpr(at.Meta)
 	initAttributeValidation(s, at)
 
diff --git a/http/codegen/openapi/v3/builder.go b/http/codegen/openapi/v3/builder.go
index 41eac62e..2042775f 100644
--- a/http/codegen/openapi/v3/builder.go
+++ b/http/codegen/openapi/v3/builder.go
@@ -133,7 +133,7 @@ func buildPaths(h *expr.HTTPExpr, bodies map[string]map[string]*EndpointBodies,
 					// Remove any wildcards that is defined in path as a workaround to
 					// https://github.com/OAI/OpenAPI-Specification/issues/291
 					key = expr.HTTPWildcardRegex.ReplaceAllString(key, "/{$1}")
-					operation := buildOperation(key, r, sbod[e.Name()], api.Random())
+					operation := buildOperation(key, r, sbod[e.Name()], api.ExampleGenerator)
 					path, ok := paths[key]
 					if !ok {
 						path = new(PathItem)
@@ -187,7 +187,7 @@ func buildPaths(h *expr.HTTPExpr, bodies map[string]map[string]*EndpointBodies,
 }
 
 // buildOperation builds the OpenAPI Operation object for the given path.
-func buildOperation(key string, r *expr.RouteExpr, bodies *EndpointBodies, rand *expr.Random) *Operation {
+func buildOperation(key string, r *expr.RouteExpr, bodies *EndpointBodies, rand *expr.ExampleGenerator) *Operation {
 	e := r.Endpoint
 	m := e.MethodExpr
 	svc := e.Service
diff --git a/http/codegen/openapi/v3/example.go b/http/codegen/openapi/v3/example.go
index 5833470b..ce1c7adc 100644
--- a/http/codegen/openapi/v3/example.go
+++ b/http/codegen/openapi/v3/example.go
@@ -12,7 +12,7 @@ type (
 )
 
 // initExample sets the example or examples of the given object.
-func initExamples(obj exampler, attr *expr.AttributeExpr, r *expr.Random) {
+func initExamples(obj exampler, attr *expr.AttributeExpr, r *expr.ExampleGenerator) {
 	examples := attr.ExtractUserExamples()
 	switch {
 	case len(examples) > 1:
diff --git a/http/codegen/openapi/v3/parameters.go b/http/codegen/openapi/v3/parameters.go
index 8e88d3cb..e39960cd 100644
--- a/http/codegen/openapi/v3/parameters.go
+++ b/http/codegen/openapi/v3/parameters.go
@@ -10,7 +10,7 @@ import (
 
 // paramsFromPath computes the OpenAPI spec parameters for the given API,
 // service or endpoint HTTP path and query parameters.
-func paramsFromPath(params *expr.MappedAttributeExpr, path string, rand *expr.Random) []*Parameter {
+func paramsFromPath(params *expr.MappedAttributeExpr, path string, rand *expr.ExampleGenerator) []*Parameter {
 	var (
 		res       []*Parameter
 		wildcards = expr.ExtractHTTPWildcards(path)
@@ -32,7 +32,7 @@ func paramsFromPath(params *expr.MappedAttributeExpr, path string, rand *expr.Ra
 
 // paramsFromHeadersAndCookies computes the OpenAPI spec parameters for the
 // given endpoint HTTP headers and cookies.
-func paramsFromHeadersAndCookies(endpoint *expr.HTTPEndpointExpr, rand *expr.Random) []*Parameter {
+func paramsFromHeadersAndCookies(endpoint *expr.HTTPEndpointExpr, rand *expr.ExampleGenerator) []*Parameter {
 	params := []*Parameter{}
 	expr.WalkMappedAttr(endpoint.Headers, func(name, elem string, att *expr.AttributeExpr) error {
 		if strings.ToLower(elem) == "authorization" {
@@ -55,7 +55,7 @@ func paramsFromHeadersAndCookies(endpoint *expr.HTTPEndpointExpr, rand *expr.Ran
 }
 
 // paramFor converts the given attribute into a OpenAPI spec parameter.
-func paramFor(att *expr.AttributeExpr, name, in string, required bool, rand *expr.Random) *Parameter {
+func paramFor(att *expr.AttributeExpr, name, in string, required bool, rand *expr.ExampleGenerator) *Parameter {
 	param := &Parameter{
 		Name:            name,
 		In:              in,
diff --git a/http/codegen/openapi/v3/response.go b/http/codegen/openapi/v3/response.go
index 0af3aa58..d2b22672 100644
--- a/http/codegen/openapi/v3/response.go
+++ b/http/codegen/openapi/v3/response.go
@@ -8,7 +8,7 @@ import (
 	"goa.design/goa/v3/http/codegen/openapi"
 )
 
-func responseFromExpr(r *expr.HTTPResponseExpr, bodies map[int][]*openapi.Schema, rand *expr.Random) *Response {
+func responseFromExpr(r *expr.HTTPResponseExpr, bodies map[int][]*openapi.Schema, rand *expr.ExampleGenerator) *Response {
 	ct := r.ContentType
 	rt, ok := r.Body.Type.(*expr.ResultTypeExpr)
 	if ok && ct == "" {
diff --git a/http/codegen/openapi/v3/types.go b/http/codegen/openapi/v3/types.go
index fc17566c..f12e5d3b 100644
--- a/http/codegen/openapi/v3/types.go
+++ b/http/codegen/openapi/v3/types.go
@@ -34,12 +34,12 @@ type (
 		schemas map[string]*openapi.Schema
 		// type names indexed by hashes
 		hashes map[uint64]string
-		rand   *expr.Random
+		rand   *expr.ExampleGenerator
 	}
 )
 
 // newSchemafier initializes a schemafier.
-func newSchemafier(rand *expr.Random) *schemafier {
+func newSchemafier(rand *expr.ExampleGenerator) *schemafier {
 	return &schemafier{
 		schemas: make(map[string]*openapi.Schema),
 		hashes:  make(map[uint64]string),
@@ -61,7 +61,7 @@ func newSchemafier(rand *expr.Random) *schemafier {
 // NOTE: entries are nil when the corresponding type is Empty.
 func buildBodyTypes(api *expr.APIExpr) (map[string]map[string]*EndpointBodies, map[string]*openapi.Schema) {
 	bodies := make(map[string]map[string]*EndpointBodies)
-	sf := newSchemafier(api.Random())
+	sf := newSchemafier(api.ExampleGenerator)
 	for _, s := range api.HTTP.Services {
 		if !mustGenerate(s.Meta) || !mustGenerate(s.ServiceExpr.Meta) {
 			continue
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 5b3710c4..083892be 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -678,7 +678,7 @@ func (d ServicesData) analyze(hs *expr.HTTPServiceExpr) *ServiceData {
 								Type:        att.Type,
 								Pointer:     pointer,
 								Required:    true,
-								Example:     att.Example(expr.Root.API.Random()),
+								Example:     att.Example(expr.Root.API.ExampleGenerator),
 								Validate:    vcode,
 							},
 						}
@@ -1060,7 +1060,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							TypeRef:      sd.Scope.GoTypeRef(pAtt),
 							Validate:     codegen.ValidationCode(pAtt, nil, httpsvrctx, required, expr.IsAlias(pAtt.Type), varn),
 							DefaultValue: pAtt.DefaultValue,
-							Example:      pAtt.Example(expr.Root.API.Random()),
+							Example:      pAtt.Example(expr.Root.API.ExampleGenerator),
 						},
 					},
 				}
@@ -1170,7 +1170,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 					TypeRef:  sd.Scope.GoTypeRef(e.Body),
 					Type:     body,
 					Required: true,
-					Example:  e.Body.Example(expr.Root.API.Random()),
+					Example:  e.Body.Example(expr.Root.API.ExampleGenerator),
 					Validate: svcode,
 				},
 			}}
@@ -1182,7 +1182,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 					TypeRef:  sd.Scope.GoTypeRefWithDefaults(e.Body),
 					Type:     body,
 					Required: true,
-					Example:  e.Body.Example(expr.Root.API.Random()),
+					Example:  e.Body.Example(expr.Root.API.ExampleGenerator),
 					Validate: cvcode,
 				},
 			}}
@@ -1293,7 +1293,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							Type:         uatt.Type,
 							Pointer:      sc.UsernamePointer,
 							Validate:     codegen.ValidationCode(uatt, nil, httpsvrctx, sc.UsernameRequired, expr.IsAlias(uatt.Type), sc.UsernameAttr),
-							Example:      uatt.Example(expr.Root.API.Random()),
+							Example:      uatt.Example(expr.Root.API.ExampleGenerator),
 						},
 					}
 					patt := e.MethodExpr.Payload.Find(sc.PasswordAttr)
@@ -1315,7 +1315,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							Type:         patt.Type,
 							Pointer:      sc.PasswordPointer,
 							Validate:     codegen.ValidationCode(patt, nil, httpsvrctx, sc.PasswordRequired, expr.IsAlias(patt.Type), sc.PasswordAttr),
-							Example:      patt.Example(expr.Root.API.Random()),
+							Example:      patt.Example(expr.Root.API.ExampleGenerator),
 						},
 					}
 					cliArgs = []*InitArgData{uarg, parg}
@@ -2074,7 +2074,7 @@ func buildRequestBodyType(body, att *expr.AttributeExpr, e *expr.HTTPEndpointExp
 					TypeRef:  svc.Scope.GoFullTypeRef(att, pkg),
 					Type:     att.Type,
 					Validate: validateDef,
-					Example:  att.Example(expr.Root.API.Random()),
+					Example:  att.Example(expr.Root.API.ExampleGenerator),
 				},
 			}
 			init = &InitData{
@@ -2096,7 +2096,7 @@ func buildRequestBodyType(body, att *expr.AttributeExpr, e *expr.HTTPEndpointExp
 		Init:        init,
 		ValidateDef: validateDef,
 		ValidateRef: validateRef,
-		Example:     body.Example(expr.Root.API.Random()),
+		Example:     body.Example(expr.Root.API.ExampleGenerator),
 	}
 }
 
@@ -2266,7 +2266,7 @@ func buildResponseBodyType(body, att *expr.AttributeExpr, loc *codegen.Location,
 					TypeRef:  tref,
 					Type:     att.Type,
 					Validate: validateDef,
-					Example:  att.Example(expr.Root.API.Random()),
+					Example:  att.Example(expr.Root.API.ExampleGenerator),
 				},
 			}
 			init = &InitData{
@@ -2288,7 +2288,7 @@ func buildResponseBodyType(body, att *expr.AttributeExpr, loc *codegen.Location,
 		Init:        init,
 		ValidateDef: validateDef,
 		ValidateRef: validateRef,
-		Example:     body.Example(expr.Root.API.Random()),
+		Example:     body.Example(expr.Root.API.ExampleGenerator),
 		View:        viewName,
 	}
 }
@@ -2339,7 +2339,7 @@ func extractPathParams(a *expr.MappedAttributeExpr, service *expr.AttributeExpr,
 					Pointer:      false,
 					Validate:     codegen.ValidationCode(c, nil, ctx, true, expr.IsAlias(c.Type), varn),
 					DefaultValue: c.DefaultValue,
-					Example:      c.Example(expr.Root.API.Random()),
+					Example:      c.Example(expr.Root.API.ExampleGenerator),
 				},
 			},
 		})
@@ -2404,7 +2404,7 @@ func extractQueryParams(a *expr.MappedAttributeExpr, service *expr.AttributeExpr
 					Pointer:      pointer,
 					Validate:     codegen.ValidationCode(c, nil, ctx, required, expr.IsAlias(c.Type), varn),
 					DefaultValue: c.DefaultValue,
-					Example:      c.Example(expr.Root.API.Random()),
+					Example:      c.Example(expr.Root.API.ExampleGenerator),
 				},
 			},
 		})
@@ -2471,7 +2471,7 @@ func extractHeaders(a *expr.MappedAttributeExpr, svcAtt *expr.AttributeExpr, svc
 					Type:         hattr.Type,
 					Validate:     codegen.ValidationCode(hattr, nil, svcCtx, required, expr.IsAlias(hattr.Type), varn),
 					DefaultValue: hattr.DefaultValue,
-					Example:      hattr.Example(expr.Root.API.Random()),
+					Example:      hattr.Example(expr.Root.API.ExampleGenerator),
 				},
 			},
 		})
@@ -2527,7 +2527,7 @@ func extractCookies(a *expr.MappedAttributeExpr, svcAtt *expr.AttributeExpr, svc
 					Type:         hattr.Type,
 					Validate:     codegen.ValidationCode(hattr, nil, svcCtx, required, expr.IsAlias(hattr.Type), varn),
 					DefaultValue: hattr.DefaultValue,
-					Example:      hattr.Example(expr.Root.API.Random()),
+					Example:      hattr.Example(expr.Root.API.ExampleGenerator),
 				},
 			},
 		}
@@ -2629,7 +2629,7 @@ func attributeTypeData(ut expr.UserType, req, ptr, server bool, rd *ServiceData)
 		Ref:         rd.Scope.GoTypeRef(att),
 		ValidateDef: validate,
 		ValidateRef: validateRef,
-		Example:     att.Example(expr.Root.API.Random()),
+		Example:     att.Example(expr.Root.API.ExampleGenerator),
 	}
 }
 
diff --git a/http/codegen/websocket.go b/http/codegen/websocket.go
index f3270344..1b0b7504 100644
--- a/http/codegen/websocket.go
+++ b/http/codegen/websocket.go
@@ -139,7 +139,7 @@ func initWebSocketData(ed *EndpointData, e *expr.HTTPEndpointExpr, sd *ServiceDa
 								TypeRef:  sd.Scope.GoTypeRef(e.StreamingBody),
 								Type:     e.StreamingBody.Type,
 								Required: true,
-								Example:  e.Body.Example(expr.Root.API.Random()),
+								Example:  e.Body.Example(expr.Root.API.ExampleGenerator),
 								Validate: svcode,
 							},
 						}}


Test patch:
diff --git a/http/codegen/openapi/v2/builder_test.go b/http/codegen/openapi/v2/builder_test.go
index 5e3635f6..1bbec104 100644
--- a/http/codegen/openapi/v2/builder_test.go
+++ b/http/codegen/openapi/v2/builder_test.go
@@ -30,7 +30,9 @@ func TestBuildPathFromFileServer(t *testing.T) {
 				Paths: make(map[string]interface{}),
 			}
 			root := &expr.RootExpr{
-				API: &expr.APIExpr{},
+				API: &expr.APIExpr{
+					ExampleGenerator: expr.NewRandom("test"),
+				},
 			}
 			fs := &expr.HTTPFileServerExpr{
 				Service: &expr.HTTPServiceExpr{