diff --git a/internal/compiler/validate_test.go b/internal/compiler/validate_test.go index 29630cf..5641ca6 100644 --- a/internal/compiler/validate_test.go +++ b/internal/compiler/validate_test.go @@ -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: `
`, + }, + }}, + } + + 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{{ @@ -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) } } @@ -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: ``, + 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{{ diff --git a/internal/gwdkanalysis/ir_contracts.go b/internal/gwdkanalysis/ir_contracts.go index 89fb173..ceddf5a 100644 --- a/internal/gwdkanalysis/ir_contracts.go +++ b/internal/gwdkanalysis/ir_contracts.go @@ -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) @@ -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 { diff --git a/internal/gwdkanalysis/ir_contracts_test.go b/internal/gwdkanalysis/ir_contracts_test.go index 5befb30..9d2d84d 100644 --- a/internal/gwdkanalysis/ir_contracts_test.go +++ b/internal/gwdkanalysis/ir_contracts_test.go @@ -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: `