Repository: goadesign/goa (go)

Issue:
V3: Problems occur when using the Parent DSL
# goa version 

v3 latest commit: cc3f94b310ab29015c3dc214fb8f1d8fee9731f0

# Following design without problems with Parent DSL.

```go
package design

import (
   . "goa.design/goa/v3/dsl"
)

var _ = Service("ParentService", func() {
   Method("show", func() {
      Payload(func(){
         Attribute("parentParam",String)
      })
      Result(Int)
      HTTP(func() {
         GET("/hoge/{parentParam}")
         Response(StatusOK)
      })
   })
})
    
var _ = Service("ChildService", func() {
   HTTP(func() {
      Parent("ParentService")
      Path("baa")
   })
   Method("child-method", func() {
      Result(Int)
      HTTP(func() {
         GET("/fuga")
         Response(StatusOK)
      })
   })
})
```


# Designs that cause problems

## Case 1

When service that does not refer to Parent is added, a problem occurs in the generated http client code.

```go

package design

import (
   . "goa.design/goa/v3/dsl"
)

var _ = Service("ParentService", func() {
   Method("show", func() {
      Payload(func(){
         Attribute("parentParam",String)
      })
      Result(Int)
      HTTP(func() {
         GET("/hoge/{parentParam}")
         Response(StatusOK)
      })
   })
})

var _ = Service("ChildService", func() {

   HTTP(func() {
      Parent("ParentService")
   })

   Method("child-method", func() {
      Result(Int)
      HTTP(func() {
         GET("/fuga")
         Response(StatusOK)
      })
   })
})

// ★ This service added.
var _ = Service("NotRelativeService", func() {
   Method("some-method", func() {
      Result(Int)
      HTTP(func() {
         GET("/zzz")
         Response(StatusOK)
      })
   })
})
```

### Error

```shellsession
exit status 1
/Users/ikawaha/go/src/github.com/ikawaha/calc/gen/http/non_relative_service/client/encode_decode.go:46:15: expected type, found ')'
...snip...
// EncodeSomeMethodRequest returns an encoder for requests sent to the
// NonRelativeService some-method server.
func EncodeSomeMethodRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, interface{}) error {
        return func(req *http.Request, v interface{}) error {
                p, ok := v.() // ←★The type required for casting is not specified.
                if !ok {
                        return goahttp.ErrInvalidType("NonRelativeService", "some-method", "", v)
                }
                body := p
                if err := encoder(req).Encode(&body); err != nil {
                        return goahttp.ErrEncodingError("NonRelativeService", "some-method", err)
                }
                return nil
        }
}
```

## Case 2

In the above design, panic occurs when the service order is changed.

ChildService -> NotRelativeService -> ParentService

```go
import (
   . "goa.design/goa/v3/dsl"
)

var _ = Service("ChildService", func() {
   HTTP(func() {
      Parent("ParentService")
   })
   Method("child-method", func() {
      Result(Int)
      HTTP(func() {
         GET("/fuga")
         Response(StatusOK)
      })
   })
})

var _ = Service("NotRelativeService", func() {
   Method("some-method", func() {
      Result(Int)
      HTTP(func() {
         GET("/zzz")
         Response(StatusOK)
      })
   })
})

var _ = Service("ParentService", func() {
   Method("show", func() {
      Payload(func(){
         Attribute("parentParam",String)
      })
      Result(Int)
      HTTP(func() {
         GET("/hoge/{parentParam}")
         Response(StatusOK)
      })
   })
})
```

### Error

