Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a DSL Redirect #2830

Merged
merged 7 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions dsl/http_redirect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dsl

import (
"goa.design/goa/v3/eval"
"goa.design/goa/v3/expr"
)

// Redirect indicates that HTTP requests reply to the request with a redirect.
// The logic is the same as the standard http package Redirect function.
//
// Redirect must appear in a HTTP endpoint expression or a HTTP file server
// expression.
//
// Redirect accepts 2 arguments. The first argument is the URL that is being
// redirected to. The second argument is the HTTP status code.
//
// Example:
//
// var _ = Service("service", func() {
// Method("method", func() {
// HTTP(func() {
// GET("/resources")
// Redirect("/redirect/dest", StatusMovedPermanently)
// })
// })
// })
//
// var _ = Service("service", func() {
// Files("/file.json", "/path/to/file.json", func() {
// Redirect("/redirect/dest", StatusMovedPermanently)
// })
// })
//
func Redirect(url string, code int) {
redirect := &expr.HTTPRedirectExpr{
URL: url,
StatusCode: code,
}
switch actual := eval.Current().(type) {
case *expr.HTTPEndpointExpr:
redirect.Parent = actual
actual.Redirect = redirect
case *expr.HTTPFileServerExpr:
redirect.Parent = actual
actual.Redirect = redirect
default:
eval.IncompatibleDSL()
}
}
20 changes: 19 additions & 1 deletion expr/http_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ type (
// MultipartRequest indicates that the request content type for
// the endpoint is a multipart type.
MultipartRequest bool
// Redirect defines a redirect for the endpoint.
Redirect *HTTPRedirectExpr
// Meta is a set of key/value pairs with semantic that is
// specific to each generator, see dsl.Meta.
Meta MetaExpr
Expand Down Expand Up @@ -261,7 +263,9 @@ func (e *HTTPEndpointExpr) Prepare() {
// Make sure there's a default success response if none define explicitly.
if len(e.Responses) == 0 {
status := StatusOK
if e.MethodExpr.Result.Type == Empty && !e.SkipResponseBodyEncodeDecode {
if e.Redirect != nil {
status = e.Redirect.StatusCode
} else if e.MethodExpr.Result.Type == Empty && !e.SkipResponseBodyEncodeDecode {
status = StatusNoContent
}
e.Responses = []*HTTPResponseExpr{{StatusCode: status}}
Expand Down Expand Up @@ -342,6 +346,20 @@ func (e *HTTPEndpointExpr) Validate() error {
}
}

// Redirect is not compatible with Response.
if e.Redirect != nil {
found := false
for _, r := range e.Responses {
if r.StatusCode != e.Redirect.StatusCode {
found = true
break
}
}
if found {
verr.Add(e, "Endpoint cannot use Response when using Redirect.")
}
}

// Validate routes

// Routes cannot be empty
Expand Down
2 changes: 2 additions & 0 deletions expr/http_file_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type (
FilePath string
// RequestPaths is the list of HTTP paths that serve the assets.
RequestPaths []string
// Redirect defines a redirect for the endpoint.
Redirect *HTTPRedirectExpr
// Meta is a list of key/value pairs
Meta MetaExpr
}
Expand Down
29 changes: 29 additions & 0 deletions expr/http_redirect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package expr

import (
"fmt"

"goa.design/goa/v3/eval"
)

type (
// HTTPRedirectExpr defines an endpoint that replies to the request with a redirect.
HTTPRedirectExpr struct {
// URL is the URL that is being redirected to.
URL string
// StatusCode is the HTTP status code.
StatusCode int
// Parent expression, one of HTTPEndpointExpr or HTTPFileServerExpr.
Parent eval.Expression
}
)

// EvalName returns the generic definition name used in error messages.
func (r *HTTPRedirectExpr) EvalName() string {
suffix := fmt.Sprintf("redirect to %s with status code %d", r.URL, r.StatusCode)
var prefix string
if r.Parent != nil {
prefix = r.Parent.EvalName() + " "
}
return prefix + suffix
}
23 changes: 13 additions & 10 deletions http/codegen/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,25 @@ import (
func TestHandlerInit(t *testing.T) {
const genpkg = "gen"
cases := []struct {
Name string
DSL func()
Code string
Name string
DSL func()
Code string
FileCount int
}{
{"no payload no result", testdata.ServerNoPayloadNoResultDSL, testdata.ServerNoPayloadNoResultHandlerConstructorCode},
{"payload no result", testdata.ServerPayloadNoResultDSL, testdata.ServerPayloadNoResultHandlerConstructorCode},
{"no payload result", testdata.ServerNoPayloadResultDSL, testdata.ServerNoPayloadResultHandlerConstructorCode},
{"payload result", testdata.ServerPayloadResultDSL, testdata.ServerPayloadResultHandlerConstructorCode},
{"payload result error", testdata.ServerPayloadResultErrorDSL, testdata.ServerPayloadResultErrorHandlerConstructorCode},
{"no payload no result", testdata.ServerNoPayloadNoResultDSL, testdata.ServerNoPayloadNoResultHandlerConstructorCode, 2},
{"no payload no result with a redirect", testdata.ServerNoPayloadNoResultWithRedirectDSL, testdata.ServerNoPayloadNoResultWithRedirectHandlerConstructorCode, 1},
{"payload no result", testdata.ServerPayloadNoResultDSL, testdata.ServerPayloadNoResultHandlerConstructorCode, 2},
{"payload no result with a redirect", testdata.ServerPayloadNoResultWithRedirectDSL, testdata.ServerPayloadNoResultWithRedirectHandlerConstructorCode, 2},
{"no payload result", testdata.ServerNoPayloadResultDSL, testdata.ServerNoPayloadResultHandlerConstructorCode, 2},
{"payload result", testdata.ServerPayloadResultDSL, testdata.ServerPayloadResultHandlerConstructorCode, 2},
{"payload result error", testdata.ServerPayloadResultErrorDSL, testdata.ServerPayloadResultErrorHandlerConstructorCode, 2},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
RunHTTPDSL(t, c.DSL)
fs := ServerFiles(genpkg, expr.Root)
if len(fs) != 2 {
t.Fatalf("got %d files, expected two", len(fs))
if len(fs) != c.FileCount {
t.Fatalf("got %d files, expected %d", len(fs), c.FileCount)
}
sections := fs[0].SectionTemplates
if len(sections) < 7 {
Expand Down
26 changes: 20 additions & 6 deletions http/codegen/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func serverEncodeDecodeFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.F
}

for _, e := range data.Endpoints {
if !isWebSocketEndpoint(e) {
if e.Redirect == nil && !isWebSocketEndpoint(e) {
sections = append(sections, &codegen.SectionTemplate{
Name: "response-encoder",
FuncMap: transTmplFuncs(svc),
Expand Down Expand Up @@ -342,7 +342,11 @@ func {{ .MountServer }}(mux goahttp.Muxer{{ if .Endpoints }}, h *{{ .ServerStruc
{{ .MountHandler }}(mux, h.{{ .Method.VarName }})
{{- end }}
{{- range .FileServers }}
{{- if .IsDir }}
{{- if .Redirect }}
{{ .MountHandler }}(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "{{ .Redirect.URL }}", {{ .Redirect.StatusCode }})
}))
{{- else if .IsDir }}
{{ .MountHandler }}(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upath := path.Clean(r.URL.Path)
rpath := upath
Expand Down Expand Up @@ -407,29 +411,35 @@ func {{ .HandlerInit }}(
configurer goahttp.ConnConfigureFunc,
{{- end }}
) http.Handler {
{{- if (or (mustDecodeRequest .) (not (or .Redirect (isWebSocketEndpoint .))) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
var (
{{- end }}
{{- if mustDecodeRequest . }}
decodeRequest = {{ .RequestDecoder }}(mux, decoder)
{{- end }}
{{- if not (isWebSocketEndpoint .) }}
{{- if not (or .Redirect (isWebSocketEndpoint .)) }}
encodeResponse = {{ .ResponseEncoder }}(encoder)
{{- end }}
{{- if (or (mustDecodeRequest .) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
encodeError = {{ if .Errors }}{{ .ErrorEncoder }}{{ else }}goahttp.ErrorEncoder{{ end }}(encoder, formatter)
{{- end }}
{{- if (or (mustDecodeRequest .) (not (or .Redirect (isWebSocketEndpoint .))) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
)
{{- end }}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept"))
ctx = context.WithValue(ctx, goa.MethodKey, {{ printf "%q" .Method.Name }})
ctx = context.WithValue(ctx, goa.ServiceKey, {{ printf "%q" .ServiceName }})

{{- if mustDecodeRequest . }}
payload, err := decodeRequest(r)
{{ if .Redirect }}_{{ else }}payload{{ end }}, err := decodeRequest(r)
if err != nil {
if err := encodeError(ctx, w, err); err != nil {
errhandler(ctx, w, err)
}
return
}
{{- else }}
{{- else if not .Redirect }}
var err error
{{- end }}
{{- if isWebSocketEndpoint . }}
Expand All @@ -451,9 +461,12 @@ func {{ .HandlerInit }}(
{{- else if .Method.SkipRequestBodyEncodeDecode }}
data := &{{ .ServicePkgName }}.{{ .Method.RequestStruct }}{ {{ if .Payload.Ref }}Payload: payload.({{ .Payload.Ref }}), {{ end }}Body: r.Body }
res, err := endpoint(ctx, data)
{{- else if .Redirect }}
http.Redirect(w, r, "{{ .Redirect.URL }}", {{ .Redirect.StatusCode }})
{{- else }}
res, err := endpoint(ctx, {{ if .Payload.Ref }}payload{{ else }}nil{{ end }})
{{- end }}
{{- if not .Redirect }}
if err != nil {
{{- if isWebSocketEndpoint . }}
if _, ok := err.(websocket.HandshakeError); ok {
Expand All @@ -465,11 +478,12 @@ func {{ .HandlerInit }}(
}
return
}
{{- end }}
{{- if .Method.SkipResponseBodyEncodeDecode }}
o := res.(*{{ .ServicePkgName }}.{{ .Method.ResponseStruct }})
defer o.Body.Close()
{{- end }}
{{- if not (isWebSocketEndpoint .) }}
{{- if not (or .Redirect (isWebSocketEndpoint .)) }}
if err := encodeResponse(ctx, w, {{ if and .Method.SkipResponseBodyEncodeDecode .Result.Ref }}o.Result{{ else }}res{{ end }}); err != nil {
errhandler(ctx, w, err)
{{- if .Method.SkipResponseBodyEncodeDecode }}
Expand Down
10 changes: 6 additions & 4 deletions http/codegen/server_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ func TestServerHandler(t *testing.T) {
Name string
DSL func()
Code string
FileCount int
SectionNum int
}{
{"server simple routing", testdata.ServerSimpleRoutingDSL, testdata.ServerSimpleRoutingCode, 7},
{"server trailing slash routing", testdata.ServerTrailingSlashRoutingDSL, testdata.ServerTrailingSlashRoutingCode, 7},
{"server simple routing", testdata.ServerSimpleRoutingDSL, testdata.ServerSimpleRoutingCode, 2, 7},
{"server trailing slash routing", testdata.ServerTrailingSlashRoutingDSL, testdata.ServerTrailingSlashRoutingCode, 2, 7},
{"server simple routing with a redirect", testdata.ServerSimpleRoutingWithRedirectDSL, testdata.ServerSimpleRoutingCode, 1, 7},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
RunHTTPDSL(t, c.DSL)
fs := ServerFiles(genpkg, expr.Root)
if len(fs) != 2 {
t.Fatalf("got %d files, expected 1", len(fs))
if len(fs) != c.FileCount {
t.Fatalf("got %d files, expected %d", len(fs), c.FileCount)
}
sections := fs[0].SectionTemplates
if len(sections) < 8 {
Expand Down
1 change: 1 addition & 0 deletions http/codegen/server_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func TestServerInit(t *testing.T) {
{"multiple endpoints", testdata.ServerMultiEndpointsDSL, testdata.ServerMultiEndpointsConstructorCode, 2, 3},
{"multiple bases", testdata.ServerMultiBasesDSL, testdata.ServerMultiBasesConstructorCode, 2, 3},
{"file server", testdata.ServerFileServerDSL, testdata.ServerFileServerConstructorCode, 1, 3},
{"file server with a redirect", testdata.ServerFileServerWithRedirectDSL, testdata.ServerFileServerConstructorCode, 1, 3},
{"mixed", testdata.ServerMixedDSL, testdata.ServerMixedConstructorCode, 2, 3},
{"multipart", testdata.ServerMultipartDSL, testdata.ServerMultipartConstructorCode, 2, 4},
{"streaming", testdata.StreamingResultDSL, testdata.ServerStreamingConstructorCode, 3, 3},
Expand Down
17 changes: 11 additions & 6 deletions http/codegen/server_mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ func TestServerMount(t *testing.T) {
Name string
DSL func()
Code string
FileCount int
SectionNum int
}{
{"multiple files constructor", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesConstructorCode, 6},
{"multiple files mounter", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesMounterCode, 9},
{"multiple files constructor /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathConstructorCode, 6},
{"multiple files mounter /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathMounterCode, 9},
{"simple routing constructor", testdata.ServerSimpleRoutingDSL, testdata.ServerSimpleRoutingConstructorCode, 2, 6},
{"simple routing with a redirect constructor", testdata.ServerSimpleRoutingWithRedirectDSL, testdata.ServerSimpleRoutingConstructorCode, 1, 6},
{"multiple files constructor", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesConstructorCode, 1, 6},
{"multiple files mounter", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesMounterCode, 1, 9},
{"multiple files constructor /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathConstructorCode, 1, 6},
{"multiple files mounter /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathMounterCode, 1, 9},
{"multiple files with a redirect constructor", testdata.ServerMultipleFilesWithRedirectDSL, testdata.ServerMultipleFilesWithRedirectConstructorCode, 1, 6},
{"multiple files with a redirect mounter", testdata.ServerMultipleFilesWithRedirectDSL, testdata.ServerMultipleFilesMounterCode, 1, 9},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
RunHTTPDSL(t, c.DSL)
fs := ServerFiles(genpkg, expr.Root)
if len(fs) != 1 {
t.Fatalf("got %d files, expected 1", len(fs))
if len(fs) != c.FileCount {
t.Fatalf("got %d files, expected %d", len(fs), c.FileCount)
}
sections := fs[0].SectionTemplates
if len(sections) < 6 {
Expand Down
27 changes: 27 additions & 0 deletions http/codegen/service_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ type (
// ServerWebSocket holds the data to render the server struct which
// implements the server stream interface.
ServerWebSocket *WebSocketData
// Redirect defines a redirect for the endpoint.
Redirect *RedirectData

// client

Expand Down Expand Up @@ -183,6 +185,16 @@ type (
// PathParam is the name of the parameter used to capture the
// path for file servers that serve files under a directory.
PathParam string
// Redirect defines a redirect for the endpoint.
Redirect *RedirectData
}

// RedirectData lists the data needed to generate a redirect.
RedirectData struct {
// URL is the URL that is being redirected to.
URL string
// StatusCode is the HTTP status code.
StatusCode string
}

// PayloadData contains the payload information required to generate the
Expand Down Expand Up @@ -595,12 +607,20 @@ func (d ServicesData) analyze(hs *expr.HTTPServiceExpr) *ServiceData {
if s.IsDir() {
pp = expr.ExtractHTTPWildcards(s.RequestPaths[0])[0]
}
var redirect *RedirectData
if s.Redirect != nil {
redirect = &RedirectData{
URL: s.Redirect.URL,
StatusCode: statusCodeToHTTPConst(s.Redirect.StatusCode),
}
}
data := &FileServerData{
MountHandler: scope.Unique(fmt.Sprintf("Mount%s", codegen.Goify(s.FilePath, true))),
RequestPaths: paths,
FilePath: s.FilePath,
IsDir: s.IsDir(),
PathParam: pp,
Redirect: redirect,
}
rd.FileServers = append(rd.FileServers, data)
}
Expand Down Expand Up @@ -825,6 +845,13 @@ func (d ServicesData) analyze(hs *expr.HTTPServiceExpr) *ServiceData {
ad.BuildStreamPayload = scope.Unique("Build" + codegen.Goify(ep.Name, true) + "StreamPayload")
}

if a.Redirect != nil {
ad.Redirect = &RedirectData{
URL: a.Redirect.URL,
StatusCode: statusCodeToHTTPConst(a.Redirect.StatusCode),
}
}

rd.Endpoints = append(rd.Endpoints, ad)
}

Expand Down
Loading