Repository: goadesign/goa
Pull request #2292 (2019-09-25 23:29:05)

Remove empty validate methods in gRPC
`field` is internal to goa and should never be validated.

raphael: Great, thank you! Is there no code being generated that assume that field is not nil?
nitinmohan87: @raphael Do you mean in the examples?
raphael: I guess wherever we generate code that accesses the field. Before the validation would have returned a response right away but now it won't. So for example the code that transforms the transport struct back into a service level struct.

Patch:
diff --git a/Makefile b/Makefile
index f95855e4..a7064f84 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 #! /usr/bin/make
 #
-# Makefile for goa v2
+# Makefile for goa v3
 #
 # Targets:
 # - "depend" retrieves the Go packages needed to run the linter and tests
diff --git a/cmd/goa/gen.go b/cmd/goa/gen.go
index 2e2eef45..6237c45c 100644
--- a/cmd/goa/gen.go
+++ b/cmd/goa/gen.go
@@ -1,11 +1,9 @@
 package main
 
 import (
-	"bufio"
 	"fmt"
 	"go/parser"
 	"go/token"
-	"io"
 	"io/ioutil"
 	"os"
 	"os/exec"
@@ -143,63 +141,8 @@ func (g *Generator) Write(debug bool) error {
 	return err
 }
 
-const goModEnvKey = "GOMOD"
-
-func findGoMod() string {
-	env := os.Getenv(goModEnvKey)
-	if _, err := exec.LookPath("go"); err != nil {
-		return env
-	}
-	mod, err := exec.Command("go", "env", goModEnvKey).Output()
-	if err != nil {
-		return env
-	}
-	return strings.TrimSpace(string(mod))
-}
-
-func (g *Generator) goaPackage() (string, error) {
-	goaPkg := "goa.design/goa"
-	if g.DesignVersion < 3 {
-		return goaPkg, nil
-	}
-	goaPkg = fmt.Sprintf("goa.design/goa/v%d", g.DesignVersion)
-	path := findGoMod()
-	if _, err := os.Stat(path); err != nil {
-		return goaPkg, nil
-	}
-	fp, err := os.Open(path)
-	if err != nil {
-		return "", err
-	}
-	defer fp.Close()
-	return parseGoModGoaPackage(goaPkg, fp)
-}
-
-var reMod = regexp.MustCompile(`^\s*(?:require )?\s*(goa\.design/goa/v\d+?)\s+([^\/]\S+?)\s*(?:\/\/.+)?$`)
-
-func parseGoModGoaPackage(pkg string, r io.Reader) (string, error) {
-	s := bufio.NewScanner(r)
-	for s.Scan() {
-		match := reMod.FindStringSubmatch(s.Text())
-		if len(match) == 3 && match[1] == pkg {
-			return match[1] + "@" + match[2], nil
-		}
-	}
-	if err := s.Err(); err != nil {
-		return "", fmt.Errorf("scan error, %v", err)
-	}
-	return pkg, nil
-}
-
 // Compile compiles the generator.
 func (g *Generator) Compile() error {
-	goaPkg, err := g.goaPackage()
-	if err != nil {
-		return err
-	}
-	if err := g.runGoCmd("get", goaPkg); err != nil {
-		return err
-	}
 	return g.runGoCmd("build", "-o", g.bin)
 }
 
diff --git a/dsl/description.go b/dsl/description.go
index 1587e9e0..94acdbba 100644
--- a/dsl/description.go
+++ b/dsl/description.go
@@ -8,7 +8,7 @@ import (
 // Description sets the expression description.
 //
 // Description may appear in API, Docs, Type or Attribute.
-// Description may also appear in Response and FileServer.
+// Description may also appear in Response and Files.
 //
 // Description accepts one arguments: the description string.
 //
diff --git a/dsl/http.go b/dsl/http.go
index d51fe16a..2611b262 100644
--- a/dsl/http.go
+++ b/dsl/http.go
@@ -699,6 +699,10 @@ func Body(args ...interface{}) {
 // Parent sets the name of the parent service. The parent service canonical
 // method path is used as prefix for all the service HTTP endpoint paths.
 //
+// Attributes of the parent method payload that map to parent path parameters
+// are automatically merged into the child method payload type if not already
+// defined.
+//
 // Parent must appear in a Service expression.
 //
 // Parent accepts one argument: the name of the parent service.
diff --git a/dsl/meta.go b/dsl/meta.go
index b7bf68ce..07c41ae7 100644
--- a/dsl/meta.go
+++ b/dsl/meta.go
@@ -126,6 +126,7 @@ import (
 //            Meta("swagger:tag:Backend:desc", "Description of Backend")
 //            Meta("swagger:tag:Backend:url", "http://example.com")
 //            Meta("swagger:tag:Backend:url:desc", "See more docs here")
+//            Meta("swagger:tag:Backend:extension:x-data", `{"foo":"bar"}`)
 //        })
 //    })
 //
@@ -150,8 +151,6 @@ func Meta(name string, value ...string) {
 	}
 
 	switch e := eval.Current().(type) {
-	case *expr.RootExpr:
-		e.Meta = appendMeta(e.Meta, name, value...)
 	case *expr.APIExpr:
 		e.Meta = appendMeta(e.Meta, name, value...)
 	case *expr.AttributeExpr:
diff --git a/expr/example.go b/expr/example.go
index b2569349..761e4f4a 100644
--- a/expr/example.go
+++ b/expr/example.go
@@ -200,6 +200,7 @@ func byFormat(a *AttributeExpr, r *Random) interface{} {
 			}
 			return res
 		}(),
+		FormatJSON: `{"name":"example","email":"mail@example.com"}`,
 	}[format]; ok {
 		return res
 	}
diff --git a/expr/http_endpoint.go b/expr/http_endpoint.go
index 9040bfe1..d74f9b34 100644
--- a/expr/http_endpoint.go
+++ b/expr/http_endpoint.go
@@ -176,7 +176,19 @@ func (e *HTTPEndpointExpr) Prepare() {
 		if c := p.CanonicalEndpoint(); c != nil {
 			if !e.HasAbsoluteRoutes() {
 				headers.Merge(c.Headers)
-				params.Merge(c.Params)
+				params.Merge(c.PathParams())
+
+				// Inherit attributes for path params from parent service
+				WalkMappedAttr(c.PathParams(), func(name, _ string, _ *AttributeExpr) error {
+					if att := c.MethodExpr.Payload.Find(name); att != nil {
+						if o := AsObject(e.MethodExpr.Payload.Type); o != nil {
+							if o.Attribute(name) == nil {
+								o.Set(name, att)
+							}
+						}
+					}
+					return nil
+				})
 			}
 		}
 	}
diff --git a/expr/root.go b/expr/root.go
index 337fa60d..daf70800 100644
--- a/expr/root.go
+++ b/expr/root.go
@@ -32,9 +32,6 @@ type (
 		Creations []*TypeMap
 		// Schemes list the registered security schemes.
 		Schemes []*SchemeExpr
-		// Meta is a set of key/value pairs with semantic that is
-		// specific to each generator.
-		Meta MetaExpr
 	}
 
 	// MetaExpr is a set of key/value pairs
diff --git a/expr/security.go b/expr/security.go
index 3a3029d5..93c24b2b 100644
--- a/expr/security.go
+++ b/expr/security.go
@@ -17,7 +17,7 @@ const (
 	BasicAuthKind
 	// APIKeyKind means "apiKey" security scheme.
 	APIKeyKind
-	// JWTKind means an "apiKey" security scheme, with support for
+	// JWTKind means an "JWT" security scheme, with support for
 	// TokenPath and Scopes.
 	JWTKind
 	// NoKind means to have no security for this endpoint.
diff --git a/grpc/codegen/protobuf.go b/grpc/codegen/protobuf.go
index be937133..f0c8ad65 100644
--- a/grpc/codegen/protobuf.go
+++ b/grpc/codegen/protobuf.go
@@ -148,7 +148,6 @@ func wrapAttr(att *expr.AttributeExpr, tname string) {
 					},
 				},
 			},
-			Validation: &expr.ValidationExpr{Required: []string{"field"}},
 		}
 	}
 	switch dt := att.Type.(type) {
diff --git a/grpc/codegen/server.go b/grpc/codegen/server.go
index 05f9091a..bfb46766 100644
--- a/grpc/codegen/server.go
+++ b/grpc/codegen/server.go
@@ -329,12 +329,12 @@ func Decode{{ .Method.VarName }}Request(ctx context.Context, v interface{}, md m
 				if vals := md.Get({{ printf "%q" .Name }}); len(vals) == 0 {
 					err = goa.MergeErrors(err, goa.MissingFieldError({{ printf "%q" .Name }}, "metadata"))
 				} else {
-					{{ .VarName }}Raw = vals[0]
+					{{ .VarName }}Raw := vals[0]
 					{{ template "type_conversion" . }}
 				}
 			{{- else }}
 				if vals := md.Get({{ printf "%q" .Name }}); len(vals) > 0 {
-					{{ .VarName }}Raw = vals[0]
+					{{ .VarName }}Raw := vals[0]
 					{{ template "type_conversion" . }}
 				}
 			{{- end }}
diff --git a/grpc/middleware/trace.go b/grpc/middleware/trace.go
index 465bd771..8175f654 100644
--- a/grpc/middleware/trace.go
+++ b/grpc/middleware/trace.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"context"
+	"regexp"
 
 	"goa.design/goa/v3/middleware"
 	"google.golang.org/grpc"
@@ -36,7 +37,7 @@ const (
 func UnaryServerTrace(opts ...middleware.TraceOption) grpc.UnaryServerInterceptor {
 	o := middleware.NewTraceOptions(opts...)
 	return grpc.UnaryServerInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
-		ctx = withTrace(ctx, o)
+		ctx = withTrace(ctx, info.FullMethod, o)
 		return handler(ctx, req)
 	})
 }
@@ -55,7 +56,7 @@ func UnaryServerTrace(opts ...middleware.TraceOption) grpc.UnaryServerIntercepto
 func StreamServerTrace(opts ...middleware.TraceOption) grpc.StreamServerInterceptor {
 	o := middleware.NewTraceOptions(opts...)
 	return grpc.StreamServerInterceptor(func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
-		ctx := withTrace(ss.Context(), o)
+		ctx := withTrace(ss.Context(), info.FullMethod, o)
 		wss := NewWrappedServerStream(ctx, ss)
 		return handler(srv, wss)
 	})
@@ -112,8 +113,14 @@ func SampleSize(s int) middleware.TraceOption {
 	return middleware.SampleSize(s)
 }
 
+// DiscardFromTrace adds a regular expression for matching a request path to be discarded from tracing.
+// see middleware.DiscardFromTrace() for more details.
+func DiscardFromTrace(discard *regexp.Regexp) middleware.TraceOption {
+	return middleware.DiscardFromTrace(discard)
+}
+
 // withTrace sets the trace ID, span ID, and parent span ID in the context.
-func withTrace(ctx context.Context, opts *middleware.TraceOptions) context.Context {
+func withTrace(ctx context.Context, fullMethod string, opts *middleware.TraceOptions) context.Context {
 	sampler := opts.NewSampler()
 	md, ok := metadata.FromIncomingContext(ctx)
 	if !ok {
@@ -123,9 +130,18 @@ func withTrace(ctx context.Context, opts *middleware.TraceOptions) context.Conte
 	var traceID string
 	{
 		traceID = MetadataValue(md, TraceIDMetadataKey)
-		if traceID == "" && sampler.Sample() {
-			// insert tracing only within sample.
-			traceID = opts.TraceID()
+		if traceID == "" {
+			var discarded bool
+			for _, discard := range opts.Discards() {
+				if discard.MatchString(fullMethod) {
+					discarded = true
+					break
+				}
+			}
+			if !discarded && sampler.Sample() {
+				// insert tracing only within sample.
+				traceID = opts.TraceID()
+			}
 		}
 	}
 	if traceID == "" {
diff --git a/http/codegen/openapi/json_schema.go b/http/codegen/openapi/json_schema.go
index 1960e314..33f0cb7b 100644
--- a/http/codegen/openapi/json_schema.go
+++ b/http/codegen/openapi/json_schema.go
@@ -48,6 +48,9 @@ type (
 
 		// Union
 		AnyOf []*Schema `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
+
+		// Extensions defines the swagger extensions.
+		Extensions map[string]interface{} `json:"-" yaml:"-"`
 	}
 
 	// Type is the JSON type enum.
@@ -71,6 +74,9 @@ type (
 		ResultType   string  `json:"mediaType,omitempty" yaml:"mediaType,omitempty"`
 		EncType      string  `json:"encType,omitempty" yaml:"encType,omitempty"`
 	}
+
+	// These types are used in marshalJSON() to avoid recursive call of json.Marshal().
+	_Schema Schema
 )
 
 const (
@@ -451,6 +457,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.Extensions = ExtensionsFromExpr(at.Meta)
 	initAttributeValidation(s, at)
 
 	return s
@@ -580,3 +587,13 @@ func buildResultTypeSchema(api *expr.APIExpr, mt *expr.ResultTypeExpr, view stri
 	}
 	buildAttributeSchema(api, s, projected.AttributeExpr)
 }
+
+// MarshalJSON returns the JSON encoding of s.
+func (s *Schema) MarshalJSON() ([]byte, error) {
+	return marshalJSON((*_Schema)(s), s.Extensions)
+}
+
+// MarshalYAML returns value which marshaled in place of the original value
+func (s *Schema) MarshalYAML() (interface{}, error) {
+	return marshalYAML((*_Schema)(s), s.Extensions)
+}
diff --git a/http/codegen/openapi/openapi_v2_builder.go b/http/codegen/openapi/openapi_v2_builder.go
index 5850d509..e2a13b08 100644
--- a/http/codegen/openapi/openapi_v2_builder.go
+++ b/http/codegen/openapi/openapi_v2_builder.go
@@ -18,7 +18,7 @@ func NewV2(root *expr.RootExpr, h *expr.HostExpr) (*V2, error) {
 	if root == nil {
 		return nil, nil
 	}
-	tags := tagsFromExpr(root.Meta)
+	tags := tagsFromExpr(root.API.Meta)
 	u, err := url.Parse(defaultURI(h))
 	if err != nil {
 		// This should never happen because server expression must have been
@@ -48,7 +48,7 @@ func NewV2(root *expr.RootExpr, h *expr.HostExpr) (*V2, error) {
 			Contact:        root.API.Contact,
 			License:        root.API.License,
 			Version:        root.API.Version,
-			Extensions:     ExtensionsFromExpr(root.Meta),
+			Extensions:     ExtensionsFromExpr(root.API.Meta),
 		},
 		Host:                host,
 		BasePath:            basePath,
@@ -106,25 +106,34 @@ func NewV2(root *expr.RootExpr, h *expr.HostExpr) (*V2, error) {
 // ExtensionsFromExpr generates swagger extensions from the given meta
 // expression.
 func ExtensionsFromExpr(mdata expr.MetaExpr) map[string]interface{} {
+	return extensionsFromExprWithPrefix(mdata, "swagger:extension:")
+}
+
+// extensionsFromExprWithPrefix generates swagger extensions from
+// the given meta expression with keys starting the given prefix.
+func extensionsFromExprWithPrefix(mdata expr.MetaExpr, prefix string) map[string]interface{} {
+	if !strings.HasSuffix(prefix, ":") {
+		prefix += ":"
+	}
 	extensions := make(map[string]interface{})
 	for key, value := range mdata {
-		chunks := strings.Split(key, ":")
-		if len(chunks) != 3 {
+		if !strings.HasPrefix(key, prefix) {
 			continue
 		}
-		if chunks[0] != "swagger" || chunks[1] != "extension" {
+		name := key[len(prefix):]
+		if strings.Contains(name, ":") {
 			continue
 		}
-		if !strings.HasPrefix(chunks[2], "x-") {
+		if !strings.HasPrefix(name, "x-") {
 			continue
 		}
 		val := value[0]
 		ival := interface{}(val)
 		if err := json.Unmarshal([]byte(val), &ival); err != nil {
-			extensions[chunks[2]] = val
+			extensions[name] = val
 			continue
 		}
-		extensions[chunks[2]] = ival
+		extensions[name] = ival
 	}
 	if len(extensions) == 0 {
 		return nil
@@ -325,7 +334,8 @@ func tagsFromExpr(mdata expr.MetaExpr) (tags []*Tag) {
 			tag.ExternalDocs = docs
 		}
 
-		tag.Extensions = ExtensionsFromExpr(mdata)
+		extensionsPrefix := fmt.Sprintf("%s:extension:", key)
+		tag.Extensions = extensionsFromExprWithPrefix(mdata, extensionsPrefix)
 
 		tags = append(tags, tag)
 	}
@@ -489,6 +499,9 @@ func responseSpecFromExpr(s *V2, root *expr.RootExpr, r *expr.HTTPResponseExpr,
 	} else if r.Body.Type != expr.Empty {
 		schema = AttributeTypeSchemaWithPrefix(root.API, r.Body, typeNamePrefix)
 	}
+	if schema != nil {
+		schema.Extensions = ExtensionsFromExpr(r.Meta)
+	}
 	headers := headersFromExpr(r.Headers)
 	desc := r.Description
 	if desc == "" {
@@ -568,12 +581,7 @@ func buildPathFromFileServer(s *V2, root *expr.RootExpr, fs *expr.HTTPFileServer
 			Schemes:      schemes,
 		}
 
-		key := expr.HTTPWildcardRegex.ReplaceAllStringFunc(
-			path,
-			func(w string) string {
-				return fmt.Sprintf("/{%s}", w[2:])
-			},
-		)
+		key := expr.HTTPWildcardRegex.ReplaceAllString(path, "/{$1}")
 		if key == "" {
 			key = "/"
 		}
@@ -632,10 +640,19 @@ func buildPathFromExpr(s *V2, root *expr.RootExpr, h *expr.HostExpr, route *expr
 			responses[strconv.Itoa(er.Response.StatusCode)] = resp
 		}
 
+		var consumes []string
+		if endpoint.MultipartRequest {
+			consumes = []string{"multipart/form-data"}
+		}
+
 		if endpoint.Body.Type != expr.Empty {
+			in := "body"
+			if endpoint.MultipartRequest {
+				in = "formData"
+			}
 			pp := &Parameter{
 				Name:        endpoint.Body.Type.Name(),
-				In:          "body",
+				In:          in,
 				Description: endpoint.Body.Description,
 				Required:    true,
 				Schema:      AttributeTypeSchemaWithPrefix(root.API, endpoint.Body, codegen.Goify(endpoint.Service.Name(), true)),
@@ -712,11 +729,12 @@ func buildPathFromExpr(s *V2, root *expr.RootExpr, h *expr.HostExpr, route *expr
 			ExternalDocs: docsFromExpr(endpoint.MethodExpr.Docs),
 			OperationID:  operationID,
 			Parameters:   params,
+			Consumes:     consumes,
 			Produces:     produces,
 			Responses:    responses,
 			Schemes:      schemes,
 			Deprecated:   false,
-			Extensions:   ExtensionsFromExpr(route.Meta),
+			Extensions:   ExtensionsFromExpr(endpoint.MethodExpr.Meta),
 			Security:     requirements,
 		}
 
diff --git a/http/encoding.go b/http/encoding.go
index 8d05cb52..b1cbda64 100644
--- a/http/encoding.go
+++ b/http/encoding.go
@@ -76,6 +76,8 @@ func RequestDecoder(r *http.Request) Decoder {
 		return gob.NewDecoder(r.Body)
 	case "application/xml":
 		return xml.NewDecoder(r.Body)
+	case "text/html", "text/plain":
+		return newTextDecoder(r.Body, contentType)
 	default:
 		return json.NewDecoder(r.Body)
 	}
diff --git a/http/middleware/request.go b/http/middleware/request.go
index 4c9c0d7f..ec24cb79 100644
--- a/http/middleware/request.go
+++ b/http/middleware/request.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 )
 
-// PopulateRequestContext returns a middleware which populates a number of standard HTTP reqeust
+// PopulateRequestContext returns a middleware which populates a number of standard HTTP header
 // values into the request context. Those values may be extracted using the
 // corresponding ContextKey type in this package.
 func PopulateRequestContext() func(http.Handler) http.Handler {
diff --git a/http/middleware/trace.go b/http/middleware/trace.go
index ec74af1f..cddeda05 100644
--- a/http/middleware/trace.go
+++ b/http/middleware/trace.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"net/http"
+	"regexp"
 
 	"goa.design/goa/v3/middleware"
 )
@@ -38,9 +39,21 @@ func Trace(opts ...middleware.TraceOption) func(http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			// insert a new trace ID only if not already being traced.
 			traceID := r.Header.Get(TraceIDHeader)
-			if traceID == "" && sampler.Sample() {
-				// insert tracing only within sample.
-				traceID = o.TraceID()
+			if traceID == "" {
+				// check for discards only if we do not already have a trace ID and before sampling.
+				var discarded bool
+				if r.URL != nil { // docs imply but do not actually state that URL cannot be nil
+					for _, discard := range o.Discards() {
+						if discard.MatchString(r.URL.Path) {
+							discarded = true
+							break
+						}
+					}
+				}
+				if !discarded && sampler.Sample() {
+					// insert tracing only within sample.
+					traceID = o.TraceID()
+				}
 			}
 			if traceID == "" {
 				h.ServeHTTP(w, r)
@@ -80,6 +93,12 @@ func SampleSize(s int) middleware.TraceOption {
 	return middleware.SampleSize(s)
 }
 
+// DiscardFromTrace adds a regular expression for matching a request path to be discarded from tracing.
+// see middleware.DiscardFromTrace() for more details.
+func DiscardFromTrace(discard *regexp.Regexp) middleware.TraceOption {
+	return middleware.DiscardFromTrace(discard)
+}
+
 // WrapDoer wraps a goa client Doer and sets the trace headers so that the
 // downstream service may properly retrieve the parent span ID and trace ID.
 func WrapDoer(doer Doer) Doer {
diff --git a/middleware/trace.go b/middleware/trace.go
index 4c8e955f..7f279abb 100644
--- a/middleware/trace.go
+++ b/middleware/trace.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"context"
+	"regexp"
 )
 
 type (
@@ -20,6 +21,7 @@ type (
 		samplingPercent int
 		maxSamplingRate int
 		sampleSize      int
+		discards        []*regexp.Regexp
 	}
 
 	// tracedLogger is a logger which logs the trace ID with every log entry
@@ -67,6 +69,12 @@ func (o *TraceOptions) SpanID() string {
 	return o.spanIDFunc()
 }
 
+// Discards returns the list of regular expressions used to match paths to be
+// discarded from tracing.
+func (o *TraceOptions) Discards() []*regexp.Regexp {
+	return o.discards
+}
+
 // TraceIDFunc configures the function used to compute trace IDs. Use this
 // option to generate IDs compatible with backend tracing systems
 // (e.g. AWS XRay).
@@ -128,6 +136,22 @@ func SampleSize(s int) TraceOption {
 	}
 }
 
+// DiscardFromTrace adds a regular expression for matching a request path to be discarded from tracing.
+// this is useful for frequent API calls that are not important to trace, such as health checks.
+// the pattern can be a full or partial match and could even support both HTTP and gRPC paths with an
+// OR expression, etc.
+//
+// note that discards can be overridden if the incoming request already has a trace ID.
+func DiscardFromTrace(discard *regexp.Regexp) TraceOption {
+	if discard == nil {
+		panic("discard cannot be nil")
+	}
+	return func(o *TraceOptions) *TraceOptions {
+		o.discards = append(o.discards, discard)
+		return o
+	}
+}
+
 // WithSpan returns a context containing the given trace, span and parent span
 // IDs.
 func WithSpan(ctx context.Context, traceID, spanID, parentID string) context.Context {


Test patch:
diff --git a/cmd/goa/gen_test.go b/cmd/goa/gen_test.go
deleted file mode 100644
index 46f6df6a..00000000
--- a/cmd/goa/gen_test.go
+++ /dev/null
@@ -1,210 +0,0 @@
-package main
-
-import (
-	"reflect"
-	"strings"
-	"testing"
-)
-
-func TestGenerator_goaPackage(t *testing.T) {
-	cases := []struct {
-		Name     string
-		Version  int
-		Expected string
-	}{
-		{
-			Name:     "specify v2",
-			Version:  2,
-			Expected: "goa.design/goa",
-		},
-		{
-			Name:     "specify v3, but go.mod file does not exist",
-			Version:  3,
-			Expected: "goa.design/goa/v3",
-		},
-	}
-	for _, c := range cases {
-		t.Run(c.Name, func(t *testing.T) {
-			pkg, err := (&Generator{
-				DesignVersion: c.Version,
-			}).goaPackage()
-			if err != nil {
-				t.Fatalf("unexpected error, %v", err)
-			}
-			if pkg != c.Expected {
-				t.Errorf("expected %v, got %v", c.Expected, pkg)
-			}
-		})
-	}
-}
-
-func TestParseGoModGoaPackage(t *testing.T) {
-	cases := []struct {
-		Name     string
-		Mod      string
-		Package  string
-		Expected string
-	}{
-		{
-			Name:     "simple require",
-			Mod:      "require    goa.design/goa/v3   v3.0.3-0.20190704022140-85024ebc66dc",
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
-		},
-		{
-			Name: "in require block",
-			Mod: `module calc
-go 1.12
-require (
-        github.com/ikawaha/kagome v1.0.0 // indirect
-        goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc
-        goa.design/plugins/v3 v3.0.1
-)
-`,
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
-		},
-		{
-			Name: "not found",
-			Mod: `module calc
-go 1.12
-require (
-        github.com/ikawaha/kagome v1.0.0 // indirect
-        goa.design/plugins/v3 v3.0.1
-)
-`,
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3",
-		},
-		{
-			Name: "with replace",
-			Mod: `module calc
-go 1.12
-replace goa.design/goa/v3 => ../../../goa.design/goa
-require (
-        github.com/ikawaha/kagome v1.0.0 // indirect
-        goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc
-        goa.design/plugins/v3 v3.0.1
-)
-`,
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
-		},
-		{
-			Name: "with comment",
-			Mod: `module calc
-go 1.12
-replace goa.design/goa/v3 => ../../../goa.design/goa
-require (
-        github.com/ikawaha/kagome v1.0.0 // indirect
-        goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc // indirect // comment
-        goa.design/plugins/v3 v3.0.1
-)
-`,
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
-		},
-		{
-			Name:     "require with comment",
-			Mod:      " require goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc// indirect // comment",
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
-		},
-		{
-			Name: "comment out",
-			Mod: `module calc
-go 1.12
-replace goa.design/goa/v3 => ../../../goa.design/goa
-require (
-        github.com/ikawaha/kagome v1.0.0 // indirect
-        // goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc
-        goa.design/plugins/v3 v3.0.1
-)
-`,
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3",
-		},
-		{
-			Name:     "without version",
-			Mod:      "     goa.design/goa/v3//v3.0.3-0.20190704022140-85024ebc66dc",
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3",
-		},
-		{
-			Name:     "different version",
-			Mod:      "goa.design/goa/v2 v2.0.3-0.20190704022140-85024ebc66dc // comment",
-			Package:  "goa.design/goa/v3",
-			Expected: "goa.design/goa/v3",
-		},
-	}
-	for _, c := range cases {
-		t.Run(c.Name, func(t *testing.T) {
-			pkg, err := parseGoModGoaPackage("goa.design/goa/v3", strings.NewReader(c.Mod))
-			if err != nil {
-				t.Fatalf("unexpected error, %v", err)
-			}
-			if pkg != c.Expected {
-				t.Errorf("expected %v, got %v", c.Expected, pkg)
-			}
-		})
-	}
-}
-
-func TestModPatternRegexp(t *testing.T) {
-	cases := []struct {
-		Name     string
-		Input    string
-		Expected []string
-	}{
-		{
-			Name:     "goa v3 release version",
-			Input:    "goa.design/goa/v3 v3.0.2",
-			Expected: []string{"goa.design/goa/v3 v3.0.2", "goa.design/goa/v3", "v3.0.2"},
-		},
-		{
-			Name:     "goa v3 commit number",
-			Input:    "goa.design/goa/v3 02c84c9b39d845611bfe6e8a96465d698ea374b1",
-			Expected: []string{"goa.design/goa/v3 02c84c9b39d845611bfe6e8a96465d698ea374b1", "goa.design/goa/v3", "02c84c9b39d845611bfe6e8a96465d698ea374b1"},
-		},
-		{
-			Name:     "goa v3 with comment",
-			Input:    "goa.design/goa/v3   v3.0.2 //comment1 //comment2 //comment3",
-			Expected: []string{"goa.design/goa/v3   v3.0.2 //comment1 //comment2 //comment3", "goa.design/goa/v3", "v3.0.2"},
-		},
-		{
-			Name:     "goa v3 with require and comment",
-			Input:    "require goa.design/goa/v3 v3.0.2 //comment",
-			Expected: []string{"require goa.design/goa/v3 v3.0.2 //comment", "goa.design/goa/v3", "v3.0.2"},
-		},
-		{
-			Name:     "goa v2",
-			Input:    "goa.design/goa/v2 v2.0.0",
-			Expected: []string{"goa.design/goa/v2 v2.0.0", "goa.design/goa/v2", "v2.0.0"},
-		},
-		{
-			Name:     "goa v123",
-			Input:    "   goa.design/goa/v123    v123.4.5   ",
-			Expected: []string{"   goa.design/goa/v123    v123.4.5   ", "goa.design/goa/v123", "v123.4.5"},
-		},
-		{
-			Name:     "without version",
-			Input:    "goa.design/goa/v3",
-			Expected: nil,
-		},
-		{
-			Name:     "comment out",
-			Input:    "// goa.design/goa/v3 v3.0.0",
-			Expected: nil,
-		},
-		{
-			Name:     "irrelevant package",
-			Input:    "github.com/ikawaha/kagome v1.10.0",
-			Expected: nil,
-		},
-	}
-	for _, c := range cases {
-		if got := reMod.FindStringSubmatch(c.Input); !reflect.DeepEqual(c.Expected, got) {
-			t.Errorf("expected %+v, got %+v", c.Expected, got)
-		}
-	}
-}
diff --git a/expr/http_endpoint_test.go b/expr/http_endpoint_test.go
index 71d9bc27..f89afc6c 100644
--- a/expr/http_endpoint_test.go
+++ b/expr/http_endpoint_test.go
@@ -60,6 +60,9 @@ func TestHTTPEndpointValidation(t *testing.T) {
 		"endpoint-missing-token-extend": {
 			DSL: testdata.EndpointExtendToken,
 		},
+		"endpoint-has-parent": {
+			DSL: testdata.EndpointHasParent,
+		},
 	}
 	for name, c := range cases {
 		t.Run(name, func(t *testing.T) {
diff --git a/expr/testdata/endpoint_dsls.go b/expr/testdata/endpoint_dsls.go
index ec63beac..99f848f2 100644
--- a/expr/testdata/endpoint_dsls.go
+++ b/expr/testdata/endpoint_dsls.go
@@ -150,6 +150,39 @@ var EndpointExtendToken = func() {
 	})
 }
 
+var EndpointHasParent = func() {
+	Service("Parent", func() {
+		HTTP(func() {
+			Path("/parents")
+			CanonicalMethod("Method")
+		})
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("parent_id", Int)
+				Attribute("query_1", String)
+			})
+			HTTP(func() {
+				GET("/{parent_id}")
+				Param("query_1")
+			})
+		})
+	})
+	Service("Child", func() {
+		HTTP(func() {
+			Path("/children")
+			Parent("Parent")
+		})
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("child_id", Int)
+			})
+			HTTP(func() {
+				GET("/{child_id}")
+			})
+		})
+	})
+}
+
 var FinalizeEndpointBodyAsExtendedTypeDSL = func() {
 	var EntityData = Type("EntityData", func() {
 		Attribute("name", String)
diff --git a/grpc/codegen/testdata/request_decoder_code.go b/grpc/codegen/testdata/request_decoder_code.go
index 4cd8cd9f..089edcdc 100644
--- a/grpc/codegen/testdata/request_decoder_code.go
+++ b/grpc/codegen/testdata/request_decoder_code.go
@@ -32,9 +32,6 @@ func DecodeMethodUnaryRPCNoResultRequest(ctx context.Context, v interface{}, md
 		if message, ok = v.(*service_unary_rpc_no_resultpb.MethodUnaryRPCNoResultRequest); !ok {
 			return nil, goagrpc.ErrInvalidType("ServiceUnaryRPCNoResult", "MethodUnaryRPCNoResult", "*service_unary_rpc_no_resultpb.MethodUnaryRPCNoResultRequest", v)
 		}
-		if err := ValidateMethodUnaryRPCNoResultRequest(message); err != nil {
-			return nil, err
-		}
 	}
 	var payload []string
 	{
@@ -55,9 +52,6 @@ func DecodeMethodMessageMapRequest(ctx context.Context, v interface{}, md metada
 		if message, ok = v.(*service_message_mappb.MethodMessageMapRequest); !ok {
 			return nil, goagrpc.ErrInvalidType("ServiceMessageMap", "MethodMessageMap", "*service_message_mappb.MethodMessageMapRequest", v)
 		}
-		if err := ValidateMethodMessageMapRequest(message); err != nil {
-			return nil, err
-		}
 	}
 	var payload map[int]*servicemessagemap.UT
 	{
@@ -99,7 +93,7 @@ func DecodeMethodClientStreamingRPCWithPayloadRequest(ctx context.Context, v int
 		if vals := md.Get("goa_payload"); len(vals) == 0 {
 			err = goa.MergeErrors(err, goa.MissingFieldError("goa_payload", "metadata"))
 		} else {
-			goaPayloadRaw = vals[0]
+			goaPayloadRaw := vals[0]
 
 			v, err2 := strconv.ParseInt(goaPayloadRaw, 10, strconv.IntSize)
 			if err2 != nil {
@@ -130,7 +124,7 @@ func DecodeMethodBidirectionalStreamingRPCWithPayloadRequest(ctx context.Context
 	)
 	{
 		if vals := md.Get("a"); len(vals) > 0 {
-			aRaw = vals[0]
+			aRaw := vals[0]
 
 			v, err2 := strconv.ParseInt(aRaw, 10, strconv.IntSize)
 			if err2 != nil {
@@ -163,7 +157,7 @@ func DecodeMethodMessageWithMetadataRequest(ctx context.Context, v interface{},
 	)
 	{
 		if vals := md.Get("Authorization"); len(vals) > 0 {
-			inMetadataRaw = vals[0]
+			inMetadataRaw := vals[0]
 
 			v, err2 := strconv.ParseInt(inMetadataRaw, 10, strconv.IntSize)
 			if err2 != nil {
@@ -202,7 +196,7 @@ func DecodeMethodMessageWithValidateRequest(ctx context.Context, v interface{},
 	)
 	{
 		if vals := md.Get("Authorization"); len(vals) > 0 {
-			inMetadataRaw = vals[0]
+			inMetadataRaw := vals[0]
 
 			v, err2 := strconv.ParseInt(inMetadataRaw, 10, strconv.IntSize)
 			if err2 != nil {
diff --git a/grpc/codegen/testdata/response_decoder_code.go b/grpc/codegen/testdata/response_decoder_code.go
index 7f781f29..8ba25c53 100644
--- a/grpc/codegen/testdata/response_decoder_code.go
+++ b/grpc/codegen/testdata/response_decoder_code.go
@@ -52,9 +52,6 @@ func DecodeMethodMessageArrayResponse(ctx context.Context, v interface{}, hdr, t
 	if !ok {
 		return nil, goagrpc.ErrInvalidType("ServiceMessageArray", "MethodMessageArray", "*service_message_arraypb.MethodMessageArrayResponse", v)
 	}
-	if err := ValidateMethodMessageArrayResponse(message); err != nil {
-		return nil, err
-	}
 	res := NewMethodMessageArrayResult(message)
 	return res, nil
 }
diff --git a/grpc/codegen/testdata/server_type_code.go b/grpc/codegen/testdata/server_type_code.go
index 19ead258..b4ecc28c 100644
--- a/grpc/codegen/testdata/server_type_code.go
+++ b/grpc/codegen/testdata/server_type_code.go
@@ -22,43 +22,6 @@ func NewMethodPayloadWithNestedTypesResponse() *service_payload_with_nested_type
 	return message
 }
 
-// ValidateMethodPayloadWithNestedTypesRequest runs the validations defined on
-// MethodPayloadWithNestedTypesRequest.
-func ValidateMethodPayloadWithNestedTypesRequest(message *service_payload_with_nested_typespb.MethodPayloadWithNestedTypesRequest) (err error) {
-	if message.AParams != nil {
-		if err2 := ValidateAParams(message.AParams); err2 != nil {
-			err = goa.MergeErrors(err, err2)
-		}
-	}
-	return
-}
-
-// ValidateAParams runs the validations defined on AParams.
-func ValidateAParams(message *service_payload_with_nested_typespb.AParams) (err error) {
-	for _, v := range message.A {
-		if v != nil {
-			if err2 := ValidateArrayOfString(v); err2 != nil {
-				err = goa.MergeErrors(err, err2)
-			}
-		}
-	}
-	return
-}
-
-// ValidateArrayOfString runs the validations defined on ArrayOfString.
-func ValidateArrayOfString(message *service_payload_with_nested_typespb.ArrayOfString) (err error) {
-	if message.Field == nil {
-		err = goa.MergeErrors(err, goa.MissingFieldError("field", "message"))
-	}
-	return
-}
-
-// ValidateBParams runs the validations defined on BParams.
-func ValidateBParams(message *service_payload_with_nested_typespb.BParams) (err error) {
-
-	return
-}
-
 // protobufServicePayloadWithNestedTypespbAParamsToServicepayloadwithnestedtypesAParams
 // builds a value of type *servicepayloadwithnestedtypes.AParams from a value
 // of type *service_payload_with_nested_typespb.AParams.
diff --git a/grpc/codegen/testdata/streaming_code.go b/grpc/codegen/testdata/streaming_code.go
index 87df8ec1..8812eb50 100644
--- a/grpc/codegen/testdata/streaming_code.go
+++ b/grpc/codegen/testdata/streaming_code.go
@@ -166,9 +166,6 @@ func (s *MethodServerStreamingArrayClientStream) Recv() ([]int, error) {
 	if err != nil {
 		return res, err
 	}
-	if err = ValidateMethodServerStreamingArrayResponse(v); err != nil {
-		return res, err
-	}
 	return NewMethodServerStreamingArrayResponse(v), nil
 }
 `
@@ -191,9 +188,6 @@ func (s *MethodServerStreamingMapClientStream) Recv() (map[string]*serviceserver
 	if err != nil {
 		return res, err
 	}
-	if err = ValidateMethodServerStreamingMapResponse(v); err != nil {
-		return res, err
-	}
 	return NewMethodServerStreamingMapResponse(v), nil
 }
 `
diff --git a/grpc/middleware/trace_test.go b/grpc/middleware/trace_test.go
index 923a2fbb..6fe58c4a 100644
--- a/grpc/middleware/trace_test.go
+++ b/grpc/middleware/trace_test.go
@@ -3,6 +3,7 @@ package middleware_test
 import (
 	"context"
 	"fmt"
+	"regexp"
 	"testing"
 
 	grpcm "goa.design/goa/v3/grpc/middleware"
@@ -16,6 +17,7 @@ var (
 	spanID     = "testSpanID"
 	newTraceID = func() string { return traceID }
 	newID      = func() string { return spanID }
+	discard    = regexp.MustCompile("Test$")
 )
 
 func TestUnaryServerTrace(t *testing.T) {
@@ -26,16 +28,22 @@ func TestUnaryServerTrace(t *testing.T) {
 	cases := map[string]struct {
 		Rate                  int
 		TraceID, ParentSpanID string
+		Discard               *regexp.Regexp
 		// output
 		CtxTraceID, CtxSpanID, CtxParentID string
 	}{
-		"no-trace": {100, "", "", traceID, spanID, ""},
-		"trace":    {100, "trace", "", "trace", spanID, ""},
-		"parent":   {100, "trace", "parent", "trace", spanID, "parent"},
+		"no-trace":             {100, "", "", nil, traceID, spanID, ""},
+		"no-trace-discarded":   {100, "", "", discard, "", "", ""},
+		"trace":                {100, "trace", "", nil, "trace", spanID, ""},
+		"trace-not-discarded":  {100, "trace", "", discard, "trace", spanID, ""},
+		"parent":               {100, "trace", "parent", nil, "trace", spanID, "parent"},
+		"parent-not-discarded": {100, "trace", "parent", discard, "trace", spanID, "parent"},
 
-		"zero-rate-no-trace": {0, "", "", "", "", ""},
-		"zero-rate-trace":    {0, "trace", "", "trace", spanID, ""},
-		"zero-rate-parent":   {0, "trace", "parent", "trace", spanID, "parent"},
+		"zero-rate-no-trace":             {0, "", "", nil, "", "", ""},
+		"zero-rate-trace":                {0, "trace", "", nil, "trace", spanID, ""},
+		"zero-rate-trace-not-discarded":  {0, "trace", "", discard, "trace", spanID, ""},
+		"zero-rate-parent":               {0, "trace", "parent", nil, "trace", spanID, "parent"},
+		"zero-rate-parent-not-discarded": {0, "trace", "parent", discard, "trace", spanID, "parent"},
 	}
 	for k, c := range cases {
 		t.Run(k, func(t *testing.T) {
@@ -72,11 +80,15 @@ func TestUnaryServerTrace(t *testing.T) {
 				md.Set(grpcm.ParentSpanIDMetadataKey, c.ParentSpanID)
 			}
 			ctx := metadata.NewIncomingContext(context.Background(), md)
-
-			_, err := grpcm.UnaryServerTrace(
+			traceOptions := []middleware.TraceOption{
 				grpcm.SamplingPercent(c.Rate),
 				grpcm.SpanIDFunc(newID),
-				grpcm.TraceIDFunc(newTraceID))(ctx, "request", unary, handler)
+				grpcm.TraceIDFunc(newTraceID),
+			}
+			if c.Discard != nil {
+				traceOptions = append(traceOptions, grpcm.DiscardFromTrace(c.Discard))
+			}
+			_, err := grpcm.UnaryServerTrace(traceOptions...)(ctx, "request", unary, handler)
 			if err != nil {
 				t.Errorf("UnaryServerTrace error: %v", err)
 			}
@@ -92,16 +104,22 @@ func TestStreamServerTrace(t *testing.T) {
 	cases := map[string]struct {
 		Rate                  int
 		TraceID, ParentSpanID string
+		Discard               *regexp.Regexp
 		// output
 		CtxTraceID, CtxSpanID, CtxParentID string
 	}{
-		"no-trace": {100, "", "", traceID, spanID, ""},
-		"trace":    {100, "trace", "", "trace", spanID, ""},
-		"parent":   {100, "trace", "parent", "trace", spanID, "parent"},
+		"no-trace":             {100, "", "", nil, traceID, spanID, ""},
+		"no-trace-discarded":   {100, "", "", discard, "", "", ""},
+		"trace":                {100, "trace", "", nil, "trace", spanID, ""},
+		"trace-not-discarded":  {100, "trace", "", discard, "trace", spanID, ""},
+		"parent":               {100, "trace", "parent", nil, "trace", spanID, "parent"},
+		"parent-not-discarded": {100, "trace", "parent", discard, "trace", spanID, "parent"},
 
-		"zero-rate-no-trace": {0, "", "", "", "", ""},
-		"zero-rate-trace":    {0, "trace", "", "trace", spanID, ""},
-		"zero-rate-parent":   {0, "trace", "parent", "trace", spanID, "parent"},
+		"zero-rate-no-trace":             {0, "", "", nil, "", "", ""},
+		"zero-rate-trace":                {0, "trace", "", nil, "trace", spanID, ""},
+		"zero-rate-trace-not-discarded":  {0, "trace", "", discard, "trace", spanID, ""},
+		"zero-rate-parent":               {0, "trace", "parent", nil, "trace", spanID, "parent"},
+		"zero-rate-parent-not-discarded": {0, "trace", "parent", discard, "trace", spanID, "parent"},
 	}
 	for k, c := range cases {
 		t.Run(k, func(t *testing.T) {
@@ -140,11 +158,15 @@ func TestStreamServerTrace(t *testing.T) {
 			}
 			ctx := metadata.NewIncomingContext(context.Background(), md)
 			wss := grpcm.NewWrappedServerStream(ctx, &testServerStream{})
-
-			err := grpcm.StreamServerTrace(
+			traceOptions := []middleware.TraceOption{
 				grpcm.SamplingPercent(c.Rate),
 				grpcm.SpanIDFunc(newID),
-				grpcm.TraceIDFunc(newTraceID))(nil, wss, stream, handler)
+				grpcm.TraceIDFunc(newTraceID),
+			}
+			if c.Discard != nil {
+				traceOptions = append(traceOptions, grpcm.DiscardFromTrace(c.Discard))
+			}
+			err := grpcm.StreamServerTrace(traceOptions...)(nil, wss, stream, handler)
 			if err != nil {
 				t.Errorf("StreamServerTrace error: %v", err)
 			}
diff --git a/http/codegen/openapi/openapi_v2_builder_test.go b/http/codegen/openapi/openapi_v2_builder_test.go
new file mode 100644
index 00000000..ceaba5f2
--- /dev/null
+++ b/http/codegen/openapi/openapi_v2_builder_test.go
@@ -0,0 +1,139 @@
+package openapi
+
+import (
+	"testing"
+
+	"goa.design/goa/v3/expr"
+)
+
+func TestBuildPathFromFileServer(t *testing.T) {
+	cases := []struct {
+		path     string
+		expected string
+	}{
+		{
+			path:     "/foo",
+			expected: "/foo",
+		},
+		{
+			path:     "/foo/{bar}",
+			expected: "/foo/{bar}",
+		},
+		{
+			path:     "/foo/{*bar}",
+			expected: "/foo/{bar}",
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.path, func(t *testing.T) {
+			s := &V2{
+				Paths: make(map[string]interface{}),
+			}
+			root := &expr.RootExpr{
+				API: &expr.APIExpr{},
+			}
+			fs := &expr.HTTPFileServerExpr{
+				Service: &expr.HTTPServiceExpr{
+					ServiceExpr: &expr.ServiceExpr{
+						Name: "service",
+					},
+				},
+				RequestPaths: []string{tc.path},
+			}
+			buildPathFromFileServer(s, root, fs)
+			for actual := range s.Paths {
+				if actual != tc.expected {
+					t.Errorf("got %#v, expected %#v", actual, tc.expected)
+				}
+			}
+		})
+	}
+}
+
+func TestBuildPathFromExpr(t *testing.T) {
+	cases := map[string]struct {
+		multipartRequest bool
+		expected         Operation
+	}{
+		"multipart request": {
+			multipartRequest: true,
+			expected: Operation{
+				Consumes: []string{"multipart/form-data"},
+				Parameters: []*Parameter{
+					&Parameter{
+						In: "formData",
+					},
+				},
+			},
+		},
+		"non multipart request": {
+			multipartRequest: false,
+			expected: Operation{
+				Consumes: nil,
+				Parameters: []*Parameter{
+					&Parameter{
+						In: "body",
+					},
+				},
+			},
+		},
+	}
+	expr.Root.API = &expr.APIExpr{
+		HTTP: &expr.HTTPExpr{
+			Path: "/",
+		},
+	}
+	for k, tc := range cases {
+		t.Run(k, func(t *testing.T) {
+			s := &V2{
+				Consumes: []string{"application/json"},
+				Paths:    make(map[string]interface{}),
+			}
+			root := &expr.RootExpr{
+				API: &expr.APIExpr{},
+			}
+			h := &expr.HostExpr{}
+			route := &expr.RouteExpr{
+				Method: "POST",
+				Endpoint: &expr.HTTPEndpointExpr{
+					MethodExpr: &expr.MethodExpr{
+						Payload: &expr.AttributeExpr{},
+					},
+					Service: &expr.HTTPServiceExpr{
+						ServiceExpr: &expr.ServiceExpr{},
+						Paths:       []string{"/foo"},
+						Params:      expr.NewEmptyMappedAttributeExpr(),
+					},
+					Headers: expr.NewEmptyMappedAttributeExpr(),
+					Body: &expr.AttributeExpr{
+						Type: expr.String,
+					},
+					MultipartRequest: tc.multipartRequest,
+				},
+			}
+			basePath := "/"
+			buildPathFromExpr(s, root, h, route, basePath)
+			for _, path := range s.Paths {
+				actual := path.(*Path).Post
+				if len(actual.Consumes) != len(tc.expected.Consumes) {
+					t.Errorf("expected the number of consumes to match %d got %d", len(actual.Consumes), len(tc.expected.Consumes))
+				} else {
+					for i, v := range actual.Consumes {
+						if v != tc.expected.Consumes[i] {
+							t.Errorf("got %#v, expected %#v at index %d", v, tc.expected.Consumes[i], i)
+						}
+					}
+				}
+				if len(actual.Parameters) != len(tc.expected.Parameters) {
+					t.Errorf("expected the number of parameters to match %d got %d", len(actual.Parameters), len(tc.expected.Parameters))
+				} else {
+					for i, v := range actual.Parameters {
+						if v.In != tc.expected.Parameters[i].In {
+							t.Errorf("got %#v, expected %#v at index %d", v.In, tc.expected.Parameters[i].In, i)
+						}
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden
index 5f6a398e..a74a77d8 100644
--- a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden
+++ b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden
@@ -1,1 +1,1 @@
-{"swagger":"2.0","info":{"title":"","version":""},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"operationId":"testService#testEndpoint","responses":{"204":{"description":"No Content response."}},"schemes":["https"],"summary":"testEndpoint testService","tags":["testService"]},"x-test-foo":"bar"}}}
\ No newline at end of file
+{"swagger":"2.0","info":{"title":"","version":"","x-test-api":"API"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"operationId":"testService#testEndpoint","parameters":[{"in":"body","name":"TestEndpointRequestBody","required":true,"schema":{"$ref":"#/definitions/TestServiceTestEndpointRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointResponseBody"}}},"schemes":["https"],"summary":"testEndpoint testService","tags":["testService"],"x-test-operation":"Operation"},"x-test-foo":"bar"}},"definitions":{"TestServiceTestEndpointRequestBody":{"title":"TestServiceTestEndpointRequestBody","type":"object","properties":{"string":{"example":"","type":"string","x-test-schema":"Payload"}},"example":{"string":""}},"TestServiceTestEndpointResponseBody":{"title":"TestServiceTestEndpointResponseBody","type":"object","properties":{"string":{"example":"","type":"string","x-test-schema":"Result"}},"example":{"string":""}}},"tags":[{"description":"Description of Backend","externalDocs":{"description":"See more docs here","url":"http://example.com"},"name":"Backend","x-data":{"foo":"bar"}}]}
\ No newline at end of file
diff --git a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden
index eced3f34..cf5b3ccb 100644
--- a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden
+++ b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden
@@ -2,6 +2,7 @@ swagger: "2.0"
 info:
   title: ""
   version: ""
+  x-test-api: API
 host: goa.design
 consumes:
 - application/json
@@ -15,12 +16,50 @@ paths:
   /:
     post:
       operationId: testService#testEndpoint
+      parameters:
+      - in: body
+        name: TestEndpointRequestBody
+        required: true
+        schema:
+          $ref: '#/definitions/TestServiceTestEndpointRequestBody'
       responses:
-        "204":
-          description: No Content response.
+        "200":
+          description: OK response.
+          schema:
+            $ref: '#/definitions/TestServiceTestEndpointResponseBody'
       schemes:
       - https
       summary: testEndpoint testService
       tags:
       - testService
+      x-test-operation: Operation
     x-test-foo: bar
+definitions:
+  TestServiceTestEndpointRequestBody:
+    title: TestServiceTestEndpointRequestBody
+    type: object
+    properties:
+      string:
+        example: ""
+        type: string
+        x-test-schema: Payload
+    example:
+      string: ""
+  TestServiceTestEndpointResponseBody:
+    title: TestServiceTestEndpointResponseBody
+    type: object
+    properties:
+      string:
+        example: ""
+        type: string
+        x-test-schema: Result
+    example:
+      string: ""
+tags:
+- description: Description of Backend
+  externalDocs:
+    description: See more docs here
+    url: http://example.com
+  name: Backend
+  x-data:
+    foo: bar
diff --git a/http/codegen/testdata/openapi_v2_dsls.go b/http/codegen/testdata/openapi_v2_dsls.go
index ec582f3f..86abfdd8 100644
--- a/http/codegen/testdata/openapi_v2_dsls.go
+++ b/http/codegen/testdata/openapi_v2_dsls.go
@@ -267,21 +267,40 @@ var ArrayValidationDSL = func() {
 }
 
 var ExtensionDSL = func() {
+	var PayloadT = Type("Payload", func() {
+		Attribute("string", String, func() {
+			Example("")
+			Meta("swagger:extension:x-test-schema", "Payload")
+		})
+	})
+	var ResultT = Type("Result", func() {
+		Attribute("string", String, func() {
+			Example("")
+			Meta("swagger:extension:x-test-schema", "Result")
+		})
+	})
 	var _ = API("test", func() {
 		Server("test", func() {
 			Host("localhost", func() {
 				URI("https://goa.design")
 			})
 		})
+		Meta("swagger:extension:x-test-api", "API")
+		Meta("swagger:tag:Backend")
+		Meta("swagger:tag:Backend:desc", "Description of Backend")
+		Meta("swagger:tag:Backend:url", "http://example.com")
+		Meta("swagger:tag:Backend:url:desc", "See more docs here")
+		Meta("swagger:tag:Backend:extension:x-data", `{"foo":"bar"}`)
 	})
 	Service("testService", func() {
 		Method("testEndpoint", func() {
-			Payload(Empty)
-			Result(Empty)
+			Payload(PayloadT)
+			Result(ResultT)
 			HTTP(func() {
 				POST("/")
 				Meta("swagger:extension:x-test-foo", "bar")
 			})
+			Meta("swagger:extension:x-test-operation", "Operation")
 		})
 	})
 }
diff --git a/http/middleware/trace_test.go b/http/middleware/trace_test.go
index 67c979e8..0f7bc1a9 100644
--- a/http/middleware/trace_test.go
+++ b/http/middleware/trace_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/http"
 	"net/http/httptest"
+	"regexp"
 	"testing"
 
 	httpm "goa.design/goa/v3/http/middleware"
@@ -26,26 +27,40 @@ func TestMiddleware(t *testing.T) {
 		spanID     = "testSpanID"
 		newTraceID = func() string { return traceID }
 		newID      = func() string { return spanID }
+		discard    = regexp.MustCompile("^/$")
 	)
 
 	cases := map[string]struct {
 		Rate                  int
 		TraceID, ParentSpanID string
+		Discard               *regexp.Regexp
 		// output
 		CtxTraceID, CtxSpanID, CtxParentID string
 	}{
-		"no-trace": {100, "", "", traceID, spanID, ""},
-		"trace":    {100, "trace", "", "trace", spanID, ""},
-		"parent":   {100, "trace", "parent", "trace", spanID, "parent"},
+		"no-trace":             {100, "", "", nil, traceID, spanID, ""},
+		"no-trace-discarded":   {100, "", "", discard, "", "", ""},
+		"trace":                {100, "trace", "", nil, "trace", spanID, ""},
+		"trace-not-discarded":  {100, "trace", "", discard, "trace", spanID, ""},
+		"parent":               {100, "trace", "parent", nil, "trace", spanID, "parent"},
+		"parent-not-discarded": {100, "trace", "parent", discard, "trace", spanID, "parent"},
 
-		"zero-rate-no-trace": {0, "", "", "", "", ""},
-		"zero-rate-trace":    {0, "trace", "", "trace", spanID, ""},
-		"zero-rate-parent":   {0, "trace", "parent", "trace", spanID, "parent"},
+		"zero-rate-no-trace":             {0, "", "", nil, "", "", ""},
+		"zero-rate-trace":                {0, "trace", "", nil, "trace", spanID, ""},
+		"zero-rate-trace-not-discarded":  {0, "trace", "", discard, "trace", spanID, ""},
+		"zero-rate-parent-not-discarded": {0, "trace", "parent", discard, "trace", spanID, "parent"},
 	}
 
 	for k, c := range cases {
+		traceOptions := []middleware.TraceOption{
+			httpm.SamplingPercent(c.Rate),
+			httpm.SpanIDFunc(newID),
+			httpm.TraceIDFunc(newTraceID),
+		}
+		if c.Discard != nil {
+			traceOptions = append(traceOptions, httpm.DiscardFromTrace(c.Discard))
+		}
 		var (
-			m       = httpm.Trace(httpm.SamplingPercent(c.Rate), httpm.SpanIDFunc(newID), httpm.TraceIDFunc(newTraceID))
+			m       = httpm.Trace(traceOptions...)
 			h       = new(testHandler)
 			headers = make(http.Header)
 		)
diff --git a/middleware/trace_test.go b/middleware/trace_test.go
index 8a87687c..242013e1 100644
--- a/middleware/trace_test.go
+++ b/middleware/trace_test.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"math"
+	"regexp"
 	"testing"
 )
 
@@ -92,4 +93,22 @@ func TestNewTraceOptions(t *testing.T) {
 			}()
 		}
 	}
+
+	// invalid discard
+	{
+		cases := map[string]struct{ Discard *regexp.Regexp }{
+			"nil": {nil},
+		}
+		for k, c := range cases {
+			func() {
+				defer func() {
+					r := recover()
+					if r != "discard cannot be nil" {
+						t.Errorf("DiscardFromTrace(%s): NewTraceOptions did *not* panic as expected: %v", k, r)
+					}
+				}()
+				NewTraceOptions(DiscardFromTrace(c.Discard))
+			}()
+		}
+	}
 }