```shellsession
22:08 $ ./goa gen calc/design
exit status 2
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x132acfa]

goroutine 1 [running]:
goa.design/goa/v3/expr.(*MappedAttributeExpr).Attribute(0x0, 0xc000292150)
        /Users/ikawaha/go/src/goa.design/goa/expr/mapped_attribute.go:147 +0x3a
goa.design/goa/v3/expr.(*HTTPEndpointExpr).PathParams(0xc0002906e0, 0x0)
        /Users/ikawaha/go/src/goa.design/goa/expr/http_endpoint.go:115 +0x85
goa.design/goa/v3/expr.(*HTTPEndpointExpr).Prepare(0xc000290160)
        /Users/ikawaha/go/src/goa.design/goa/expr/http_endpoint.go:180 +0x9df
goa.design/goa/v3/eval.prepareSet(0xc000229bc0, 0x3, 0x4, 0x0, 0x0)
        /Users/ikawaha/go/src/goa.design/goa/eval/eval.go:201 +0xae
goa.design/goa/v3/expr.(*RootExpr).WalkSets(0x1bd1e00, 0x1717290)
        /Users/ikawaha/go/src/goa.design/goa/expr/root.go:118 +0xab0
goa.design/goa/v3/eval.RunDSL(0x7ffeefbff66b, 0x1)
        /Users/ikawaha/go/src/goa.design/goa/eval/eval.go:44 +0x260
main.main()
        /Users/ikawaha/go/src/github.com/ikawaha/calc/goa305431394/main.go:55 +0x1bf
```

Pull request:
Handle child HTTP service headers and params.
Handle potentially non-initialized data structures when computing headers and
params for endpoints of services with parents.

Fix #2342

Patch:
diff --git a/expr/http_endpoint.go b/expr/http_endpoint.go
index 04756d68..5e7264ed 100644
--- a/expr/http_endpoint.go
+++ b/expr/http_endpoint.go
@@ -57,6 +57,9 @@ type (
 		// Meta is a set of key/value pairs with semantic that is
 		// specific to each generator, see dsl.Meta.
 		Meta MetaExpr
+		// prepared is true if Prepare has been run. This field is required to
+		// avoid infinite recursions.
+		prepared bool
 	}
 
 	// RouteExpr represents an endpoint route (HTTP endpoint).
