Skip to content
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
50 changes: 49 additions & 1 deletion internal/compiler/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3621,6 +3621,31 @@ func TestValidateManifestRejectsInvalidContractReferenceRoutes(t *testing.T) {
}
}

func TestValidateManifestRejectsDefaultContractRouteOnDynamicPage(t *testing.T) {
app := appFixture{
Pages: []gwdkir.Page{{
Package: "pages",
ID: "blog.show",
Route: "/blog/{slug}",
Source: "pages/blog-show.page.gwdk",
Blocks: gwdkir.Blocks{
Paths: true,
View: true,
ViewBody: `<form g:command="posts.CreateComment"></form>`,
},
}},
}

err := validateManifest(gowdk.Config{}, app)
if err == nil {
t.Fatal("expected invalid dynamic default contract route diagnostic")
}
diagnostics := err.(ValidationErrors)
if !hasDiagnosticMessage(diagnostics, "contract_route_invalid", "dynamic page route", "/blog/{slug}") {
t.Fatalf("Missing dynamic default contract_route_invalid diagnostic: %#v", diagnostics)
}
}

func TestValidateManifestRejectsDefaultQueryRouteWithDynamicParams(t *testing.T) {
app := appFixture{
Pages: []gwdkir.Page{{
Expand All @@ -3643,7 +3668,7 @@ func TestValidateManifestRejectsDefaultQueryRouteWithDynamicParams(t *testing.T)
if !hasDiagnosticCode(diagnostics, "contract_route_invalid") {
t.Fatalf("Missing contract_route_invalid diagnostic: %#v", diagnostics)
}
if !strings.Contains(diagnostics[0].Message, "without query, fragment, or params") {
if !strings.Contains(diagnostics[0].Message, "dynamic page route") {
t.Fatalf("unexpected diagnostic message: %s", diagnostics[0].Message)
}
}
Expand Down Expand Up @@ -3820,6 +3845,29 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) {
}
})

t.Run("default command route conflicts with inherited action route", func(t *testing.T) {
app := appFixture{
Pages: []gwdkir.Page{{
ID: "patients",
Route: "/patients",
Blocks: gwdkir.Blocks{
View: true,
ViewBody: `<main><form g:command="patients.CreatePatient"></form></main>`,
Actions: []gwdkir.Action{{Name: "Save"}},
},
}},
}

err := validateManifest(gowdk.Config{}, app)
if err == nil {
t.Fatal("expected default command/action route method conflict")
}
diagnostics := err.(ValidationErrors)
if !hasDiagnosticMessage(diagnostics, "route_method_conflict", "POST", "/patients", "command contract patients.CreatePatient", "action patients.Save") {
t.Fatalf("Missing default command/action route_method_conflict diagnostic: %#v", diagnostics)
}
})

t.Run("duplicate command routes conflict", func(t *testing.T) {
app := appFixture{
Pages: []gwdkir.Page{{
Expand Down
18 changes: 16 additions & 2 deletions internal/gwdkanalysis/ir_contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,22 @@ func appendContractReferences(program *gwdkir.Program, template gwdkir.Template)
method := source.BackendRouteMethod(ref.Method)
path := ref.Path
if path == "" && template.Route != "" {
if ref.Kind == view.ContractReferenceQuery {
routeIsDynamic := routeHasDynamicParams(template.Route)
if routeIsDynamic {
program.Diagnostics = append(program.Diagnostics, gwdkir.Diagnostic{
Code: "contract_route_invalid",
Source: template.Source,
Span: templateOffsetSpan(template, ref.Start, ref.End),
Message: fmt.Sprintf("%s %s must declare an explicit route on dynamic page route %q", irContractReferenceKind(ref.Kind), ref.Name, template.Route),
})
} else if ref.Kind == view.ContractReferenceQuery {
method = "GET"
} else if method == "" {
method = "POST"
}
path = template.Route
if !routeIsDynamic {
path = template.Route
}
}
importAlias, contractType := splitContractReferenceName(ref.Name)
importPath := contractReferenceImportPath(template.Imports, importAlias)
Expand All @@ -51,6 +61,10 @@ func appendContractReferences(program *gwdkir.Program, template gwdkir.Template)
}
}

func routeHasDynamicParams(route string) bool {
return strings.Contains(route, "{")
}

func splitContractReferenceName(name string) (string, string) {
before, after, ok := strings.Cut(name, ".")
if !ok {
Expand Down
32 changes: 32 additions & 0 deletions internal/gwdkanalysis/ir_contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,38 @@ func TestBuildProgramDerivesPageOwnedContractRoutes(t *testing.T) {
}
}

func TestBuildProgramRejectsDynamicPageOwnedDefaultContractRoutes(t *testing.T) {
program := BuildProgram(gowdk.Config{}, Sources{Pages: []gwdkir.Page{{
Package: "pages",
ID: "blog.show",
Route: "/blog/{slug}",
Blocks: gwdkir.Blocks{
Paths: true,
View: true,
ViewBody: `<main>
<form g:command="posts.CreateComment"></form>
<section g:query="posts.GetComments"></section>
</main>`,
},
}}})

refs := contractRefsByName(program.ContractRefs)
if ref := refs["posts.CreateComment"]; ref.Method != "POST" || ref.Path != "" {
t.Fatalf("dynamic default command method/path = %s %s, want POST with empty non-routable path", ref.Method, ref.Path)
}
if ref := refs["posts.GetComments"]; ref.Method != "" || ref.Path != "" {
t.Fatalf("dynamic default query method/path = %s %s, want empty non-routable metadata", ref.Method, ref.Path)
}
if len(program.Diagnostics) != 2 {
t.Fatalf("expected two dynamic default contract route diagnostics, got %#v", program.Diagnostics)
}
for _, diagnostic := range program.Diagnostics {
if diagnostic.Code != "contract_route_invalid" {
t.Fatalf("expected contract_route_invalid diagnostic, got %#v", diagnostic)
}
}
}

func TestBuildProgramKeepsComponentQueryNonRoutable(t *testing.T) {
program := BuildProgram(gowdk.Config{}, Sources{Components: []gwdkir.Component{{
Package: "components",
Expand Down