@@ -163,6 +166,18 @@ func (e *HTTPEndpointExpr) QueryParams() *MappedAttributeExpr {
 // Prepare computes the request path and query string parameters as well as the
 // headers and body taking into account the inherited values from the service.
 func (e *HTTPEndpointExpr) Prepare() {
+	// Avoid infinite recursions when traversing parents.
+	if e.prepared {
+		return
+	}
+	e.prepared = true
+	if e.Headers == nil {
+		e.Headers = NewEmptyMappedAttributeExpr()
+	}
+	if e.Params == nil {
+		e.Params = NewEmptyMappedAttributeExpr()
+	}
+
 	// Inherit headers and params from parent service and API
 	headers := NewEmptyMappedAttributeExpr()
 	headers.Merge(Root.API.HTTP.Headers)
@@ -174,6 +189,7 @@ func (e *HTTPEndpointExpr) Prepare() {
 
 	if p := e.Service.Parent(); p != nil {
 		if c := p.CanonicalEndpoint(); c != nil {
+			c.Prepare()
 			if !e.HasAbsoluteRoutes() {
 				headers.Merge(c.Headers)
 				params.Merge(c.PathParams())


Test patch:
diff --git a/expr/http_endpoint_test.go b/expr/http_endpoint_test.go
index 4229e295..e9d1483a 100644
--- a/expr/http_endpoint_test.go
+++ b/expr/http_endpoint_test.go
@@ -31,6 +31,69 @@ func TestHTTPRouteValidation(t *testing.T) {
 	}
 }
 
+func TestHTTPEndpointPrepare(t *testing.T) {
+	cases := map[string]struct {
+		DSL     func()
+		Headers []string
+		Params  []string
+		Error   string
+	}{
+		"valid": {
+			DSL:    testdata.ValidRouteDSL,
+			Params: []string{"base_id", "id"},
+		},
+		"with parent": {
+			DSL:     testdata.EndpointWithParentDSL,
+			Headers: []string{"pheader", "header"},
+			Params:  []string{"pparam", "param"},
+		},
+		"with parent revert": {
+			DSL:     testdata.EndpointWithParentRevertDSL,
+			Headers: []string{"pheader", "header"},
+			Params:  []string{"pparam", "param"},
+		},
+		"error": {
+			DSL:   testdata.EndpointRecursiveParentDSL,
+			Error: "service \"Parent\": Parent service Child is also child\nservice \"Child\": Parent service Parent is also child",
+		},
+	}
+	for n, c := range cases {
+		t.Run(n, func(t *testing.T) {
+			if c.Error == "" {
+				root := expr.RunDSL(t, c.DSL)
+				e := root.API.HTTP.Services[len(root.API.HTTP.Services)-1].HTTPEndpoints[0]
+
+				ht := expr.AsObject(e.Headers.AttributeExpr.Type)
+				if len(*ht) != len(c.Headers) {
+					t.Errorf("got %d headers, expected %d", len(*ht), len(c.Headers))
+				} else {
+					for _, n := range c.Headers {
+						if ht.Attribute(n) == nil {
+							t.Errorf("header %q is missing", n)
+						}
+					}
+				}
+
+				pt := expr.AsObject(e.Params.AttributeExpr.Type)
+				if len(*pt) != len(c.Params) {
+					t.Errorf("got %d params, expected %d", len(*pt), len(c.Params))
+				} else {
+					for _, n := range c.Params {
+						if pt.Attribute(n) == nil {
+							t.Errorf("param %q is missing", n)
+						}
+					}
+				}
+			} else {
+				err := expr.RunInvalidDSL(t, c.DSL)
+				if err.Error() != c.Error {
+					t.Errorf("got error %q, expected %q", err.Error(), c.Error)
+				}
+			}
+		})
+	}
+}
+
 func TestHTTPEndpointValidation(t *testing.T) {
 	cases := map[string]struct {
 		DSL    func()
diff --git a/expr/testdata/endpoint_dsls.go b/expr/testdata/endpoint_dsls.go
index 2837e10b..d304d6dc 100644
--- a/expr/testdata/endpoint_dsls.go
+++ b/expr/testdata/endpoint_dsls.go
@@ -37,6 +37,91 @@ var DuplicateWCRouteDSL = func() {
 	})
 }
 
+var EndpointWithParentDSL = func() {
+	Service("Parent", func() {
+		Method("show", func() {
+			Payload(func() {
+				Attribute("pparam", String)
+				Attribute("pheader", String)
+			})
+			HTTP(func() {
+				POST("/{pparam}")
+				Header("pheader")
+			})
+		})
+	})
+	Service("Child", func() {
+		HTTP(func() {
+			Parent("Parent")
+		})
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("param", String)
+				Attribute("header", String)
+				Attribute("pheader", String)
+			})
+			HTTP(func() {
+				POST("/{param}")
+				Header("header")
+			})
+		})
+	})
+}
+
+var EndpointWithParentRevertDSL = func() {
+	Service("Child", func() {
+		HTTP(func() {
+			Parent("Parent")
+		})
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("param", String)
+				Attribute("header", String)
+				Attribute("pheader", String)
+			})
+			HTTP(func() {
+				POST("/{param}")
+				Header("header")
+			})
+		})
+	})
+	Service("Parent", func() {
+		Method("show", func() {
+			Payload(func() {
+				Attribute("pparam", String)
+				Attribute("pheader", String)
+			})
+			HTTP(func() {
+				POST("/{pparam}")
+				Header("pheader")
+			})
+		})
+	})
+}
+
+var EndpointRecursiveParentDSL = func() {
+	Service("Parent", func() {
+		HTTP(func() {
+			Parent("Child")
+		})
+		Method("show", func() {
+			HTTP(func() {
+				POST("/")
+			})
+		})
+	})
+	Service("Child", func() {
+		HTTP(func() {
+			Parent("Parent")
+		})
+		Method("show", func() {
+			HTTP(func() {
+				POST("/")
+			})
+		})
+	})
+}
+
 var EndpointBodyAsPayloadProp = func() {
 	Service("Service", func() {
 		Method("Method", func() {